Seminarthemen→ Die Architektur der Java VM→ I.• II.• III.• IV. Die Java VM zur Laufzeit • V.• VI.• VII.
In diesem Kapitel werden die Befehle der JVM in Beispielen verwendet, um einzelne Abläufe aufzuzeigen. Sofern eizelne Befehle unbekannt sind, sollten sie im Kapitel V. Befehlssatz nachgeschlagen werden. Dieses ist als Nachschlagewerk gedacht und deshalb diesem Kapitel nachgestellt. Der in den Beispielen angegebenen Bytecode
-Assembler wurde mit dem Disassembler javap
erzeugt. Kapitel VI. enthält weiter Hinweise zu diesem Werkzeug. Die Beispiele basieren auf einigen Java-Klassen, die dazu erstellt wurden. Diese können im Abschnitt Beispieldateien auch heruntergeladen werden.
Der Runtime Constant Pool ist eine Laufzeit-Datenstruktur in der Mehtode-Area, die beim Laden einer Klasse von der JVM erzeugt wird. Der RCP konnte im Kapitel II. nicht detailiert betrachtet werden, da der Aufbau des Constant Pools, aus dem der RCP abgeleitet wird, erst im Kapitel III. betrachtet wurde.
Bei der schematischen Betrachtung des Dateiformats hat sich bereits gezeigt, dass der Dateiaufbau ungeeignet ist, um bestimmte Felder performant aufzufinden und auszulesen. Der RCP ist jedoch ein essentieller Bestandteil des dynamischen Linkens von Methoden, Feldern und Klassen, und muss daher möglichst performant ein bestimmten Eintrag im RCP auffinden. Der RCP übernimmt dabei die Nummerierung der Einträge des CP, da Befehle, die als Operanden Einträge des (R)CP verwenden, diese über den numerischen Index referenzieren.
Die Funktionsweise des RCPs wird anhand einer Beispielimplementierung gut verdeutlicht. Die performanteste Datenstruktur, um mit einem numerischen Index ein bestimmtes Element aufzufinden, ist zweifellos ein Array, welches an dieser Stelle auch von den allermeisten Implementierung verwendet werden dürfte. Die Implementierungen unterscheiden sich wohl nur im verwendeten Elementtyp des Arrays. Als Beispiel wird dieser so definiert, dass er denen der Arrays gleicht, die für die lokalen Variabeln oder den Operand-Stack des JVM-Stacks verwendet werden. Das RCP ist
long
- und double
-Werte belegen 2 Elemente.reference
) aufgelöst und mit diesen ersetzt.
Bei der Erzeugung des RCP werden die Konstanten aus dem CP typabhängig (tag
) in eine 32-Bit breite Laufzeitdarstellung umgewandelt:
int, long, float, double
) umgewandelt, und direkt im Array abgelegt. Wie bei LV und OS auch, müssen hier für long
und double
zwei aufeinanderfolgende Elemente verwendet werden.String
erzeugt, und eine reference
darauf im Array abgelegt.String
(entspricht Class.getName()
). Eine reference
darauf wird im Array abgelegt. Beim dynmaischen Linken, wird die symbolische Referenz mit einer reference
auf die Class
-Instanz der beannten Klasse ersetzt. Kann der Name nicht aufgelöst werden, wird die symbolische Referenz mit einer reference
auf die ausgelöste Exception ersetzt.SymbolicRef
erzeugt, und eine reference
darauf im Array abgelegt.
class SymbolicRef { // Klasse ist nur ein Beispiel, existiert nicht wirklich!
final String class; // interner Name der definierenden Klasse
final String name; // Name des Felds, der Methode
final String descriptor; // Descriptor des Felds, der Methode
}
Beim dynamischen Linken wird zunächst die Klasse analog zum CONSTANT_Class
-Eintrag aufgeöst. Die Referenz auf die SymbolicRef
-Instanz wird dann mit einer reference
auf das Feld bzw. die Methode ersetzt. Kann Klasse, Feld oder Methode nicht aufgelöst werden, oder ist der Zugriff aus der Klasse nicht erlaubt, wird eine entsprechende Exception ausgelöst und eine reference
darauf anstelle der SymbolicRef
im Array abgelegt.
Die CONSTANT_Utf8
und CONSTANT_NameAndType
Einträge im CP werden nur indirekt bei der Erzeugung des RCP verwendet. Wenn der Compiler diese Konstanten immer ans Ende des CPs legt, kann der RCP auch weniger Elemente enthalten, da alle zur Laufzeit verwendeten Konstanten immernoch an ihrem ursprünglichen Index liegen. Natürlich ist der beschiebene Aufbau des RCP nur eine mögliche Implementierung, die aber gut zeigt, wie der RCP funktioniert, und auch recht praxisnah ist. Im Beispiel ist es nicht nötig, zusätzliche Typ- oder Zustandsinformationen über die im RCP enthaltenen Werte anzulegen. Grundsätzlich ist die Typsicherheit und Typkompatibilität bereits durch die Verifikation sichergestellt, und muss zur Laufzeit nicht wiederholt überprüft werden. Die Execution Engine kann daher davon ausgehen, dass:
String
-Konstanten verwendete Elemente eine reference
auf den String
enthalten.
Bei Klassen, Feldern und Methoden werden zunächst symbolische (namentliche) Referenzen hinterlegt. Das es sich um einen solches Element handeln muss, ergibt sich aus dem Befehl, der auf den RCP zugreift. Für diese Elemente kann die Execution Engine die richtige Aktion abhängig von der Klasse des (durch die enthaltene reference
) referenzierten Objektes bestimmen:
String
→ Es handelt sich um eine symbolische Referenz auf eine Klasse; Auflösen, Wert ersetzen und verwenden.Class
→ Bereits erfolgreich aufgelöste Klasse; Wert verwenden.SymbolicRef
→ Symbolische Feld- oder Methodenreferenz; Auflösen, Wert ersetzen und verwenden.Field, Method
→ Bereits erfolgreich aufgelöstes Feld/Methode; Wert verwenden.Exception
→ Auflösung fehgeschlagen; Exception erneut auslösen.Auch dies ist natürlich nur eine Möglichkeit der Verarbeitung, die wieder gut zeigt, wie die JVM arbeitet. Spezifiziert ist dabei, dass
reference
zurückliefert.In der Beispielimplementierung ist dieses Verhalten impliziet enthalten.
Das Beispiel zum Runtime Constant Pool hat die Vorgänge des dynamischen Linkens mit Fokus auf den RCP beschrieben. Dieser Abschnitt betrachtet den Ablauf des dynamsichen Auflösens von symbolischen Referenzen detailierter.
Eine symbolische Referenz referenziert ein Feld, eine Klasse oder Methode durch Benennung ihres Namens als Text eines String
-Objektes:
java/lang/String
, de/fhw/MyClass
usw.String
-Objekten, hier für Länge einer Liste: int length;
)length
I
java.util.LinkedList
(wie symb. Klassenreferenz)
String
-Objekten, hier für die Object.equals
-Methode): equals
(Ljava/lang/Object;)Z
java/lang/Object
(wie symb. Klassenreferenz)
Die symbolischen Referenzen werden beim Laden einer Klasse aus dem CP der class
-Datei erzeugt. Die JVM läd jedoch keine Klasse bevor es nicht notwenig wird. Was das bedeutet, wird beim Betrachten der Auflösung der symbolischen Referenzen deutlich. Jedes Java-Programm wird durch Aufruf einer Klasse, die eine main
-Methode enthält, gestartet. Dies ist (abgesehen von den System-Klassen wie Class, Object, String
usw.) die erste Klasse, die von der JVM geladen wird. Sie refernziet in CP weitere Klasse die jedoch nicht umgehend geladen werden. Erst wenn während der Ausführung des Programms ein Zugriff in den RCP einer bereits geladenen Klasse erfolgt, der dort eine symbolische Referenz enthält, wird versucht, diese zu einer Laufzeit-Referenz auf das gesuchte Objekt (Class
, Field
, Method
) aufzulösen. Dazu kann das Laden einer symbolisch referenzierten Klasse nötig sein. Evt. wurde diese auch schon geladen, um eine andere erforderliche Klasse zu verifizieren. Findet die Execution Engine bei der Programmausführung eine symbolische Referenz im RCP vor, werden (abhängig von der Art der Referenz) folgende Schritte durchgeführt:
ClassLoader
zu laden. Schlägt dies fehl, merken und Exception auslösen.ClassLoader
gelieferten Bytearrays.<clinit>
, Initalwerte der Felder setzen...)reference
auf die Class
-Instanz der benannten Klasse ersetzen (und diese zur weiteren Verarbeitung nutzen)referenz
auf die Field
- bzw. Method
-Instanz ersetzen (und diese zur weiteren Verarbeitung nutzen)
Ein Feld oder eine Methode ist damit noch nicht vollständigt bestimmt, denn eine symbolische Referenz benennt jeweils die definierende Klasse eines Feldes oder einer Methode. Beide lassen sich jedoch bekanntlich in abgeleiteten Klassen überschreiben oder im Falle eines Interfaces überhaupt erst implementieren. Daher wird bei der Verarbeitung des Programms immer abhängig von der konkreten Instanz (this
) oder bei statischen Methoden und Feldern der aktuellen Klasse des Frames gebunden. Der dabei verwendeten Algorithmus wird für Methoden im Abschnitt Methodenaufruf und Rückgabe betrachtet.
Für die Erzeugung von Objekten, also Instanzen von reference
-Typen, existiert analog zur Java-Sprache der Befehl new
. Dieser alloziert jedoch lediglich einen ausreichend großen Speicherblock auf dem Heap und legt eine reference
darauf auf dem OS ab.
void createInstance() {
new String("No hay banda! There is no band. It is all an illusion... ");
}
Anschließend muss noch die Initalisierungsmethode (analog: Konstruktor) aufgerufen werden, um die neue Instanz zu initalisieren. Die Java-Methode createInstance()
sieht deassembliert so aus:
void createInstance();
0: new #20; // Speicher für Instanz von java/lang/String allozieren
3: ldc #22; // String aus CP:"No hay banda! There is no band. It is all an illusion..."
5: invokespecial #24; // Methode java/lang/String.<init>:(Ljava/lang/String;)V aufrufen
8: return // Frame von createInstance ohne Rückgabewert beenden
Grundsätzlich läuft die Erzeugung neuer Insatnzen nach diesem Schema ab:
new
alloziert neuen Speicher für die Instanzdup
wird nun die reference
auf dem OS verdoppelt)<init>
auf den OS gebracht.invokespecial
aufgeruen.Die Verdopplung der Instanzreferenz wird gewöhnlich verwendet, damit nach dem Aufruf der Initialisierungsmethode, die eine Objektreferenz verbraucht, noch eine Referenz auf das neue Objekt auf dem OS liegt, mit welcher dies etwa einem Feld zugewiesen werden kann.
Arrays werden nicht über den new
-Befehl erzeugt. Es gibt eine Reihe unterschielicher Befehle zur Erzeugung von Arrays, je nachdem welchen Elementtyp und Dimentsionen des Arrays verwendet werden:
Befehl | Beschreibung |
---|---|
newarray | Erzeugt ein eindimentsionales Array eines primitiven Datentyps. |
anewarray | Erzeugt ein eindimentsionales Array eines reference -Typs. |
multianewarray | Erzeugt ein mehrdimentsionales Array eines reference -Typs. |
Es gibt jeweils ein get- und ein put-Befehl für statische (getstatic
, putstatic
) und nicht statische Felder (getfield
, putfield
), da der Wert nicht statischer Felder von einer konkreten Instanz (z.B this
) abhängig ist (dynamisches binden).
public final static int ANSWER = 42;
private int question;
void getAndPutFields() {
this.question = Example.ANSWER;
}
Die Java-Methode getAndPutFields()
entspricht deassembliert:
void getAndPutFields();
0: aload_0 // this-Referenz aus LV 0 auf OS (für putfield-Befehl)
1: getstatic #8; // Konstante 42 auf OS (Compiler-Optimierung: bipush 42)
3: putfield #28; // Wert vom OS in Feld question:I der this-Instanz ablegen
6: return
Zuerst wird die this
-Referenz, die bei nicht statischen Methoden immer in der lokalen Variable 0 übergeben wird, auf den OS gebracht. Danach wird der Wert der Konstanten ANSWER
auf den OS abgelegt. Da diese innerhalb der gleichen Klasse liegt, wie die getAndPutFields
-Methode, hat der Compiler diese Operation durch bipush 42
ersetzt, welche schneller ausgeführt werden kann, da kein Zugriff auf den RCP nötig ist. Bei nicht final
-Feldern oder dem Zugriff aus anderen Klasse wird das Feld (wie im Beispiel) mit dem getstatic
-Befehl ausgelesen. Anschließend wird der Wert im referenzierten Feld der this
-Instanz abgelegt.
Es gibt eine ganze Reihe verschiedener Befehle um Konstanten auf den OS abzulegen. Der Wert kann dabei:
Bytecode
direkt nachfolgen (byte
- oder short
-Wertebereich; für alle Ganzzahltypen)String
-Instanzen).Alle Befehle sind zudem Typabhängig. Das heißt, für jeden Typ existieren seperate Befehle. Außerdem gibt es einige Sonderbefehle, die mit konstanten numerischen Werten versehen werden.
void accessConstants() {
int i = 1;
i += 100;
i *= 10;
i *= 300;
float f = 1.0f;
f += 100.0f;
double d = 1.0;
d += 100.0d;
}
Die Java-Methode accessConstants()
führt einige (sinnlose) Berechnungen mit Konstanten durch, die jedoch mit unterschiedlichen Befehlen auf den OS gebracht werden, wie die deassemblierte Form zeigt (zur Orientierung mit LocalVariableTable
):
void accessConstants();
0: iconst_1 // int 1 auf den OS (Wert im Befehl)
1: istore_1 // vom OS in LV1(i) ablegen
2: iinc 1, 100 // LV1(i) direkt (ohne OS) um 100 erhöhen
5: iload_1 // Wert von LV1(i) auf OS
6: bipush 10 // int 10 (im Bytecode als 1 Byte nach Befehl) auf OS
8: imul // i * 10 auf OS
9: istore_1 // und wieder vom OS in LV1(i) ablegen
10: iload_1 // erneut LV1(i) auf OS
11: sipush 300 // int 300 (im Bytecode als 2 Bytes nach Befehl) auf OS
14: imul // i * 300 auf OS
15: istore_1 // und nocheinmal vom OS in LV1(i) ablegen
16: fconst_1 // float 1.0 auf OS (Wert im Befehl)
17: fstore_2 // vom OS in LV2(f) ablegen
18: fload_2 // Wert von LV2(f) auf OS
19: ldc #31; // float 100.0f aus (R)CP #33 auf OS
21: fadd // f * 100.0 auf OS
22: fstore_2 // und wieder in LV2(f) ablegen
23: dconst_1 // double 1.0 auf PS (Wert im Befehl)
24: dstore_3 // vom OS in LV3(d) ablegen
25: dload_3 // Wert von LV3(d) auf OS
26: ldc2_w #32; // double 100.0d aus (R)CP #34 auf OS (2 Elemente)
29: dadd // d + 100.0 auf OS
30: dstore_3 // wieder vom OS in LV3(d) ablegen
31: return
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 this LExample;
2 30 1 i I
18 14 2 f F
25 7 3 d D
Der Ablauf ist durch die Kommentare sicherlich ausreichend beschreiben. Im Fokus stehen die verschiedene Befehle, die genutzt werden, um eine Konstante auf den OS zu bringen. Für kleine Konstanten existieren für jeden primitiven Datentyp einige Befehle in denen der Wert, der auf den OS gebracht wird, impliziet enthalten ist (hier: iconst_1
, fconst_1
, dconst_1
). Ganzzahlige Konstanten im byte
- oder short
-Wertebereich können mit dem bipush
bzw. sipush
direkt als Wert im Bytecode
kodiert und auf den OS gebracht werden. Für größere Ganzzahlwerte oder Fließkommakonstanten werden entsprechende Einträge im (R)CP angelegt. Konstanten vom Typ byte, short, int, char, float, String
werden dann mit ldc
(Index 0-255) oder ldc_w
(Index 0-65536) auf den OS gebracht. Für die 64-Bit breiten long
- und double
-Konstanten wird ldc2_w
(Index 0-65536) verwendet. Zudem kann über aconst_null
die null
-Referenz auf den OS abgelegt werden. Da für den Vergleich gegen null
die Befehle ifnull
und ifnonnull
existieren, musste der Wert der null
-Referenz nicht spezifiziert werden, und kann somit von jeder Implementierung selbst bestimmt werden.
Die JVM unterscheidet vier verschiedene Befehle zum Aufruf einer Methode. Welcher Befehl verwendet werden muss, hängt von der Art der Methode und den damit verbundenen Schritten der JVM ab. Die Beispiele stammen zunächst alle aus der Klasse Example
.
static
)
Die einfachste Form bilden statische Methoden, da sie wie der Name sagt, keine Dynamik beinhalten, müssen sie nicht zur Laufzeit an eine bestimmte Instanz gebunden werden, sondern sind immer Teil einer zur Compile-Zeit bekannten Klasse, die bereits durch den CONSTANT_Methodref
-Eintrag im CP der aufrufenden Klasse eindeutig und endgültig bestimmt und nicht instanzabhängig ist.
void invokeStatic() {
Example.mul(6, 7);
}
public static int mul(int i1, int i2) {
return i1 * i2;
}
Der Aufruf der statischen Methode mul
in der Methode invokeStatic()
zeigt sich deassembliert so:
void invokeStatic();
0: bipush 6 // int 6 auf OS
2: bipush 7 // int 7 auf OS
4: invokestatic #40; // Methode mul:(II)I aufrufen
7: pop // Ergebnis vom OS, da dieser bei Verlassen leer sein muss
8: return // und das Ergebnis hier nicht weiter verarbeitete wird
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LExample;
Eine statischen Methode wird Aufgerufen, indem
invokestatic
im CP referenziert wird.Die JVM verarbeitet den Aufruf, indem dann
public static int mul(int, int);
Stack=2, Locals=2, Args_size=2
0: iload_0 // int 6 aus LV0 auf OS
1: iload_1 // int 7 aus LV1 auf OS
2: imul // Ergebnis von 6 * 7 auf den OS
3: ireturn // Ergebnis vom OS dieses Frames auf OS des aufrufenden Frames
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 i1 I // wird hier mit int 6 initialisiert
0 4 1 i2 I // wird hier mit int 7 initialisiert
Nachdem die Methode ausgeführt wurde, wird die Kontrolle zurück zum aufrufenden Frame transferiert, indem
Die statische Initialisierungsmethode von Klassen <clinit>
wird nicht über invokestatic
aufgerufen. Die JVM ruft diese Methode implizit bei der Initialisierung einer Klasse auf, wenn deren interne Darstellung in der Methode-Area angelegt wird.
public, protected, default
)Die zweite Form eines Methodenaufrufs betrifft alle Methoden, die nicht zu einem Interface gehören, und von außerhalb der Klasse selbst aufgerufen oder auch überschrieben werden können. Die jeweils richtige Implementierung muss hier immer abhängig von einer konkreten aktuellen Objekt-Instanz bestimmt werden.
void invokePublic() {
sub(2048, 2006);
}
public int sub(int i1, int i2) {
return i1 - i2;
}
Der Aufruf der nicht statischen Methode sub
in der Methode invokePublic()
entspricht deassembliert:
void invokePublic();
0: aload_0 // Instanz der Klasse (für welche die Methode aufgerufen wird) auf OS
1: sipush 2048 // Parameter auf OS
4: sipush 2006
7: invokevirtual #56; // Methode sub:(II)I aufrufen
10: pop // Ergebnis weiterverarbeiten
11: return
public int sub(int, int);
Stack=2, Locals=3, Args_size=3
0: iload_1
1: iload_2
2: isub
3: ireturn
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this LExample;
0 4 1 i1 I
0 4 2 i2 I
Der Kontrolltransfer und das Erzeugen und Beenden von Frames geschieht analog zu statischen Methoden, abgesehen davon, dass
reference
auf die Objekt-Instanz auf den OS der aufrufenden Methode gebracht wird, für welche die Methode aufgerufen werden soll, was durch invokevirtual
-Befehl angezeigt wird.this
hinterlegt ist.
Die durch invokevirtual
referenzierte Methode im (R)CP ergibt erst zusammen mit der beim Aufruf angegebenen Objekt-Instanz eine eindeutige Methodenimplementierung, die durch ein Lookup bestimmt werden muss. Der Algorithmus wird später noch betrachtet.
public abstract
)
Für den Aufruf einer Interface-Methode wird die dritte Form des Methodenaufrufs verwendet. Diese unterscheidet sich in der Form leicht von der invokevirtual
-Methode, da hier als weiterer Operand die Anzahl der Methodenparameter angegeben wird. Diese ließe sich ebensogut (wie bei anderen Methodenaufrufen auch) aus dem Descriptor bestimmen. Die Angabe war einst von Sun als Geschwindigkeitsoptimierung gedacht, und muss daher auch weiterhin angegeben werden. Dem Befehls-Opcode folgen daher hier immer 4 Byte, die zur Laufzeit z.B. auch durch eine zuvor bestimmte Methodenreferenz ersetzt werden könnten, ohne dass sich dadurch die Länge und Offsets der Befehle einer Methode änderte.
int invokeInterface(IExample obj) {
return obj.doSomething(42);
}
Der Aufruf der Methode doSomething
der Instanz obj
in der Methode Example.invokeInterface
zeigt sich dementsprechend deassembliert so (Example
implementiert IExample
nicht selbst):
int invokeInterface(IExample);
Stack=2, Locals=2, Args_size=2
0: aload_1 // obj-Referenz aus LV 1 auf OS
1: bipush 42 // Methodenparameter auf OS
3: invokeinterface #61, 2; // Interface-Methode IExample.doSomething:(I)I aufrufen
8: ireturn
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LExample;
0 9 1 obj LIExample;
Natürlich muss auch für Interface-Methoden ein Lookup ausgeführt werden, da die Methoden-Implementierung von der konkreten Instanz (hier: obj
) abhängt. Dieses unterscheidet sich vom Lookup beim invokevirtual
-Befehl nur durch die Sonderbedingung, dass für alle Interface-Methoden bereits bekannt ist, dass die Methode überall aufgerufen werden kann.
private, super,
Konstruktoren)
Die letzte Form des Methodenaufrufs betrifft Methoden, die zwar auch auf Instanzen angewendet werden, für welche jedoch bekannt ist, dass die Implementierung direkt in der benannten Klasse bzw. einer Superklasse dieser Klasse enthalten ist. Die folgenden Beispiele verwenden die Klassen Near
und Far
.
class Near {
float value;
...
private float getValue() {
return this.value;
}
public float getValueNear() {
return getValue();
}
}
Der Aufruf der privaten Methode getValue()
in der öffentlichen Methode getValueNear()
sieht deassembliert so aus:
public float getValueNear();
0: aload_0 // this aus LV 0 auf OS
1: invokespecial #22; // Methode this.getValue:()F aufrufen
4: freturn // Ergebnis zurückgeben
Für private Methoden ist bereits bekannt, dass die Implementierung in der Klasse von this
enthalten ist. Ein Lookup entfällt daher, weshalb der invokespecial
-Berfehl verwendet wird. Dieser wird auch genutzt, wenn eine Initialisierungsmethode <init>
aufgerufen wird, von welcher ebenso bekannt ist, dass die Implementierung in der durch die beim Methodenaufruf verwendeten Instanz eindeutig und endgültig bestimmt ist.
Außerdem wird der Befehl verwendet, um Methoden der Superklasse aufzurufen. Hier muss zwar ein Lookup stattfinden, das jedoch bei der direkten Superklasse der Insatnz-Klasse begonnen werden kann.
class Far extends Near
...
float getValueFar() {
return super.getValueNear();
}
Near getNear(float v) {
return new Near(v);
}
}
Der Aufruf der Methode der Superklasse Near.getValueNear()
in der Methode Far.getValueFar()
und die Objekterzeugung in der Methode Far.getNear
sind deassembliert beschrieben durch:
float getValueFar();
0: aload_0
1: invokespecial #18; // Methode Near.getValueNear:()F aufrufen
4: freturn
Near getNear(float);
0: new #3; // neue Instanz von Near allozieren
3: dup // Referenz darauf verdoppeln
4: fload_1 // Methodenparameter auf OS
5: invokespecial #8; // Methode Near.<init>:(F)V aufrufen
8: areturn // verbliebene Referenz zurückgeben
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LFar;
0 9 1 v F
Für getValueFar()
kann das Lookup mit der Superklasse von Far
(also Near
) beginnen. Deshlab wird statt invokevirtual
der invokespecial
-Befehl verwendet.
Beim Aufruf der Initilisierungsmethode <init>
in der Methode Far.getNear
ist die Klasse, welche den Konstruktor implementiert durch die Klasse der Instanz, die durch new
erzeugt wurde, bestimmt. Das Lookup entfällt somit.
Beim Aufruf von nicht statischen Methoden muss die Klasse, welche die Implementierung der refernzierten Methode bestimmt, durch eine Suche ausgewählt werden, die von der Klasse der beim Aufruf verwendeten Objekt-Instanz abhängt. Dieser Vorgang ist auch als dynamsiches Binden von Methoden bekannt.
CI
MCP
kann aufgelöst werden.invokespecial
C
ist direkt durch CI
bestimmt (keine Suche mehr), wenn:MCP
ist eine Initialisierungsmethode <init>(...)
.MCP
ist private
.C
= direkte Superklasse von CI
(CI
ist auch = der aktuellen Frame-Klasse).invokevirtual, invokeinterface
C
= Klasse der Objekt-Instanz CI
C
Instanz-Methode M ≡ MCP
, wird M
aufgerufen, sonst 2.
invokevirtual
: M
muss für C
zugreifbar sein)C
eine Superklasse S
, wird das Lookup rekursive für S
ausgeführt, sonst AbstractMethodError
.
Der Befehlssatz enthält letztlich nur einen Befehl der direkt und ausschließlich für den Exception-Meschanismus genutzt wird: athrow
. Damit wird eine auf dem OS befindende Exception-Instanz als Exception ausgelößt.
void nestedCatch() {
try {
try {
tryThis();
} catch (Exc1 e) {
handle1();
}
} catch (Exc2 e) {
handle2();
}
}
Die Java-Methode nestedCatch()
lößt selbst keine Exception aus. Dies geschieht irgendwo in der abstrakten Methode tryThis()
, die im deassemblierten Beispielcode entsprechend schematisch hinzugefügt wurde:
void nestedCatch();
0: aload_0 // this auf OS
1: invokevirtual #70; // Methode tryThis:()V aufrufen
4: goto 20
7: astore_1 // Exception-Instanz in LV1 sichern
8: aload_0 // this auf OS
9: invokevirtual #73; // Methode handle1:()V aufrufen
12: goto 20
15: astore_1 // Exception-Instanz in LV1 sichern
16: aload_0 // this auf OS
17: invokevirtual #76; // Methode handle2:()V aufrufen
20: return
Exception table:
from to target type
0 4 7 Class Example$Exc1
0 12 15 Class Example$Exc2
void tryThis();
...
aload_? // Referenz auf eine Exception-Instanz aus LV? auf OS bringen
athrow // Auslösen der Exception
...
Tritt beim Aufruf von tryThis
keine Exception auf, wird die Verarbeitung direkt bei Offset 20 fortgesetzt. Der Kontrollfluss nach dem Auslösen einer Exception wird durch die Exception
-Tabelle der Methoden bestimmt. Die Execution-Engine sucht darin nach einem passenden Exception-Handler (type
)
from, to
) aktiv ist, in den der PC gerade verweist.target
) des Handlers fortgesetzt.
Wird die Exception auch beim untersten Frame nicht gefangen, wird der aktuelle Thread beendet. Die Schachtelung von try-catch
-Blöcken wird in der Tabelle durch sich überschneidende aktive Bereiche deutlich. Für nicht geschachtelte Blöcke überschneiden sich die Bereiche einzelner Einträge nicht.
Unterprogramme gruppieren mehrere Befehle innerhalb des Bytecode
s einer Methode, damit diese von mehreren alternativen Pfaden aus aufgerufen werden könne, bevor die Ausführung beim nächsten Befehl des Pfades fortgesetzt wird. Dies wird typisch für den finally
-Block der Java-Sprache eingesetzt, für welche die JVM keine speziellen Befehle oder Mechanismen kennt.
...
10: jsr_w 140 // um 140 byte springen, Rücksprung-Adresse (13) auf OS
13: ... // irgendein Befehle nach dem Unterprogramm
...
55: jsr 95 // um 95 byte springen, Rücksprung-Adresse (58) auf OS
58: ... // irgendein Befehl nach dem Unterprogramm
...
150: astore_3 // Rücksprung-Adresse (13 oder 58) vom OS in LV3 sichern
... // die Unterprogramm-Anweisungen
???: ret 3 // PC auf Rücksprung-Adresse in LV3 setzen
... // erster Befehl nach dem Unterprogramm, etwa durch goto angesprungen
Die Befehle ab Offset 150 bilden das eigentliche Unterprogramm. Dies sichert zunächst die Rücksprungadresse, die vom jsr
- bzw. jsr_w
-Befehl auf den OS gebracht wurde, in einer lokalen Variable (hier 3). Nachdem das Unterprogramm abgearbeitet ist, wird vom ret
-Befehl die Rücksprungadresse aus der lokalen Variable in den PC kopiert, und die Ausführung bei diesem Offset fortgesetzt. Diese Befehle sind für diese Art von Kontrolltransfern notwendig, da es aus Sicherheitsgründen keine expliziete Möglichkeit gibt, den PC zu beeinflussen. jsr
verwendet einen 1-byte
-Offset (-128 bis +127 zum PC), jsr_w
einen 2-byte
-Offset (-32768 bis +32767 zum PC).
Seminarthemen→ Die Architektur der Java VM→ I.• II.• III.• IV. Die Java VM zur Laufzeit • V.• VI.• VII.