Java Virtual Machine


Java-Programme werden im Gegensatz zu nativen Programmen nicht direkt durch das Betriebssystem ausgeführt, sondern von der Java Virtual Machine (JVM). Die JVM kümmert sich um die korrekte Ausführung des Programms auf dem jeweiligen Betriebssystem. Der Programmierer muss also nicht auf spezifische Eigenschaften eines Betriebssystems Rücksicht nehmen sondern kann sich darauf verlassen, dass sein Programm auf allen Systemen läuft wie es soll. Anders als ein simpler Interpreter bietet die JVM jedoch durch ihren just-in-time-compiler (JIT-Compiler) jedoch eine performance, welche nativen Programmen ähnelt. Um das zu bewerkstelligen besteht die JVM aus einer Reihe von komplexen Komponenten welche hier grob vorgestellt werden sollen.

Java Virtual Machine

Klassenlader-Subsystem

Das Klassenlader-Subsystem (Class Loader Subsystem) hat die Aufgabe den Java-Bytecode zu laden, daraus Klassen zu generieren und diese in den zugehörigen Kontext einzubinden.

Laden

Zuerst wird dazu ein Klassenlader aufgerufen. Dieser erhält dafür den voll qualifizierten Klassennamen und muss die Klasse zurück geben.

  • Bootstrap Class Loader: Lädt initiale Klassen zur  grundlegenden Ausführung eines Programms wie beispielsweise die java.lang-Klassen.
  • Extension Class Loader: Lädt Bibliotheken welche in das Programm eingebunden wurden.
  • Application Class Loader: Lädt die Klassen des Programms.

Klassenlader sind in einer Hierarchie angeordnet. Kann ein Klassenlader eine Klasse nicht finden, so wird der Nachfolgende Klassenlader aufgerufen. Die einzelnen Klassenlader bestimmen bereits die Sichtbarkeit einer Klasse. Wurde eine Klasse über den Extension-Klassenlader geladen, so kann diese nicht von Klassen aufgerufen werden, welche von dem Bootstrap-Klassenlader geladen wurden, jedoch sehr wohl von Klassen welche vom Application-Klassenlader geladen wurden. Wird eine Klasse selbst im Bootstrap-Klassenlader nicht gefunden, so wird eine ClassNotFoundException geworfen.

Linken

Durch das Linken wird die Klasse in die JVM eingebunden. Zuerst wird dabei verifiziert ob es sich um eine gültige Klasse handelt. Dazu wird der vorliegende Bytecode überprüft. Hierzu müssen gegebenenfalls weitere Klassen geladen werden.

In dem Vorbereitungschritt werden statische Felder initialisiert. Dabei wird jedoch noch kein Code ausgeführt.

Im letzten Schritt des Linken werden symbolische Referenzen aufgelöst. Das bedeutet, dass Klassen, Interfaces, Methoden und Felder auf welche die Klasse referenziert mit dieser quasi verbunden werden. Dabei wird auch überprüft ob der Zugriff auf die jeweiligen Referenzen auch erlaubt ist.

Initialisieren

Bei der Initialisierung werden statische Felder gesetzt, welche keine Konstanten sind und bei denen Code ausgeführt werden muss. Der Initialisierungsschritt erfolgt wenn eine neue Klasse mit new() initialisiert wird oder eine statische Methode oder ein statisches Feld aufgerufen wird.

Speicherbereiche

  • Methodenbereich: In diesem Bereich liegt der Programmcode zu den einzelnen Methoden.
  • Heap: Der Heap speichert erzeugte Objekte.
  • Stack: Im Stack werden wie bei einem gewöhnlichen Stack lokale Variablen und die Ergebnisse aus einzelnen Operationen gespeichert. Zudem werden Referenzen auf benutzte Methoden und eine Referenz auf die aktuelle Exception-Tabelle im Stack gespeichert.
  • Register: Im Register wird für jeden Thread ein eigener Programmzähler gespeichert.
  • Native Method Stack: Ein Stack für die Ausführung nativer Methoden. Dieser Stack wird durch die Regeln des Betriebssystem befüllt.

Ausführungseinheit

Nachdem der Bytecode durch den Klassenlader analysiert und auf den Methodenbereich im Hauptspeicher gelegt wurde muss dieser noch ausgeführt werden. Dazu ist die Ausführungseinheit (execution engine) zuständig. Die Ausführung geschieht auf zweierlei Art:

  • Interpreter: Der Interpreter durchläuft den Bytecode Schritt für Schritt und führt die darin enthaltenen Befehle sofort aus. Der Interpreter kann Code schnell einlesen und mit seiner Verarbeitung beginnen, da jeder Befehl jedoch einzeln analysiert und interpretiert 
    werden muss ist die Ausführung eher langsam. Er eignet sich vor allem für Code, der nur selten ausgeführt wird.
  • JIT-Compiler: Der Jit-Compiler liest den Bytecode und wandelt ihn in Maschinencode, der zu dem Betriebssystem und der eingesetzten Prozessorarchitektur passt. Das Analysieren und Optimieren des Codes ist aufwendig, die anschließende Ausführung ist jedoch enorm schnell, da sie auf nativem Code stattfindet. Der JIT-Compiler eignet sich vor allem für Code der oft hintereinander ausgeführt werden muss.

Garbage Collection

Die Garbage Collection kümmer sich um Objekte im Hauptspeicher, welche nicht mehr benötigt werden. Wir eine neue Klasse instantiiert, so wird Speicherplatz im Heap angelegt. Wird die Klasse nicht mehr benutzt, so bleibt sie jedoch vorerst im Hauptspeicher zurück. Der Garbage Collector kümmert sich um all die alten Objekte im Heap, welche nicht mehr benutzt werden und löscht sie wenn nötig. Der Garbage Collector hat einen starken Einfluss auf die Performance eines Programms, da er mitunter die gesamte Ausführung anhält um Speicher zu bereinigen.

Java Native Interface

Das Java Native Interface (JNI) ermöglich es anderen Programmen mit Java-Programmen zu interagieren. So können beispielsweise in C geschriebene Programme in Java ausgeführt werden. Da Java-Programme auf allen Betriebssystemen laufen müssen wird manche Funktionalität, welche beispielsweise nur in Windows genutzt werden kann, nicht in Java zur Verfügung gestellt. Durch das JNI kann dies umgangen werden und dennoch auf beispielsweise Windows DLL-Dateien zugegriffen werden. 

Native Method Library

Wird ein Programm durch den JIT-Compiler in nativen Code übersetzt läuft dieser fast ohne den Einfluss der JVM. Hin und wieder ist es dennoch nötig, dass auch die Ausführungseinheit wie beispielsweise der Interpreter Funktionen des Betriebssystems aufrühren muss. Die Native Method Library stellt Bibliotheken zur Verfügung mit denen die Ausführungseinheit die nötigen Aufgaben erledigen kann.