Ein Operationscode (opcode) der JVM wird repräsentiert durch ein vorzeichenloses
Byte. Jedem dieser Bytes ist eine Mnemonik zugerordnet. Der vollständige Befehlssatz
der Java Virtual Machine wird innerhalb der
Java Virtual Machine Specification
detailliert beschrieben.
Kennzeichnung und Wertigkeit von JVM-Befehlen
Ein JVM-Befehl besteht aus einem Operationscode, der angibt, welche Operation
ausgeführt werden soll und aus möglicherweise darauffolgenden Operanden, die für
die Operation benötigt werden. Mögliche Operanden-Bytes folgen direkt im Anschluss
an das Byte, das den Opcode repräsentiert. Bei datenabhängigen Operationen
werden die Operationsbezeichnungen (Mnemoniks) mit Buchstaben gekennzeichnet. So
bedeuted die Abkürzung iadd
, dass zwei Werte vom Typ int
addiert werden, wohingegen fadd
eine Addition zweier
float
-Werte vornimmt. Die folgende Übersicht zeigt die Zuordnung der
verwendeten Buchstaben zum Datentyp:
Als Beispiele für JVM-Befehle mit unteschiedlicher Anzahl von Bytes
(Operanden-Bytes) sollen einige Befehle genannt werden. Die folgenden Anweisungen
besitzen der Reihe nach kein Operanden-Byte, ein Operanden-Byte und zwei
Operanden-Bytes:
Der Befehl iadd
ist einwertig, d.h. er wird im Bytecode durch
ein einzelnes Byte repräsentiert. Der genannte Additionsbefehl setzt voraus, dass
sich die zu addierenden Zahlenwerte auf dem Operandenstack befinden. Erscheint also
innerhalb einer Klassendatei (Methodenbereich) die Zahl 60 (hex.) dann werden
automatisch zwei Zahlen, die im Integer-Format vorliegen müssen, vom Stapel geholt,
addiert und das Ergebnis wird anschließend wieder auf den Operandenstack gelegt.
Da der Befehlssatz der JVM viele einwertige Befehle besitzt kann ein Programm
relativ kompakt in einer Klassendatei repräsentiert werden.
Beschreibung des Operandenstapels
Bei der Beschreibung von Befehlen, die mit Hilfe des Operandenstacks
abgearbeitet werden, ist es hilfreich eine Oprandenstack-Beschreibung zu nennen.
Für den Additonsbefehl iadd
würde eine derartige Beschreibung des Stapels wie folgt lauten:
Operandenstapel bei iadd
: ..., value1,
value2 --> ..., result
Der Wert value2 liegt vor der Addition (linke Seite vom Pfeil) an oberster
Stelle des Stapels. Nach der Addition (rechte Seite vom Pfeil) befindet sich
value1 und value2 nicht mehr auf dem Operandenstack, sondern nur das Endergebnis
result. Die drei aufeinanderfolgenden Punkte in der Darstellung stehen für Werte,
die sich bereits auf dem Stapel befinden aber für die betrachtete Operation nicht
interessant sind. Die folgende Übersicht zeigt ein Additionsbeispiel, wobei
der Programmausschnitt innerhalb einer statischen main
-Methode steht.
Die JVM-Befehle, die diesen Quelltext realisieren, sind in der zweiten Spalte
aufgeführt. Die einzelnen Anweisungen haben Auswirkungen auf den Frame der Methode.
Die beiden Elemente Operandenstapel und Array der lokalen Variablen des Frames
sind in den weiteren Spalten notiert, wobei die Änderungen auf diese Elemente
sichtbar werden. Im genannten Array steht bei Index 0 eine Referenz auf das bekannte
String
-Array, das einer main
-Methode als einziges
Argument übergeben wird. Das dargestellte Array der lokalen Variablen besteht
aus vier Elementen, wobei bei Index 1 die Variable value1, bei Index 2 die Variable
value2 und bei Index 3 das Ergebnis result der Berechnung eingetragen ist.
Wichtige Gruppen von JVM-Befehlen
Die folgende Tabelle zeigt eine Übersicht über wichtige JVM-Befehle
(Mnemoniks bzw. Operationscode) und deren Bedeutung. Einige dieser Befehle können
zu Gruppen zusammengefasst werden.
Tabelle 4.6. Auswahl einiger Befehle (Mnemoniks und Operationscodes) der Java Virtual
Machine und deren Bedeutung.
Mnemonik |
Opcode |
Bedeutung |
nop |
0 |
Keine Operation (no operation). |
aconst_null |
1 |
Lege eine null Objekt
reference auf den Operandenstapel.
|
iconst_m1 |
2 |
Lege die int -Konstante -1 auf den
Operandenstapel.
|
iconst_0, ..., iconst_5 |
3, ..., 8 |
Lege int -Konstante (0 bzw. 1,
2, 3, 4, 5) auf den Operandenstapel. Für Werte größer als 5 bzw. kleiner
als 0 wird der Befehl bipush bereitgestellt (z.B.
bipush 9 , es werden zwei Bytes für diesen Befehl benötigt).
Der Operand (z.B. die Zahl 3) ist bei diesen Befehlen implizit und es ist
lediglich ein Byte für den entsprechenden Befehl nötig.
|
lconst_0, lconst_1 |
9, 10 |
Lege long -Konstante (0 bzw. 1)
auf den Operandenstapel.
|
fconst_0, ..., fconst_2 |
11, ..., 13 |
Lege float -Konstante (0.0 bzw.
1.0, 2.0) auf den Operandenstapel.
|
dconst_0, dconst_1 |
14, 15 |
Lege double -Konstante (0.0 bzw.
1.0) auf den Operandenstapel.
|
bipush, sipush |
16, 17 |
Lege nachfolgendes Byte bzw. Short auf den
Operandenstack. Dabei wird das Byte bzw. Short (setzt sich aus zwei
nachfolgenden Bytes zusammen) zunächst in einen int -Wert
umgewandelt und dieser wird anschließend auf den Operandenstack gelegt.
|
iload, iload_0, ..., iload_3 |
21, 26, ..., 29 |
Lege einen int -Wert von einer
lokalen Variable auf den Operandenstapel. Bei z.B. iload_3
ist der Index (3) der lokalen Variable im zugehörigen Array (Stichwort:
Frame einer Methode) implizit im Befehl codiert (es wird nur ein Byte
benötigt). Der Bytecode von z.B. iload 9 besteht aus zwei
Bytes und lädt die lokale Variable mit Index 9 auf den Stack.
|
istore, istore_0, ..., istore_3 |
54, 59, ..., 62 |
Speichere den int -Wert, der an
oberster Stelle des Stapels liegt, in das Array der lokalen Variablen.
Der Index dieses Arrays ist bei den 1-Byte Befehlen (z.B. istore_3 )
entsprechend angegeben und bei istore wird der Index im
nachfolgenden Byte codiert (z.B. istore 9 ; der Bytecode dieses
Befehls wäre 36 09 ; hexadezimal 36 entspricht dezimal 54).
|
pop |
87 |
Nehme oberstes Element vom Operandenstapel. |
dup |
89 |
Verdoppele das oberste Element des
Operandenstapels (an der Spitze des Stapels befinden sich nach der
Operation zwei gleiche Elemente).
|
iadd, ladd, fadd, dadd |
96, ..., 99 |
Nehme die beiden obersten Elemente vom
Operandenstack und addiere diese. Lege danach das Ergebnis auf den Stack.
Der Buchstabe vor dem add gibt den jeweiligen Typ der Addition bzw. der
Operanden an.
|
ifeq, ifne, iflt, ifge, ifgt, ifle |
153, ..., 158 |
Verzweige, je nach Vergleich des obersten
Elements (vom Typ int ) des Operandenstapels mit dem Wert 0.
|
goto |
167 |
Unbedingte Verzweigung. |
ireturn, lreturn, freturn, dreturn,
areturn, return |
172, ..., 177 |
Gebe einen Rückgabewert an den Methodenaufrufer
zurück.
|
getstatic, putstatic, getfield, putfield
|
178, ..., 181 |
Setzen bzw. auslesen von Instanzvariablen
und Klassenvariablen (statische Variablen).
|
invokevirtual, invokespecial, invokestatic,
invokeinterface |
182, ..., 185 |
Aufrufe von Methoden.
|
Typkategorien
Bei einigen JVM-Befehlen wie z.B.
pop
muss der Typ des
Wertes, der sich an oberster Stelle des Operandenstapels befindet, aus einer
bestimmten Typkategorie stammen. Bei JVM-Befehlen wie z.B.
pop2
wird je nachdem
welchen Typ die Werte des Operandenstapels haben unterschiedlich verfahren. Die
folgende Tabelle zeigt die Zuordnung der tatsächlichen JVM-Typen zu den bei der
Abarbeitung der Befehle (interner Typ) verwendeten Typen:
Tabelle 4.7. Zuordnung: Tatsächlicher Typ - intern verwendeter Typ - Typkategorie.
Tatsächlicher Typ |
Interner Typ |
Typkategorie |
boolean |
int |
1 |
byte |
int |
1 |
char |
int |
1 |
short |
int |
1 |
int |
int |
1 |
float |
float |
1 |
reference |
reference |
1 |
returnAddress |
returnAddress |
1 |
long |
long |
2 |
double |
double |
2 |
Es liegen z.B. die Typen boolean
und byte
bei
der Abarbeitung von Befehlen in einer int
-Repräsentation vor (siehe
dazu bastore
und
baload
).
Ablegen von ganzen Zahlen auf dem Operandenstapel
Unter Verwendung des Datentyps long
können ganzzahlige Werte
im Bereich von -263 bis 263-1
gespeichert werden. Wird einer Variablen von einem bestimmten ganzzahligen Datentyp
ein entsprechender Zahlenwert zugewiesen, ist die Übersetzung dieser Anweisung in
Bytecode abhängig vom konkreten Datentyp der Variablen und dem Zahlenwert. Die
Initialisierung einer Variablen mit einem Wert, z.B. mit der auführbaren Anweisung
int a = 2;
, wird auf Bytecodeebene dadurch realisiert, dass zunächst
der Zahlenwert 2 auf einen entsprechenden Operandenstapel gelegt wird, wobei je
nach Größe der Zahl unterschiedliche JVM-Befehle für diese Operation verwendet
werden. Die folgende Übersicht zeigt verschiedene Zahlenbereiche und die JVM-Befehle,
die bei der Compilierung erzeugt werden:
Es kann z.B. für die Zuweisung des Zahlenwertes 2 an eine Variable sowohl
der JVM-Befehl iconst_2
als auch ldc2_w
durch den
Übersetzer erzeugt werden, abhängig vom Typ der Variablen (z.B. byte
oder int
bzw. long
). Mit lconst_0
und
lconst_1
werden zwei Ein-Byte-Befehle für das Ablegen der
Long-Konstanten 0 bzw. 1 auf den Operandenstapel zur Verfügung gestellt, analog
zu den entsprechenden Integer-Befehlen iconst_0
und iconst_1
.
Die folgenden kurzen Beispiele zeigen die Übersetzungen durch den Java-Compiler
bei unterschiedlichen Datentypen:
byte a = 2; 0: iconst_2
1: istore_1
int a = 2; 0: iconst_2
1: istore_1
long a = 2; 0: ldc2_w #2; //long 2L
3: lstore_1
long a = 1; 0: lconst_1
1: lstore_1
Für das Ablegen von Gleitkommazahlen enthält der JVM-Befehlssatz die
einzelnen Ein-Byte-Befehle fconst_x
und dconst_x
(x
ist Platzhalter für einfache Konstanten wie z.B. 1.0). Alle anderen
Gleitkommazahlen werden ebenfalls mit ldc
bzw. ldc_w
und ldc2_w
auf den Stapel gelegt, indem auf eine entsprechende
Konstante im Konstantenpool verwiesen wird.
Binärdarstellung einer Gleitkommazahl (32 Bit)
Die Binärdarstellung der Gleitkommazahl 3.0 durch 32 Bit ist wie folgt
aufgebaut:
Bit 31: 0 Vorzeichen (1 Bit)
Bits 30-23: 10000000 Exponent (8 Bits)
Bits 22-0: 1000000 00000000 00000000 Mantisse (23 Bits)
Ist das 31. Bit (Vorzeichenbit) in der Binärdarstellung 0, dann handelt es
sich um eine positive Zahl. Bei einer negativen Gleitkommazahl ist das höchstwertige
Bit 1. Mit der folgenden Gleichung können die Bits 30-23 der Binärdarstellung
einer Gleitkommazahl ermittelt werden:
Zunächst wäre für x die im Beispiel verwendete Zahl 3.0 zu setzen. Der
Logarithmus von 3.0 zur Basis 2 lautet dann 1.5850 (gerundet). Die beiden Zeichen,
die den Logarithmusausdruck einschließen, entsprechen Math.floor
;
für Math.floor(1.5850)
ist das Ergebnis die ganze Zahl 1 und damit
e = 1. Der Zahlenwert e wird jedoch nicht direkt durch die 8 Exponentenbits codiert,
sondern es wird zu diesem Wert noch ein sogenannter Biaswert
addiert, den die Norm IEEE 754 mit B = 127 festlegt (single bzw. 32 Bit). Durch
die Verwendung eines Biaswertes (dt. Verzerrungswert) können die 8 Exponentenbits
ohne Vorzeichen gewertet werden und der darstellbare Dezimalzahlenbereich geht von
0 bis 255. Es ergibt sich im Beispiel:
e = 1
E = e + B = 1 + 127 = 128
= (10000000)b
Nachdem die Bits 30-23 mit 10000000
ermittelt wurden, soll nun
der unterste Bitblock (Mantisse) mit Hilfe der folgenden Gleichung errechnet
werden:
Wird x durch den Beispielwert 3.0 und e durch den zuvor errechneten Wert 1
ersetzt, führt dies über das Zwischenergebnis (1.5 - 1) * 223
zu:
m = 4194304
= (1000000 00000000 00000000)b
Werden nun die drei ermittelten Bitblöcke zusammengesetz, ergibt sich die
binäre Darstellung (einfache Genauigkeit bzw. 32 Bit) der Gleitkommazahl 3.0:
0 10000000 10000000000000000000000
Die binäre Darstellung der Zahl 3.0 kann mit Hilfe der folgenden Anweisungen
gewonnen werden:
int a = Float.floatToIntBits(3.0f);
String s = Integer.toBinaryString(a);
System.out.println(s);
Führende Nullen werden bei der Ausgabe nicht beachtet, d.h. Bit 31 wird
nicht mit ausgegeben, da es den Wert 0 besitzt.
Umrechnung: 32 Bits einer Gleitkommazahl - Dezimalzahlenwert
Die Binärdarstellung der Gleitkommazahl -3.7 durch 32 Bit ist wie folgt
aufgebaut:
Bit 31: 1 Vorzeichen (1 Bit)
Bits 30-23: 10000000 Exponent (8 Bits)
Bits 22-0: 1101100 11001100 11001101 Mantisse (23 Bits)
Das Vorzeichen der negativen Zahl -3.7 wird in der Binärdarstellung durch
Setzen von Bit 31 berücksichtigt. Mit der folgenden Gleichung kann eine gegebene
Bitfolge, die eine Gleitkommazahl repräsentiert, in einen Dezimalzahlenwert z
umgerechnet werden:
Der Wert v, der das Vorzeichen der Gleitkommazahl bestimmt, wird durch Bit
31 festgelegt. Zur Ermittlung von e tragen die 8 Exponentenbits 10000000
und ein sogenannter Biaswert (dt. Verzerrungswert) bei, der
durch IEEE 754 mit B = 127 festgelegt ist (single bzw. 32 Bit). Der Zahlenwert
für m wird binär durch die Bits 22-0 repräsentiert. Die drei Größen zur Berechnung
von z ergeben sich wie folgt:
v = (1)b = 1
e = E - B
= (10000000)b - 127 = 128 - 127
= 1
m = (1101100 11001100 11001101)b
= 7130317
Werden die ermittelten Zahlen in die entsprechende Gleichung eingesetzt,
ergibt sich über das Zwischenergebnis
z = (-1)1 * (1.0 + 7130317 / 8388608) * 2 das auf
vier Nachkommastellen gerundete Endergebnis zu:
z = -3.7000
Umrechnung der Wertemengen von Gleitkommazahlen (Value Set
Conversion)
Bei der Abarbeitung von float
- und double
-Typen
wird unter bestimmten Bedingungen eine Value Set Conversion
(dt. Umrechnung der Wertemenge) vorgenommen, falls eine Umrechnung zwischen
verschiedenen Wertebereichen der Gleitkommazahlen möglich bzw. notwendig ist.
Grundlage einer "Value Set Conversion" ist die Norm IEEE 754, die
Standarddarstellungen für binäre Gleitkommazahlen definiert und u.a. Verfahren
für die Durchführung mathematischer Operationen festlegt. IEEE 754 definiert die
beiden Grundformate für binäre Gleitkommazahlen 32 Bit (single precision) und 64
Bit (double precision) und zwei erweiterte Formate.
Diese Umwandlung vom Gleitkommawerten ist abhängig von der Implementierung der
virtuellen Java-Maschine. Eine JVM, die einen erweiterten Wertebereich für
Gleitkommazahlen unterstützt, kann bzw. muss eine Gleitkommazahl einem erweiterten
Wertebereich bzw. einem Standardwertebereich zuordnen können.
Bei dieser Umrechnung von Wertemengen wird der Typ float
bzw.
double
nicht geändert. Eine Umrechnung wird vorgenommen wenn ein Wert
vom Typ float
bzw. double
kein Element eines festgelegten
float
- bzw. double
-Wertebereiches ist. Der ursprüngliche
Wert wird dann dem am nächsten liegenden Wert innerhalb des geforderten
Wertebereiches zugeordnet.