Erste Schritte mit JVM – Java Virtual Machine Register-Befehlssatz und Stack-Befehlssatz
Jeder Vorgang in der virtuellen HotSpot-Maschine erfordert Push- und Popping-Schritte.
Aufgrund des plattformübergreifenden Designs werden Java-Anweisungen auf Basis des Stacks entworfen. Verschiedene Plattformen verfügen über unterschiedliche CPU-Architekturen und können daher nicht registerbasiert konzipiert werden. Die Vorteile bestehen darin, dass es plattformübergreifend ist, der Befehlssatz klein ist und der Compiler einfach zu implementieren ist. Der Nachteil besteht darin, dass die Leistung verringert wird und mehr Anweisungen erforderlich sind, um dieselbe Funktion zu erreichen.
Verweise
- Java Virtual Machine-Spezifikation (JavaSE8)
- Vertiefte Kenntnisse der Java Virtual Machine
Zwei Hauptmerkmale des Befehlssatzes von JVM
Funktionen basierend auf der Stack-Architektur
Einfacheres Design und Implementierung, geeignet für ressourcenbeschränkte Systeme (die virtuelle HotSpot-Maschine basiert darauf):
- Vermeiden Sie Probleme bei der Registerzuordnung: Verwenden Sie zur Zuweisung Nulladressenanweisungen.
- Die meisten Anweisungen im Befehlsstrom sind Anweisungen mit Nulladresse, und ihre Ausführung hängt vom Operationsstapel ab.
- Der Befehlssatz ist kleiner und für Compiler einfacher zu implementieren.
- Es ist keine Hardwareunterstützung erforderlich, die Portabilität ist besser und die plattformübergreifende Implementierung ist besser
Merkmale der registerbasierten Architektur
Typische Anwendungen sind x86-Binärbefehlssätze: beispielsweise herkömmliche PCs und die virtuelle Davlik-Maschine von Android.
- Die Befehlssatzarchitektur ist vollständig auf Hardware angewiesen und weist eine schlechte Portabilität auf.
- Hervorragende Leistung und effizientere Ausführung, wodurch weniger Anweisungen zum Abschließen eines Vorgangs erforderlich sind.
- In den meisten Fällen wird der Befehlssatz, der auf der Registerarchitektur basiert, tendenziell von Befehlen mit einer Adresse, Befehlen mit zwei Adressen und Befehlen mit drei Adressen dominiert, während der Befehlssatz, der auf der stapelbasierten Architektur basiert, von Befehlen mit Nulladresse dominiert wird Anweisungen.
Holen Sie sich den Stack-Befehlssatz-Befehl javap
Hinweis: Wenn Sie JDK17 verwenden, können Sie den Konsolenbefehl möglicherweise nicht finden (da die Standardinstallation diesen Befehl nicht konfiguriert). Gehen Sie einfach in das Installationsverzeichnis von JDK17 und fügen Sie bin zur Umgebungsvariablen hinzu.
Kompilieren Sie zunächst ein einfaches 1+2
Programm:
public class StackOneTest {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a+b;
}
}
Verwenden Sie dann javap -c class文件全名
, wir erhalten den folgenden Inhalt in der Konsole:
D:\CodeProjects\Eclipse\StackTest\bin\testjava>javap -c StackOneTest.class
Compiled from "StackOneTest.java"
public class testjava.StackOneTest {
public testjava.StackOneTest();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
}
Da es keine experimentelle Umgebung für registerbasierte Befehlssätze gibt, können Sie die Methode selbst überprüfen.
Semantik des Stack-Befehlssatz-Bytecodes
Überblick über das Design des Bytecode-Befehlssatzes
Im Befehlssatz der Java Virtual Machine enthalten die meisten Anweisungen Informationen über die Datentypen, die sie bedienen. Beispielsweise
iload
wird die Anweisung verwendet, um Daten des Typs aus der lokalen Variablentabelleint
in den Operandenstapel zu laden, während die Anweisung Daten des Typsad
lädt .float
Die Operationen dieser beiden Anweisungen können durch denselben Code implementiert werden, sie müssen jedoch über unabhängige Opcodes verfügen. Bei den meisten Bytecode-Anweisungen, die sich auf Datentypen beziehen, verfügen die Opcode-Mnemoniken über Sonderzeichen, die angeben, welchen Datentyp die Anweisung bedient:i
stelltint
die Datenoperation vom Typ dar,l
repräsentiertlong
, repräsentiert,s
repräsentiertshort
,b
repräsentiertbyte
,c
repräsentiertchar
,f
repräsentiertfloat
,d
repräsentiertdouble
,a
repräsentiertreference
. Es gibt auch einige Anweisungen, deren Mnemonik den Datentyp nicht explizit mit Buchstaben angibt, z. B.arraylength
die Anweisung, die keine Sonderzeichen zur Darstellung des Datentyps enthält, der Operand jedoch immer nur ein Objekt vom Typ Array sein kann. Es gibt andere Anweisungen, z. B. unbedingte Sprunganweisungen,goto
die unabhängig vom Datentyp sind.Da die Opcode-Länge der Java Virtual Machine nur ein Byte beträgt, üben Opcodes, die Datentypen enthalten, großen Druck auf das Design des Befehlssatzes aus. Wenn jede datentypbezogene Anweisung alle Laufzeitdatentypen der Java Virtual Machine unterstützt, kann es sein, dass sie den Zahlenbereich überschreitet, der durch ein Byte dargestellt werden kann. Daher stellt der Befehlssatz der Java Virtual Machine nur begrenzte typbezogene Anweisungen für bestimmte Operationen bereit. Mit anderen Worten, der Befehlssatz wird bewusst so konzipiert, dass er nicht vollständig unabhängig ist (nicht orthogonal, d. h. nicht jeder Datentyp und jeder). Für jeden Vorgang gibt es entsprechende Anweisungen. Es gibt separate Anweisungen, mit denen bei Bedarf nicht unterstützte Typen in unterstützte Typen konvertiert werden können.
Tabelle 2-2 listet die von der Java Virtual Machine unterstützten Bytecode-Befehlssätze auf. Verwenden Sie die durch die Datentypspalte dargestellten Sonderzeichen, um
opcode
die Anweisungsvorlage der Spalte zu ersetzenT
und eine bestimmte Bytecode-Anweisung zu erhalten. Wenn die durch die Spalten „Anweisungsvorlage“ und „Datentyp“ in der Tabelle bestimmte Zelle leer ist, bedeutet dies, dass die virtuelle Maschine die Ausführung dieser Operation für diesen Datentyp nicht unterstützt.load
Anweisungen haben beispielsweise denint
Operationstypiload
, es gibt jedoch keinebyte
ähnlichen Anweisungen vom Operationstyp. Bitte beachten Sie, dass, wie aus Tabelle 2-2 hervorgeht, die meisten Anweisungen die Integer-Typenbyte
,char
und nicht unterstützenshort
und keine der Anweisungenboolean
den Typ überhaupt unterstützt. Der Compiler erweitert die „byte
and“ -Typdaten zur Kompilierungszeit oder zur Laufzeit mit Vorzeichen auf die entsprechenden Typdaten und führt eine Nullerweiterung der „ and“ -Typdaten auf die entsprechenden Typdaten durch. Ebenso werden bei der Verarbeitung von Arrays der Typen , und diese ebenfalls konvertiert , um die Bytecode-Anweisungen des entsprechenden Typs für die Verarbeitung zu verwenden. Daher können die meisten Operationen mit Operanden der tatsächlichen Typen , und mithilfe von Anweisungen mit Operanden des Rechentyps (Rechentyp) ausgeführt werden .short
int
boolean
char
int
boolean
byte
short
char
int
boolean
byte
char
short
int
aus: Java Virtual Machine Specification (JavaSE8)
Zeitplan:
Tatsächliche Typ- und Beziehungszuordnungstabelle:
Befehlssatz laden und speichern
Anweisungen zum Laden lokaler Variablen auf den Operandenstapel:
Anweisung | beschreiben |
---|---|
iload |
Laden Sie den Typ int in den Operandenstapel |
iload <n> |
Lädt den int-Typ am angegebenen Index in den Operandenstapel |
lload_<n> |
Lädt den Long-Typ am angegebenen Index in den Operandenstapel |
fload |
Laden Sie den Float-Typ in den Operandenstapel |
fload <n> |
Lädt den Float-Typ am angegebenen Index in den Operandenstapel |
dload |
Laden Sie den Double-Typ in den Operandenstapel |
dload <n> |
Lädt den Double-Typ am angegebenen Index in den Operandenstapel |
aload |
Referenztypen auf den Operandenstapel laden |
aload <n> |
Lädt den Referenztyp am angegebenen Index in den Operandenstapel |
Anweisungen zum Speichern des Operandenstapels in der lokalen Variablentabelle:
Anweisung | beschreiben |
---|---|
istore |
Speichern Sie den Typ int in der lokalen Variablentabelle |
istore <n> |
Speichern Sie den int-Typ in einer lokalen Variablen am angegebenen Index |
lstore <n> |
Speichern Sie den Long-Typ in einer lokalen Variablen am angegebenen Index |
fstore |
Speichern Sie den Float-Typ in der lokalen Variablentabelle |
fstore <n> |
Speichern Sie den Float-Typ in einer lokalen Variablen am angegebenen Index |
dstore |
Speichern Sie den Doppeltyp in der lokalen Variablentabelle |
dstore <n> |
Speichert einen Double-Typ in einer lokalen Variablen am angegebenen Index |
astore |
Speichern Sie den Referenztyp in der lokalen Variablentabelle |
astore_<n> |
Speichert einen Referenztyp in einer lokalen Variablen am angegebenen Index |
Anweisungen zum Laden von Konstanten auf den Operandenstapel:
Anweisung | beschreiben |
---|---|
bipush |
Laden Sie die vorzeichenbehaftete Bytekonstante (-128~127) in den Operandenstapel |
sipush |
Laden Sie die vorzeichenbehaftete Kurzkonstante (-32768~32767) in den Operandenstapel |
ldc |
Laden Sie eine Konstante vom Typ int, float oder String in den Operandenstapel |
ldc_w |
Ähnlich wie ldc, aber für größere konstante Poolindizes |
ldc2_w |
Laden Sie eine Konstante vom Typ long oder double auf den Operandenstapel |
aconst_null |
Laden Sie null auf den Operandenstapel |
iconst_m1 |
Laden Sie die Ganzzahl -1 auf den Operandenstapel |
iconst <i> |
Laden Sie ganzzahlige Konstanten auf den Operandenstapel |
lconst <1> |
Laden Sie die Long-Integer-Konstante 1 auf den Operandenstapel |
fconst <f> |
Laden Sie Gleitkommakonstanten in den Operandenstapel |
dconst <d> |
Laden Sie eine Gleitkommakonstante mit doppelter Genauigkeit auf den Operandenstapel |
Anweisungen zum Erweitern der lokalen Variablentabelle, um auf Indizes oder unmittelbare Werte zuzugreifen:
Anweisung | beschreiben |
---|---|
wide |
Erweitern Sie die Breite des lokalen Variablenindex, der von der nächsten Anweisung verwendet wird |
Arithmetischer Befehlssatz
Rechenanweisungen:
Operationstyp | Anweisung |
---|---|
Zusatz | iadd 、ladd 、fadd 、dadd |
Subtraktion | isub 、Isub 、fsub 、dsub |
Multiplikation | imul 、mul 、fmul 、dmul |
Aufteilung | idiv 、ldiv 、fdiv 、ddiv |
Fragen Sie nach Überschüssen | irem 、Irem 、frem 、drem |
Anweisungen zur logischen Bitoperation:
Operationstyp | Anweisung |
---|---|
Bitweises ODER | ior 、lor |
Bitweises UND | iand 、land |
Bitweises XOR | ixor 、lxor |
Weitere Bedienungsanleitungen:
Operationstyp | Anweisung |
---|---|
negativen Wert finden | ineg 、Ineg 、fneg 、dneg |
Schicht | ishl 、ishr 、iushr 、Ishl 、Ishr 、lushr |
Automatische Inkrementierung lokaler Variablen | iinc |
Vergleichen | dcmpg 、dcmpl 、fcmpg 、fcmpl 、Icmp |
Bytecode-Analyse des Stapelbefehlssatzes
Arithmetischer Bytecode in der Hauptmethode
D:\CodeProjects\Eclipse\StackTest\bin\testjava>javap -c StackOneTest.class
Compiled from "StackOneTest.java"
public class testjava.StackOneTest {
public testjava.StackOneTest();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
}
Aus dem obigen Bytecode ist leicht zu erkennen, dass JVM
zuerst das Objekt erstellt StackOneTest
und dann die Objektvariable in den Operandenstapel (Standardkonstruktor) geladen wird. Konzentrieren wir uns auf den main
Befehlssatz:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
Zunächst 1
werden die beiden Konstanten 2
in den Operandenstapel geladen und gespeichert. Als nächstes:
4: iload_1
5: iload_2
6: iadd
7: istore_3
Zwei Variablen werden auf den Operandenstapel geladen a
, b
dann mithilfe von arithmetischen Anweisungen hinzugefügt und schließlich wird das Ergebnis gespeichert (die dritte Variable wird hier nicht verwendet, das Laden der Variablen wurde möglicherweise vom Compiler optimiert). Wir modifizieren es geringfügig:
Es kann festgestellt werden, dass nach Verwendung der dritten Variablen JVM
das korrekte Laden der Variablen durchgeführt wurde
Bytecode-Analyse mit Methoden
Wir fügen eine Methode zur weiteren Analyse hinzu:
public class StackOneTest {
public static int add(int a, int b) {
return a+b;
}
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = add(a, b);
System.out.println(c);
}
}
Von hier aus können wir sehen, dass invokestatic
es sich um den Befehlssatz zum Aufrufen der Methode handeln sollte. Seine Funktionsweise ähnelt der vorherigen, Sie müssen jedoch auf Treturn
Anweisungen mit unterschiedlichen Rückgabewerttypen achten (T ist ein Codename, der von Generics entlehnt ist).
Multiple-Stack-Bytecode-Analyse
Ändern Sie den Code:
public class StackOneTest {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = 3;
int d = a+b;
int e = a+c;
}
}
Analysieren Sie auf die gleiche Weise:
Es kann festgestellt werden, dass der Bytecode auch zeilenweise bearbeitet wird. Die Verarbeitung von Variablen d
und e
Positionen ähnelt der ersten Stelle, außer dass a
sie wiederholt in den Stapel verschoben wird.