C Grundlagen zur Java Virtual Machine

Werbung
D3kjd3Di38lk323nnm
1405
C
Grundlagen zur Java Virtual Machine
Dieser Anhang stellt einige Grundlagen zur Java Virtual Machine und zur Ausführung
von Java-Programmen vor.
C.1
Wissenswertes rund um die Java Virtual
Machine
Java ist eine Programmiersprache, die in einer Laufzeitumgebung, der Java Virtual
Machine (JVM), ausgeführt wird. Im Gegensatz zu anderen Programmiersprachen, etwa C++, wird der Sourcecode beim Kompilieren mit dem Java-Compiler javac nicht
in die Maschinensprache des jeweiligen Computers übersetzt, sondern in eine Zwischensprache, den plattformunabhängigen Bytecode. Der Name rührt daher, dass die
Instruktionen in Form von Bytes codiert sind. Dieser Bytecode wird nicht direkt vom
Prozessor des Rechners ausgeführt. Stattdessen handelt es sich beim Bytecode um Befehle für einen speziellen virtuellen Computer, nämlich für die Java Virtual Machine.
Diese stellt einen Computer im Computer dar und ermöglicht so eine Abstraktion von
der darunterliegenden Hardware. Die Plattformunabhängigkeit von Java wird dadurch
erreicht, dass für jedes Betriebssystem eine eigenständige JVM existiert, die Befehle
im Bytecode-Format ausführt. Abbildung C-1 deutet den prinzipiellen Ablauf an.
Abbildung C-1 Ablauf beim Kompilieren und Ausführen eines Java-Programms
C.1.1
Ausführung eines Java-Programms
Für die Ausführung eines Java-Programms haben früher die ersten JVMs den Bytecode Instruktion für Instruktion interpretiert und abgearbeitet. Aufgrund der durchaus
effizienten Arbeitsweise des Bytecode-Interpreters konnte damit bereits eine einigermaßen akzeptable Ausführungsgeschwindigkeit erzielt werden, die jedoch deutlich unter
Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1
1406
C Grundlagen zur Java Virtual Machine
der Geschwindigkeit kompilierter Programme lag. Als Folge davon haben die Ausführungszeiten dieser ersten JVMs lange Zeit das Gerücht genährt, Java-Programme würden (zu) langsam ablaufen. Kompilierte C++-Programme, die in die jeweilige Maschinensprache des Zielrechners übersetzt werden, waren bis zum Erscheinen von JDK 1.4
bzw. JDK 5 performanter. In den letzten Jahren wurden aber immer leistungsfähigere
JVMs entwickelt, sodass sich die Ausführungsgeschwindigkeit von Java-Programmen
immer mehr derjenigen kompilierter C++-Programme angenähert bzw. mit aktuellen
JVMs diese sogar teilweise überflügelt hat.
Heutzutage sind JVMs also extrem leistungsfähig. Das wird unter anderem dadurch erreicht, dass während bzw. parallel zu der eigentlichen Programmausführung
der Bytecode in Maschinensprache übersetzt wird. Man spricht von einem Just-inTime-Compiler (kurz JIT). Führt man diese Transformation für den gesamten Bytecode durch, so kann das allerdings recht aufwendig werden. Für selten durchlaufene
Programmteile wiegt somit der erzielte Geschwindigkeitsgewinn bei der Ausführung
der kompilierten Anweisungen nicht den zeitlichen Aufwand zur Transformation auf.
Demnach ist diese Form der Optimierung manchmal sogar kontraproduktiv und langsamer als eine Ausführung per Interpreter. Aktuelle JVMs nutzen daher eine intelligentere Vorgehensweise bei der Programmausführung, nämlich das sogenannte HotspotOptimierungsverfahren.1 Hierbei werden die häufig durchlaufenen Programmteile (die
Hotspots) erkannt und nur diese kompiliert und optimiert.2 Abschnitt 22.1.3 geht auf
einige Optimierungen im Detail ein.
C.1.2
Sicherheit und Speicherverwaltung
Um Java-Programme möglichst robust zu machen, wurden verschiedene Sicherheitsmechanismen in die Sprache integriert. Insbesondere wurde die Speicherverwaltung
so gestaltet, dass man sich als Entwickler kaum darum kümmern muss: Der per new
angeforderte Speicher für Objekte muss nicht explizit freigegeben werden. Das wird
stattdessen automatisch durch eine spezielle Komponente der JVM, den Garbage Collector, erledigt. Fehler durch zu früh oder mehrmals freigegebene Speicherbereiche sind
damit ausgeschlossen. Allerdings verbleibt das Problem von nicht freigegebenem Speicher. Derartige Memory Leaks sind in Java im Vergleich zu Sprachen mit manuellem
Speichermanagement eher selten, da die JVM diese automatisch sehr zuverlässig verhindert. Es gibt jedoch Spezialfälle, für die dies nicht gilt, nämlich genau dann, wenn
1
Die Ausführungsmodi der JVM lassen sich über JVM-Aufrufparameter steuern: -Xint aktiviert den Interpreter-Modus. Ohne diese Angabe erfolgt eine Ausführung im Hotspot-Modus.
2
JDK 7 beinhaltet einige derart extreme Optimierungen, die sogar zum Teil Probleme bereiten: In namhaften Projekten wie Apache Lucene Core und Solr beobachtet man JVMAbstürze oder auch Berechnungsfehler. Diese Probleme entstehen dadurch, dass mitunter
zu forsch optimiert wird und die erzielten Resultate nicht immer fehlerfrei sind (siehe dazu http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7044738). Einige Fehler wurden mit dem ersten Update vom JDK 7 behoben. Andere Optimierungen sind
immer noch mit Vorsicht zu genießen, etwa die Aufrufoption -XX:AggressiveOpts.
C.1 Wissenswertes rund um die Java Virtual Machine
1407
nicht alle Referenzen auf Objekte korrekt freigegeben werden, etwa weil eine andere
Programmkomponente eine Referenz weiterhin speichert. Details zur Garbage Collection beschreibt Abschnitt 8.5.
Achtung: Speicherverwaltung
Zwar befreit der Garbage Collector den Entwickler von der expliziten Speicherverwaltung und vermeidet mögliche Fehler einer manuellen Speicherfreigabe. Allerdings existieren verschiedene Varianten der Garbage Collection, die sich auf
die Performance auswirken und bei Bedarf mit Bedacht gewählt werden sollten.
Bei nicht zufriedenstellender Performance ist die Analyse der Garbage-CollectionVorgänge mithilfe eines geeigneten Tools, etwa dem zuvor im Buch vorgestellten
VisualVM (vgl. Abschnitt 22.1.4), sehr hilfreich, um mögliche Probleme aufspüren
und beheben zu können.
Ein Schutz vor Fehlern beim Speicherzugriff wird durch verschiedene Sicherheitsmechanismen erreicht. Zum einen kann man mit Referenzen nur auf Objekte zugreifen,
nicht aber mit dem Referenzwert rechnen, wie dies bei einigen anderen Sprachen möglich ist. Auch Bereichsüberschreitungen bei Array-Zugriffen werden von der JVM verhindert – als Folge werden automatisch IndexOutOfBoundsExceptions ausgelöst.
Diese Sicherheitsmechanismen verhindern sowohl das versehentliche als auch das mutwillige Auslesen oder Beschreiben von Speicherbereichen, die eigentlich nicht adressierbar sein sollten. Selbst beim Auftreten derartiger Zugriffsprobleme verbleibt die
JVM in einem definierten, arbeitsfähigen Zustand, und man kann kontrolliert über Exception Handling auf die Fehlersituation reagieren. Im besten Fall existiert dazu ein
catch-Block zur Fehlerbehandlung. Dieser wird durch die JVM angesprungen, sodass die dortigen Anweisungen ausgeführt werden. Eine Fehlersituation bewirkt demnach nur, dass die Anweisungen, die der Exception auslösenden Programmstelle folgen,
nicht mehr ausgeführt werden und stattdessen die Ausführung des Programms mit dem
umgebenden catch-Block fortgesetzt wird. Findet sich kein passender catch-Block
zur Fehlerbehandlung, d. h., wird die Exception nicht im Programm behandelt, so führt
dies zu einem Abbruch des Programms, jedoch nicht zu einem Absturz der JVM.
C.1.3
Sicherheit und Classloading
Beim Start einer JVM ist immer eine Klasse anzugeben, deren main()-Methode ausgeführt werden soll, wie hier für eine Klasse MyClass gezeigt:
java MyClass
Zunächst muss der Bytecode der entsprechenden Klasse in die JVM geladen werden.
Dazu dient eine Instanz eines sogenannten ClassLoaders, der selbst eine Java-Klasse
ist. Diese spezielle Klasse lädt die Klassendateien (.class-Dateien). Was so alles an
Klassen geladen wird, wenn Sie ein Java-Programm starten, sehen Sie durch Angabe
der JVM-Option -verbose beim Aufruf:
Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1
1408
C Grundlagen zur Java Virtual Machine
java -verbose MyClass
Anhand der Ausgabe erkennt man, dass zunächst die wichtigsten Interfaces und Klassen
des JDKs geladen werden:
[Opened
[Loaded
[Loaded
[Loaded
[Loaded
[Loaded
...
[Loaded
[Loaded
[Loaded
...
C:\Programme\Java\jdk1.7.0\jre\lib\rt.jar]
java.lang.Object from C:\Programme\Java\jdk1.7.0\jre\lib\rt.jar]
java.io.Serializable from C:\Programme\Java\jdk1.7.0\jre\lib\rt.jar]
java.lang.Comparable from C:\Programme\Java\jdk1.7.0\jre\lib\rt.jar]
java.lang.CharSequence from C:\Programme\Java\jdk1.7.0\jre\lib\rt.jar]
java.lang.String from C:\Programme\Java\jdk1.7.0\jre\lib\rt.jar]
java.lang.Class from C:\Programme\Java\jdk1.7.0\jre\lib\rt.jar]
java.lang.Cloneable from C:\Programme\Java\jdk1.7.0\jre\lib\rt.jar]
java.lang.ClassLoader from C:\Programme\Java\jdk1.7.0\jre\lib\rt.jar]
Bei diesen Ladevorgängen der Klassendateien finden einige Prüfungen statt, um zu
verhindern, dass Systemklassen verändert werden – insbesondere der ClassLoader
selbst. Anschließend wird durch die Komponente Bytecode Verifier sichergestellt, dass
keine ungültigen Bytecode-Instruktionen in der .class-Datei enthalten sind. Derartige Instruktionen könnten durch Übertragungsfehler oder mutwillige Veränderungen am
Bytecode entstehen. Beispielsweise besitzt die JVM den Befehl iadd zum Addieren
von int-Zahlen. Würde dieser mit zwei float-Werten ausgeführt, käme es zu Problemen in der JVM. Der Bytecode Verifier prüft nun den gesamten Bytecode nach derartigen Verstößen. Wird ein solcher entdeckt, so wird ein java.lang.VerifyError
ausgelöst und das Programm beendet. Damit wird verhindert, dass Instruktionen ausgeführt werden, die möglicherweise eine Fehlfunktion der JVM auslösen könnten.
Werden Klassen per Netzwerk oder aus anderen potenziell gefährlichen Quellen geladen, so ist es aus Sicherheitsgründen zum Schutz vor externen Angriffen wünschenswert, dass für diese Klassen eingeschränkte Rechte gelten. Genau das wird innerhalb
der JVM durch eine Instanz der Klasse SecurityManager sichergestellt. Dieser kann
einem ausgeführten Programm (bzw. dessen Klassen) zur Laufzeit gewisse Aktionen erlauben oder verbieten. Beispielsweise sind für derart geladene Klassen standardmäßig
Dateisystemoperationen verboten. Das ist sinnvoll, um den Rechner, der das Programm
ausführt, vor Angriffen aus dem Netz zu schützen.
Herunterladen