Seminarthemen→ Die Architektur der Java VM→ I.• II. Strukturen der Java VM • III.• IV.• V.• VI.• VII.
Da für die meisten der Komponenten in der Fachliteratur bestimmte englische Namen gebrächlich sind, werden diese auch in diesem Artikel verwendet, um Missverständnisse zu vermeiden. Einige dieser Namen leiten sich dabei auch von einer gleichnamigen Java-Klasse ab. Für Komponenten, dessen Bezeichnung nicht einheitlich gebraucht wird, wird entsprechend ein deutscher Name verwendet.
Bei der Java VM handelt es sich um eine Stack-Maschine. D.h. die Operanden der meisten Befehle werden auf einen Verarbeitungs-Stack erwartet, zur Ausführung von diesem geholt, und das Ergebnis auch auf diesem Stack hinterlegt. So kann der Bytecode
sehr kompakt gehalten werden, da die meisten Befehle keine oder impliziete Parameter haben. Dieser zur Ausführung verwendete Stack, ist bei der JVM ein 32-Bit LIFO Stack, der Operand-Stack (siehe Abb. 2) genannt wird, und Teil des in Abb. 1 sichtbare JVM-Stacks ist.
Der Java-Bytecode
, der im .class
-Dateiformat gespeichert wird, wird klassenweise vom ClassLoader
geladen, und ggf. vom Verifier
überprüft, bevor eine interne Darstellung der Klasse in der Methode Area abgelegt wird (Darstellungsfrom ist dem JVM-Designer überlassen). Die in Bytecode
implementierten Methoden einer Klasse werden, wie angedeutet, über den JVM-Stack/Operand-Stack ausgeführt. Eine Klassen wird dabei erst dann dynamisch (zur Laufzeit) geladen, wenn sie benötigt wird.
Neben dem Java-Bytecode
kann die JVM auch nativen Code einbeziehen. Dazu werden native Programme bei Bedarf als Methoden vom Native Methode Linker gelinkt, in der Native Methode Area abgelegt, und auf dem Native Stack des aktiven Threads ausgeführt. Dies ist ein herkömmlicher LIFO-Stack, um verschachtelte Funktionsaufrufe zu organisieren. Als Schnittstelle zwischen Java und nativen Methoden dient heute meist das JNI (Java Native Interface), mit dem C oder C++ Programme als Shard Library (plattformspeziefisch) eingebunden werden können.
Objekte (Instanzen von Klassen-Typen, die zur Laufzeit erzeugt werden), werden auf dem Heap abgelegt, der beim Start der JVM angelegt wird. Die interne Darstellung eines Objekts ist auch hier nicht festgeschrieben, und somit dem JVM-Designer überlassen. Der Heap wird von einem automatisch Speichermanager verwaltet, der Gargabe Collector genannt wird. Dieser identifiziert Objekte, welche nicht länger benötigt werden, und gibt deren Speicherbereiche wieder frei.
Die JVM ist, wie die Java-Sprache auch, konzeptionel streng typisiert, besitzt zudem einige zusätzliche interne Typen. Trotz der strengen Typisierung existiert jedoch nur ein Zeigertyp (reference
), da Typüberprüfungen einmalig beim Laden einer Klasse vom Verifier durchgeführt werden. Die Typinformationen werden intern erhalten, aber zur Laufzeit nur bei expliziet im Bytecode
kodierten Casts und Typüberprüfungen kontrolliert. Vorausgesetzt der Verifier ist fehlerlos implementiert, ist Java trotzdem jederzeit typsicher!
Die JVM-Spezifikation unterscheidet zwischen einer Java VM und einem Java Laufzeit-System. Unter einer JVM versteht die Spezifikation eine abstrakte Rechenmaschine, die entwurfen wurde, um Java-Bytecode
zu verarbeiten. Funktionsfähige Implementierungen dieser Spezifikation heißen Java-Laufzeit-System, und benötigen zusätzlich zur VM auch die grundlegenden mit der VM verwobenen Klassen, welche auch Teil der API (Application and Programming Interface, "Programmierschnittstelle") sind. Zum JRE gehören die Klassen der Pakete (und deren Unterpackete):
Object
, von der alle Klassen und Interfaces abgeleitet werden.Class
, die als Schnittstelle zur internen Klassendarstellung fungiert, und zusammen mit dem Paket java.lang.reflect
Grundlage der Selbstreflexion ist.Byte
, Short
, Integer
, Long
, Float
, Double
und Character
.String
für Zeichenketten (die logisch kein char[]
sind!)System
für Umgebungsvariablen und (Betriebs-)Systemeingenschaften.SecurityManager
und das Paket java.security
verwalten Sicherheitsaspekte.Thread
, als Grundlage des JVM-Thread Mechanismus.java.lang.ref
, das Soft-, Weak- und Phantom-Referenzen ermöglicht.Collection
, Date
, Calendar
, ResourceBundle
und Properties
java.util.zip
, java.util.jar
, java.util.regex
InputStream
und OutputStream
für byte
-weise Ein-Ausgabe.Reader
und Writer
für die Ein-Ausgabe von Unicode-Zeichen..class User Bytecode |
.class JRE (API) Bytecode |
Native Libraries |
|
Thread 1
…
PC JVM-Stack Native Stack Thread n
PC JVM-Stack Native Stack |
ClassLoader, Verifier | (JNI), Native Methode Linker | |
Methode Area | Heap automatische Speicherverwaltung, enthält Objekte |
Native Methode Area | |
char
als vorzeichenbehaftete Integer in Zweierkomplementdarstellung gespeichert. Einzelne char
's werden als vorzeichenloser Integer dargestellt, die Uft-16-Zeichen repräsentieren. Da long
-Werte 64 Bit breit sind, die JVM aber 32-Bit Worte verarbeitet, werden Werte vom Typ long
durch 2 Wörter dargestellt. Die Typen byte
, short
und char
werden zudem intern für Berechnungen automatisch zu int
erweitert, als solche verarbeitet, und anschließend entsprechend dem Ausgangstyp abgeschnitten und zurückkonvertiert.Name | Bit | Initalwert | Wertebereich (inclusiv) | |
---|---|---|---|---|
von | bis | |||
byte | 8 | 0 | -128 (-27) | 127 (27-1) |
short | 16 | 0 | -32768 (-215) | 32767 (27-1) |
int | 32 | 0 | -2147483648 (-231) | 2147483647 (231-1) |
long | 64 | 0L | -9223372036854775808 (-263) | 9223372036854775807 (263-1) |
char | 16* | \u0000 | 0 (\u0000) | 656635 (\uFFFF) |
NaN
= not a Number, für Ergebnisse ungültiger Operationen.long
-Werte belegen auch double
-Werte 2 JVM-Worte.
Zusätzlich zu den zur Java-Sprache analogen standard value sets für float
- und double
-Werte, die auch für die Kodierung in .class
-Dateien vorgeschriebenen sind, kann die JVM noch ein extended float value set und ein extended double value set unterstützen. Der entscheidenden Unterschiede zwischen den in der JVM verwendeten value sets und dem IEEE 754 Standard sind:
Name | Bit | Initalwert | Wertebereich (inclusiv) | |
---|---|---|---|---|
von | bis | |||
float | 32 | 0.0f | ± 1.40239846-45 | ± 3.40282347+38 |
double | 64 | 0.0d | ± 4.94065645841246544-324 | ± 1.79769313486231570+308 |
boolean
-Typboolean
. Dieser wird aber intern so gut wie garnicht unterstützt. Stattdessen werden boolsche Werte in int
umgewandelt und dann verarbeitet. Wie schon in C entspricht false
= 0 und true
= 1. Compiler müssen diese bereits entsprechend als 0 oder 1 kodieren. Auch für boolean
-Arrays existieren keine eigenen Befehle. Diese werden mit den Befehlen baload
und bastore
für byte
-Arrays angefasst. Der boolean
-Typ ist vermutlich ausschließlich zur Typüberprüfung durch den Verifier definiert.returnAddress
-Typjsr
und ret
Befehle zur Unterstützung von Unterprogrammen verwendet. Es wird dabei ein konstanter byte
-Offeset zu dem Befehl des Bytecode
s angegeben, bei welchem die Ausführung am Ende des Unterprogramms fortgesetzt werden soll. Dieser ganzzahlige vom Compiler berechnete Wert entspricht keinem Integraltyp der Java-Sprache, und kann auch nicht vom Programmierer zur Laufzeit beeinflusst werden.
Die Werte von reference
-Typen entsprechen Referenzen auf zur Laufzeit erzeugte Objekt-Instanzen. Dabei werden vier verschiedene Gruppen unterschieden, die jedoch alle durch Instanzen der finalen System-Klasse java.lang.Class
dargestellt werden:
Eine reference
kann auch die spezielle Referenz null
sein, die keinen Laufzeittyp hat, aber zu allen reference
-Typen gecastet werden kann. Welchen Wert die null
-Referenz hat, schreibt die Spezifikation nicht vor. Dies begründet sich auch durch die ifnull
- und ifnonnull
-Befehle, durch welche eine konkrete Bennenung des null
-Werts überflüssig wird, da somit eine Referenz auf dem Stack bereits auf Gleich- und Ungleicheit mit null
geprüft werden kann.
Jeder Thread besitzt einen privaten Java-Virtual-Machine-Stack, auf dem Frames organisiert werden. Für jeden Methodenaufruf wird ein Frame erzeugt, auf den Stack des aktuellen Threads gebracht und die Kontrolle an den erzeugten Frame übertragen. Das PC-Register des Threads verweist während der Ausführung auf den nächsten Befehl des Bytecode
s. Nachdem die Methode ausgeführt wurde, wird der Frame wieder vom JVM-Stack entfernt, und der Rückgabewert (falls vorhanden) auf den Operand-Stack des aufrufenden Frames abgelegt.
Lokale Variablen | Operand-Stack | RCP-Referenz |
push
, pop
, dup
BefehleBytecode
der Methode kodiert)long
- und double
-Werte belegen 2 untrennbare Stack-ElementeBytecode
der Methode kodiert)long
- und double
-Werte belegen 2 Variablen-Elemente, Zugriff über kleineren Index, Zugriff auf größeren Index illegal.reference
-Typen entspricht dies der Übergabeart "by Reference". Welcher Methoden-Parameter in welcher LV abgelegt wird, hängt von der Art der Methode ab:
t
= enthält Referenz auf die Instanz dessen Methode aufgerufen wurde (this
in der Java-Sprache).p1-pn
= enthält aktuelle Parameter 1 bis n der aufgerufenen Methodel1-ln
= je nach Methodenrumpf weitere n lokale variablen , zunächst mit "illegaler Zugriff" initialisiert.Bytecode
der Methode aus der class
-Datei referenziert dort aufgerufene Methoden und Felder über eine symbolische Referenz – einen Index in den Constant Pool der Datei. Dynamisches Binden übersetzt symbolische Referenz (bei erster Verwendung) in konkrete Referenzen auf die entsprechende Methode oder das entsprechende Feld. Eine konkrete Referenz entspricht einem Offset oder Zeiger in die Laufzeitdatenstruktur in der Methode Area.Bytecode
-Implementierung) enthält das PC-Register eine Offset vom Methodenanfang der aktuellen Methode, der auf den gerade ausgeführten Befehl verweist. Bei der Ausführung nativer Methoden ist der Inhalt des PC-Registers undefiniert. Es ist breit genug, um eine returnAddress
oder einen nativen Zeiger zu enthalten.Da die Größe des Operand-Stack und die Anzahl der lokalen Variaben bereits zur Compilte-Zeit für jede Methode feststehen, kann die benötigte Speicher für ein Frame leicht bestimmt werden. Je nach Implementierung addieren sich noch eine Bereich konstanter Länger für zusätzliche interne Verwaltungsdaten (aktuelle Methode, aktueller Frame, aktuelle Klasse, …) zur Größe eines Frames hinzu.
Das folgende Java-Code würde beim Aufruf von foo()
in Thread 1 bzw. method()
in Thread n zu der in Abb. 3 dargestellten Situation auf den Stacks führen. Die aktuell arbeitenden Frames sind active()
und subMethode()
. CP-Ref 1.1 und 1.2 würden auf den RCP der Klasse X
verweisen, CP-Ref 1.3 auf den der Klasse Y
.
class X { …
void foo() {
calledByFoo();
…
}
void calledByFoo() {
int x = Y.active();
…
}
}
class Y { …
static int active() { … }
}
foo() Frame 1.1 LV 1.1 OS 1.1 RCP-Ref 1.1 |
calledByFoo() Frame 1.2 LV 1.2 OS 1.2 RCP-Ref 1.2 |
active() Frame 1.3 LV 1.3 OS 1.3 RCP-Ref 1.3 |
method() Frame n.1 LV n.1 OS n.1 RCP-Ref 2.1 |
subMethode() Frame n.2 LV n.2 OS n.2 RCP-Ref 2.1 |
Alle Threads der JVM teilen sich eine Methode-Area, die dem Bereich für compilierten Code von konventionellen Sprachen entspricht. Sie enthält eine implemenetierungsabhängige interne Darstellung jeder geladenen Klasse. Eine Klasse wird dynamisch geladen wenn sie benötigt wird, indem zunächst ein ClassLoader
eine Bytecode
-Repräsentation (gewöhnlich der Inhalt der .class
-Datei) erzeugt, und diese an die JVM weitergibt, welche die Information darin aufbereitet und daraus einen Eintrag in der Methode-Area angelgt. Der Eintrag enthält auch Referenzen auf den ClassLoader
, der die Klasse definiert hat.
Klasse A
Runtime Constant Pool
Numerische Konstanten
String Konstanten
(Symbolische) Referenzen auf
Felder, Methoden, Klassen Felder
Methoden (Code)
Attibutte
|
… | Klasse N
RCP
Felder
Methoden (Code)
Attibutte
|
class
-Datei enthaltenen Constant Pools (CP), in dem
int
, long
, float
oder double
dargestellt werden.String
-Objekte abgelegt werden.String
-Namen identifiziert werden, verwenden die meisten Implementierungen eine Hash-Map. Ob die Attribute von Feldern und Methoden ebenfalls hier enthalten sind oder seperat zusammen mit dem Feldwert bzw. Methodenimplementierung ist nicht festgelegt.Der Heap-Speicher ist der Laufzeit-Datenspeicher der JVM und wird von allen Threads verwendet, um Instanzen von Klassen oder Arrays zu speichern. Die Darstellung von Objekten ist nicht durch die Spezifikation festgelegt. Es sind verschiedene Formen denkbar, die vor allem darauf abziehlen, möglichst
Der Speicher des Heap wird dabei von einer automatischen Speicherverwaltung, dem Garbage Collector (GC) verwaltet, der nicht mehr benötigte Objekte perioisch einsammelt. Bei der Erzeugung neuer Objekt wird zwar expliziet im Bytecode
Speicher vom Heap reserviert. Dieser kann jedoch nicht expliziet wieder freigegeben werden. Die Speicherfreigabe obliegt dem Garbage Collector, der nicht mehr benötigte Objekte automatisch nach einer bestimmten Strategie bestimmt. Diese kann je nach Anforderungen an die jeweiligen JVM gewählt werden. Zum Beispiel:
rc
rc = 1
rc++
rc--
rc == 0
wird das Objekt nicht mehr benötigt
Grundsätzlich ist der Gabrabe Collector ein unabhängiger Prozess dessen Aktivität nicht vorhergesagt, deaktiviert oder eingeschränkt werden kann. Sie kann einzig über System.gc()
ausgelöst werden. Ein Programmierer sollte daher im im Hinterkopf haben, dass
null
gesetzt werden sollten!!!finalize()
-Methode zwar Ressourcen außerhalb der JVM freigeben kann, sich aber nicht verhersagen lässt wann genau der GC diese aufruft.ClassLoader
Damit eine intere Darstellung einer Klasse in der Methode-Area erzeugt werden kann, muss diese zuvor von einem ClassLoader
geladen werden. Dieser gibt den geladenen Bytecode
dann an die JVM weiter, welche eine implementierungsabhängige intere Darstellung erzeugt. Hierbei werden verschiedene ClassLoader
verwendet:
ClassNotFoundException
Die JVM erfragt unbekannte Klassen über den SystemClassLoader
. Findet dieser die gesuchte Klasse nicht in der ClassPath
-Umgebung delegiert er die Anfrage an den ExtensionClassLoader
weiter, welcher ihm unbekannte Klassen seinerseits an den BootStrapClassLoader
delegiert. Kennt auch diese die gesuchte Klasse nicht, wird eine ClassNotFoundException
ausgelöst.
Die Verwendeung verschiedener ClassLoader
hat die Ziele:
Jedes JRE enthält dazu wenigstens:
BootStrapClassLoader
java.*
, javax.*
…ExtendsionClassLoader
SystemClassLoader
ClassPath
-Systemumgebung bekannten Klassen.
Außerdem können auch eigene ClassLoader
erstellt werden. Diese auch als UserClassLoader oder CustomClassLoader bekannten Klassen sind jedoch kein explizierter Teil der JVM. Sie sind Unterklassen von java.lang.ClassLoader
und werden wie normale Klassen compiliert, initialisiert und instanziert.
Jeder ClassLoader
hat einen eigenen Namensraum. D.h. die Klasse Foo
, die von ClassLoaderX
geladen wurde, wäre ungleich einer Klasse Foo
, die von ClassLoaderY
geladen wurde, selbst dann, wenn sie aus der selben class
-Datei erzeugt würde.
Die Klassen von Arrays werden nicht von einem ClassLoader
geladen, sondern zur Laufzeit direkt von der JVM selbst erzeugt.
Die Execution-Engine ist der virtuelle Pozessor der JVM. Im Zusammenspiel mit dem Java-Stack und dem PC-Register jedes Threads führt er die Befehle des Bytecode
aus. Die grundsätzliche Ablauf der Ausführung entspricht dem einer gewöhnlichen Stack-Maschine:
Bytecode
-Befehl bei PC-Offset laden.Byte
Opcode + Operandenlänge) inkrementieren, dann wieder 1.
Eine einfache JVM besteht abstrakt betrachtet aus einem großen Case-Verteiler, der für jeden Befehl des Bytecode
s äquivaltente Maschienencodeaufrufe beinhaltet. Die Ausführung ansich wird heute meist über verschiedene Strategien optimiert, die im Folgenden kurz skizziert werden:
Bei der Just-In-Time Code-Generierung wird der Bytecode
einer Methode zur Laufzeit in nativen Maschinencode übersetzt, welcher bei jedem weiteren Aufruf der Methode direkt verwendet wird. Grundsätzlich kann ein JIT-Compiler auch schnelleren nativen Maschinencode erzeugen, als es herkömmliche (AOT-)Compiler können, da er "Closed-World"-Annahmen treffen kann.
Der Bytecode-Compiler ist eine Weiterentwicklung des JIT-Compilers. Die aktuelle HotSpot-Technologie kompiliert den Bytecode zur Laufzeit in nativen Maschinencode und optimiert diesen abhängig von der verwendeten Plattform. Diese Optimierung findet dabei nach und nach statt, was den Effekt hat, dass Programmteile nach mehrmaliger Abarbeitung schneller werden. Nicht selten ist die Ausführung des Java-Bytecodes dann genau so schnell oder sogar schneller als herkömmlich compilierte Programme.
Eine detailiertere Betrachtung der Abläufe innerhalb der JVM bietet das Kapitel IV. Die Java VM zur Laufzeit.
Die Möglichkeit native Funktionen oder Biblioteken zu nutzen ist Voraussetzung für eine Zusammenarbeit zwischen dem JRE und einem darunter liegenden Betriebssystem. Ein JRE, das ohne BS direkt auf der Hardware arbeitet, unterstützt meist auch keine nativen Bibliotheken. Wenn diese jedoch unterstützt werden, verwendet die JVM den Native Methode Linker, um zur Laufzeit externe Bibliotheken einzubinden, und in der Native Methode Area abzulegen. Jeder Thread verfügt dann über einen zusätzlichen Native Stack. Dies ist ein herkömmlicher Kellerspeicher wie ihn auch C nutzen würde, um Aufrufhierachien zu verwalten. Das PC-Register des Threads darf bei der Ausführung nativer Funktionen auch die dort üblichen Zeiger auf den nächsten nativen Befehl enthalten. Es ist dafür entsprechend breit angelegt.
Das JNI vereinheitlicht Parameterübergabe, Aufruf und Rückgabe zwischen den nativen Funktionen und der JVM-Welt. Auch auf die Objekte auf dem Heap kann in den nativen Methoden über das JNI zugegriffen werden.
Die Unterstützung nativer Bibliotheken ist ein sehr weitreichendes Feld, auf das an dieser Stelle nicht weiter eingegangen wird.
Seminarthemen→ Die Architektur der Java VM→ I.• II. Strukturen der Java VM • III.• IV.• V.• VI.• VII.