Implementierung eines Java Runtime Systems

Werbung
Software-Engineering Seminar
Implementierung eines Java Runtime Systems
Autoren:
Christian Riekenberg ([email protected])
Matthias von der Grün ([email protected])
Betreuender Dozent:
Olaf Herden
BA-Horb
Studienrichtung Informationstechnik
Projektbearbeitungszeit:
November 2005
Implementierung eines Java Runtime Systems
Abstract
Bei der Implementation einer Java Virtual Machine (JVM), werden verschiedene Techniken
eingesetzt, die nicht nur Plattformunabhängigkeit ermöglichen sollen, sondern auch Java als
Programmiersprache sicher machen sollen. Ein wichtiger Aspekt ist hierbei die Kapselung
der Speicherverwaltung vor dem Programmierer. Es wird veranschaulicht, wie dies mit
unterschiedlichsten Techniken wie Bytecode-Interpretierung einer Runtime oder dem
Garbage Collector ermöglicht wird.
Christian Riekenberg
Matthias von der Grün
2 / 21
05.01.2006
1.
Einführung.......................................................................................................................4
1.1.
Geschichtlicher Hintergrund....................................................................................4
1.2.
Prinzip der plattformunabhängigen Programmierung ..............................................4
1.3.
Verbreitung von Interpreter-Sprachen.....................................................................5
2. Bestandteile einer Implementierung ................................................................................6
2.1.
ClassLoader ...........................................................................................................6
2.2.
Der Interpreter ........................................................................................................7
2.3.
Bytecode Verifizierung ............................................................................................8
2.4.
Built-In Classes.......................................................................................................9
2.5.
Native Methoden.....................................................................................................9
2.6.
Threads ................................................................................................................10
2.7.
Just-In-Time Compiler...........................................................................................11
2.7.1.
Vorteile der JIT-Kompilierung........................................................................11
2.7.2.
Nachteile der JIT-Kompilierung .....................................................................11
2.7.3.
Implementierungen von JIT Compiler............................................................12
3. Garbage Collector .........................................................................................................12
3.1.
Aufgaben ..............................................................................................................12
3.1.1.
"Old Object" - Collector .................................................................................13
3.2.
Funktionsweise .....................................................................................................13
3.3.
Algorithmen ..........................................................................................................13
3.3.1.
Mark-Sweep- und Mark-Compact-Algorithmus..............................................13
3.3.2.
Algorithmen zur Defragmentierung des Speichers ........................................15
3.4.
Konfigurationen.....................................................................................................15
3.4.1.
Standard-Konfiguration .................................................................................16
3.4.2.
Parallel-Konfiguration....................................................................................16
3.4.3.
Concurrent Low Pause Collector...................................................................16
3.4.4.
Parallel Scavenge Collector ..........................................................................17
3.4.5.
Incremental Garbage Colletor (Train Collector) .............................................17
4. Performance und Optimierung ......................................................................................18
4.1.
Hot-Spot-Optimierung ...........................................................................................18
4.2.
Kompiliermethoden...............................................................................................18
4.2.1.
Client Compiler .............................................................................................19
4.2.2.
Server Compiler ............................................................................................19
4.3.
Optimierung der Garbage Collection.....................................................................19
4.3.1.
Garbage Collection – Generational Copying .................................................19
4.3.2.
Inlining frequently-called Methods .................................................................19
5. Zusammenfassung und Zukunftsaussichten .................................................................20
6. Quellenangaben............................................................................................................21
6.1.
Literatur- und Abbildungsverzeichnis ....................................................................21
Implementierung eines Java Runtime Systems
1. Einführung
1.1. Geschichtlicher Hintergrund
Die Entwicklung von Java ist unter anderem auf den Wunsch zurückzuführen, eine
Programmiersprache zu haben, die in Embedded Systems anwendbar sein und dort sicher
und robust laufen sollte. Eine der größten Schwächen der sonst so leistungsfähigen
Programmiersprache C ist die unsichere Handhabung von Zeigern, die schnell zu
Laufzeitfehlern führen kann. Das Projekt „Green Project“ um James Gosling entschied sich
daher eine Sprache zu konzipieren, die diese Nachteile umgeht und gleichzeitig für
Embedded Systeme (wie z.B. Toaster, Videorecorder oder Fernseher) geeignet ist und dort
auch einfach einen Informationsaustausch über Netzwerke ermöglicht. Hierfür wurde das
Prinzip einer plattformunabhängigen, interpretierten Sprache gewählt. Die dazu benötigten
Runtime Environments, die den passenden Interpreter für jedes System enthalten,
organisieren gleichzeitig auch die Speicherverwaltung. Somit ist keine explizite
Speicherverwaltung durch den Entwickler mehr notwendig, was einem System mehr
Stabilität verleiht. Tools wie der Garbage Collector sorgen u.a. dafür, dass nicht mehr
benötigter Speicher freigegeben wird und sich somit eine Anwendung nicht bis zu einem
Buffer Overflow aufstaut.
1.2. Prinzip der plattformunabhängigen Programmierung
In höheren Programmiersprachen wird die Komplexität der Maschinensprache vor dem
Entwickler verborgen. Die erstellten Programme können somit jedoch nicht direkt ausgeführt
werden und müssen zunächst in eine Form gewandelt werden, die ein Computer verarbeiten
kann. Neben der Kompilierung besteht hier die Möglichkeit der Interpretierung von Sprachen.
Interpretierte Sprachen laufen direkt vom aktuellen Quellcode und bieten somit eine
Plattformunabhängigkeit, da der für das jeweilige System angepasste Interpreter alle
plattformspezifischen Besonderheiten behandelt.
Bekannte interpretierte Sprachen sind Basic, Perl, PHP oder Ruby. Shell Skripte und BatchDateien sind Beispiele von einfacheren
interpretierten
Sprachen.
Es gibt verschiedene Stufen der
Konzeption
einer
interpretierten
Sprache. Shell Skripte und BatchDateien sind die reinste Form der
Interpretation, da diese zeilenweise
abgearbeitet werden, während andere
durchaus Präprozessor-Anweisungen
und
Optimierungsschritte
des
ursprünglichen
Code
beinhalten
können, um die Performance zur
Laufzeit zu verbessern.
Ein weiterer Vorteil ist die Möglichkeit
einen Fehler zur Laufzeit mittels
Abbildung 1 : Vorgang eine Java-Kompilierung
Debbuging zu beheben. Wenn bei der
(Quelle: [JAV05])
Ausführung
eines
kompilierten
Programms ein Fehler auftritt, ist es schwierig herauszufinden, wo dieser sich im Quellcode
befindet, da diese Zeile im Code durchaus nicht mehr vorhanden sein kann. Ein
interpretierter Code ist auch bei der Ausführung intakt und bei einem Fehler wird direkt zu
der entsprechenden Zeile gesprungen. Der Nachteil dieser Methode ist der, dass ein
Compiler viele potentielle Fehler im Vorfeld finden und auf Korrektur besteht, ehe das
Christian Riekenberg
Matthias von der Grün
4 / 21
05.01.2006
Implementierung eines Java Runtime Systems
Programm ausgeführt werden kann. Aus diesem Grund sind viele interpretierte Sprachen
auch keine reine Interpretation mehr, sondern werden im Vorfeld kompiliert. Der Java
Compiler ist ein Beispiel dieser Programmiertechnik. Der reine Quellcode wird durch einen
Compiler überprüft und optimiert, jedoch wird bei Java kein Maschinencode erzeugt, sondern
ein Bytecode (welcher immer noch plattformunabhängig ist), der durch einen Interpreter
ausgeführt wird. Der Interpreter ist die Java Virtual Machine (JVM), die es für die meisten
gängigen Systeme gibt. Dieser wird jedoch für die Ausführung benötigt und da ein Code bei
jedem Durchlauf von der JVM interpretiert werden muss, ergibt sich eine enorme
Verschlechterung in der Performance.
Somit ist die Plattformunabhängigkeit immer mit einem Nachteil behaftet, der nicht für jede
Anwendung unbeachtet bleiben kann.
1.3. Verbreitung von Interpreter-Sprachen
Durch die Verbreitung des Internet besteht ein ständig steigender Bedarf an Homogenität
innerhalb von Systemen. Der Aufwand für die Anpassung an sämtlichen Systemen würde
den wirtschaftlichen Rahmen sprengen. Daher wird es immer bedeutsamer, dass
Programme auf heterogenen Systemen lauffähig sind. Folgende Studie der Internetseite
developer.com aus dem Jahre 2004 ergab, dass die Mehrzahl der meist verbreiteten
Programmiersprachen vom Typ der interpretierten sind.
Position Vorheriges Jahr
1
1
2
2
3
3
4
6
5
5
6
4
7
9
8
11
9
7
10
10
Abbildung 2 (Quelle:[DEV05])
Name
Java
C
C++
PHP
(Visual) Basic
Perl
Delphi/Pascal
Python
SQL
JavaScript
Prozent
16,9
16,3
15,3
10,4
10,1
8,4
4,8
4,7
2,8
1,6
Sprachtyp
Compiliert / Interpretiert
Compiliert
Compiliert
Interpretiert
Interpretiert
Interpretiert
Compiliert
Interpretiert
Interpretiert
Interpretiert
Der große Vorteil der Plattformunabhängigkeit wird in den meisten Fällen also dem großen
Nachteil der schlechteren Performance vorgezogen. Die enorme Verbreitung von Java ist
somit dem Trend zu verdanken, ein Programm auf möglichst vielen Systemen, mit ein und
demselben Code zu betreiben.
Christian Riekenberg
Matthias von der Grün
5 / 21
05.01.2006
Implementierung eines Java Runtime Systems
2. Bestandteile einer Implementierung
2.1. ClassLoader
Ein Grundpfeiler der Java Implementation ist das „ClassLoader“-Konzept. Es sorgt dafür,
dass die kompilierten Klassen der Java Runtime zur Verfügung gestellt werden und die Java
Runtime somit nichts vom Dateisystem des jeweiligen Systems wissen muss. Sobald eine
Klasse aus einer anderen Klasse heraus referenziert wird, sorgt der ClassLoader dafür, dass
diese der Runtime mit eigenem Namensraum zur Verfügung gestellt wird.
Dazu folgendes Beispiel:
class A
{
static String s = new java.util.Date().toString();
}
public static void main( String args[] )
{
B b = new B();
}
class B
{
A a;
}
Wird nun die Klasse A gestartet, werden automatisch auch andere Klassen aufgrund von
Referenzen geladen. Da in der main()-Funktion ein Objekt der Klasse B erzeugt wird, ist es
nachvollziehbar, dass auch hierfür ein Namensraum durch den class loader bereitgestellt
werden muss. Nicht ganz so deutlich ist jedoch, dass auch die statische Variable in der
Klasse A dafür sorgt, dass eine weitere Klasse geladen werden muss. Für die
Datumsermittelung wird die Klasse java.util.Date benötigt, die sich in der Standard-Bibliothek
von Java befindet. Aus dieser Standard-Bibliothek wird auch die Klasse java.lang.Object
bezogen, da jede Klasse automatisch von der Object-Klasse erbt und implizit bei der
Klassendefinition von A steht : class A extends Object.
Für die verschiedensten Klassen stehen unterschiedliche ClassLoader zur Verfügung, die in
einer fest definierten Reihenfolge nach der Klasse suchen. Die Klassen der StandardBibliothek, die sogenannten Bootstrap-Klassen befinden sich in einem jar-Archiv (zumeist
lib/rt.jar) und werden zuerst durchsucht. Bei ergebnisloser Suche werden die Archive des
Erweiterungs-Verzeichnis lib/ext durchsucht. Dies geschieht automatisch, ohne dass ein
Pfad hierzu angegeben werden muss. Ist auch hier die Suche erfolglos, wird der
Klassenpfad durchlaufen und durchsucht.
Wie erwähnt können bei der Suche nach einer Klasse verschiedene ClassLoader involviert
sein. Folgende ClassLoader sind für das Laden unterschiedlicher Klassen zuständig:
System-ClassLoader
AppletClassLoader
RMIClassLoader
Christian Riekenberg
Matthias von der Grün
Auch Bootstrap-ClassLoader genannt und ist für die
Suche innerhalb der Standard-Bibliothek zuständig
Wird verwendet um Java-Applets aus dem Netz zu laden.
Lädt entfernte Objekte, die per RMI lokal verfügbar
gemacht werden sollen
6 / 21
05.01.2006
Implementierung eines Java Runtime Systems
2.2. Der Interpreter
Der Interpreter ist das Kernstück der JVM und dient dazu, kompilierte Java-Programme
auszuführen, die als Bytecode in einer class-Datei vorliegen. Aus der Konsole wird der
Interpreter wie folgt aufgerufen :
java Options Classname Arguments
Der Interpreter ruft die main()-Methode der angegebenen Klasse auf und wird erst beendet,
nachdem alle Threads die innerhalb dieser main()-Methode erstellt wurden, beendet sind.
Die angegebenen Argumente stehen in der main()-Methode als String-Array zur Verfügung
und können u.a. zur Steuerung des Programms genutzt werden.
Die Interpretation kann auf verschiedenen Arten geschehen (siehe Abbildung 3), wobei hier
nur der JIT-Compiler (siehe Abschnitt 3) und der normale Java-Interpreter betrachtet werden
soll.
Abbildung 3: Vorgang der Interpretation (Quelle:[LIN05])
Zum Ausführen der Bytecode Befehle wird ein Java Interpreter verwendet, der die einzelnen
Befehle nacheinander interpretiert. Entsprechend der Bedeutung eines Bytecode Befehls
wird eine bestimmte Aktion durchgeführt. Dies ist die am meisten bevorzugte
Implementierung der virtuellen Java Maschine. Diese Softwareimplementierung ist
gegenüber
den
anderen
Möglichkeiten
relativ
einfach,
dafür
leidet
die
Ausführungsgeschwindigkeit darunter. Allerdings ist die Bezeichnung Java Interpreter etwas
irreführend, da nicht Java Quelltext sondern Bytecode interpretiert wird, weshalb die
Bezeichnung Bytecodeinterpreter hier besser geeignet scheint.
Christian Riekenberg
Matthias von der Grün
7 / 21
05.01.2006
Implementierung eines Java Runtime Systems
2.3. Bytecode Verifizierung
Normalerweise wird Java Bytecode durch einen Java-Compiler (wie z.B. javac) erzeugt.
Jedoch ist es auch
möglich, einen BytecodeAssembler wie Jasmin zu
verwenden
um
nicht
verifizierten Bytecode zu
erstellen.
Bei
einem
solchen
Bytecode
ist
man
Mächtigkeit von
Mächtigkeit von
Mächtigkeit
von
Mächtigkeit
von
in
der
Lage
fehlerhaften
Bytecode, der aus
Bytecode
oder bösartigen Code zu
Bytecode
der aus
Bytecode
Java übersetzt
wurde
erzeugen,
da zum einen
Java übersetzt wurde
dieser Code nicht durch
den
Java-Compiler
überprüft
wurde.
Die
Mächtigkeit von Bytecode
ist höher als die von
Bytecode der durch Java
erzeugt wurde. Dies ist
Abbildung 4 : Mächtigkeit von Bytecode
bei
Applets
zu
verdeutlichen, denen es nicht gestattet ist Datei-Manipulationen vorzunehmen.
Des weiteren werden in Java Klassen erst bei Bedarf geladen, was bedeuten kann, dass zu
diesem Zeitpunkt nicht sichergestellt ist, dass die referenzierten Methoden und
Klassenvariablen auch wirklich (noch) existieren. Außerdem kann die Klassen-Datei von
einem nicht vertrauenswürdigen Compiler erzeugt worden sein, der nicht alle
Beschränkungen einhält, die für Klassen-Dateien gelten. Aus diesen Gründen findet beim
Laden von Klassen-Dateien eine Überprüfung statt, diese Überprüfung wird in 4 Phasen
durchgeführt:
1. Überprüfung, ob das Format der Klassen-Dateien eingehalten wird.
2. Überprüfung von allgemeinen Bedingungen, wie z.B.
- Sicherstellung, dass finale Klassen keine Unterklassen besitzen bzw. finale
Methoden nicht überschrieben werden.
3. Überprüfung des Bytecodes
- Überprüfung, ob zur Laufzeit keine unzulässige Datentypumwandlung stattfindet,
- Methoden mit zulässigen Argumenten initialisiert werden,
- das kein Stack Over- oder Underflow entsteht.
In der dritten Phase, die während der Programmausführung durchgeführt wird, werden
bestimmte Bytecode Befehle nach einer Überprüfung durch ihre Quick Variante ersetzt,
damit vor der nächsten Ausführung der Befehl nicht noch einmal überprüft wird. So müssen
z.B. bei einem Methodenaufruf die Anzahl und die Typen der Parameter überprüft werden.
Da dies in der Regel erst zur Laufzeit erfolgen kann, wird nach einer erfolgreichen
Überprüfung der Befehl durch seine Quick Variante ersetzt, damit bei einer evtl. weiteren
Ausführung dies nicht noch einmal überprüft werden muss.
Bei einer fehlerhaften Implementierung des Bytecode Verifiers, erlaubte es bei früheren
Internet Explorer Versionen eine unzulässige Typumwandlung vorzunehmen, was
ermöglichte Zeiger umzusetzen um so nicht erlaubter Operationen durchzuführen. Dies
verdeutlicht wie wichtig die zeitaufwendige Verifizierung ist.
Christian Riekenberg
Matthias von der Grün
8 / 21
05.01.2006
Implementierung eines Java Runtime Systems
2.4. Built-In Classes
Bei einer Implementierung der JVM wird eine Bibliothek mit Standardpaketen zur Verfügung
gestellt, die viele Routinearbeiten wie z.B. das Verarbeiten von Dateien vereinfachen. Diese
sind in der Java API-Dokumentation ausführlich beschrieben (java.sun.com) und es sollen
hier nur einige exemplarisch genannt werden:
•
•
•
•
•
java.lang
java.io
java.util
java.applet
java.awt
Die einzelnen Pakete enthalten Klassen oder Unterpakete, deren Funktionalität so genutzt
oder durch überschreiben den eigenen Bedürfnissen angepasst werden können.
2.5. Native Methoden
Die Plattformunabhängigkeit von Java kann nicht in jeder Situation aufrechterhalten werden.
Was bei der Programmierung von Algorithmen keine Probleme bereitet, erweist sich beim
bearbeiten von Dateien oder beim pixelgenauen Ausgeben auf einem Bildschirm als eine
Funktionalität, die sehr von der Architektur eines Systems abhängig ist. Aus diesem Grund
sind viele Funktionalitäten in native Methoden eingebunden, die meist schnelleres oder
hardwarenahes C benutzen. Dies wird selbst in der Standard-API verwendet und ist dort
nicht immer ersichtlich. Als Beispiel für eine Native Methode dient read() aus der Klasse
java.io.FileInputStream, die
ein Byte aus einer Datei
liest. Hier hinter verbirgt
sich C-Code, der von Java
heraus aufgerufen wird.
Das Java Native Interface
(JNI) wurde aus zwei
Gründen implementiert. Der
im Beispiel erwähnte Zugriff
auf Systemressourcen stellt
hierbei den wichtigsten
Grund dar. Ein weiterer ist
die
Steigerung
von
Performance, die durch
Ausführung von optimiertem
C-Code
in
einigen
Bereichen erreicht werden
kann. Diese Steigerung wird
Abbildung 5: Vorgehensweise mit Nativen Methoden
jedoch auf Kosten der
(Quelle:[STA05])
Portabilität erreicht und eine
Verwendung von nativen Methoden führt unweigerlich zur Aufgabe der drei Eigenschaften
von Java : einfach, sicher und zuverlässig. Es wird nicht nur die Systemunabhängigkeit
aufgegeben, so dass für jedes System die Funktionalität neu implementiert werden muss,
sondern auch die Komplexität und Wartbarkeit wird dadurch verschlechtert.
Christian Riekenberg
Matthias von der Grün
9 / 21
05.01.2006
Implementierung eines Java Runtime Systems
2.6. Threads
Die ursprüngliche Form der Programmierung sieht einen sequentiellen Programmablauf vor.
Es gibt innerhalb der Laufzeit einen eindeutigen Einstiegspunkt, den Programmablauf und
einen eindeutigen Ausstiegspunkt. Ein einzelner Thread verhält sich äquivalent zu diesem
Verlauf. Ein Thread ist jedoch kein
eigenständiges Programm und ist nicht
imstande, selbständig ausgeführt zu werden.
Es wird innerhalb eines Programms
ausgeführt. Der Vorteil ist, dass mehrere
Threads parallel laufen können und somit
eine Pseudo-Mutlithreading ermöglichen,
indem jedem Thread eine definierte Zeit des
gesamten Prozesses zur Verfügung gestellt
wird. Der Grund, warum Threads in Java
implementiert wurden ergibt sich aus den
Anforderungen zeitgemäßer Programme.
Für einen Webbrowser ist es z.B.
unumgänglich mehrere Threads parallel
laufen zu lassen, um zeitsynchron mehrere
Aufgaben zu erledigen. So wird es
ermöglicht, das eine Seite durchsucht wird,
während zeitgleich ein Applet geladen und
ein Download im Hintergrund läuft.
Um dies auch auf einem System mit nur
einer CPU zu realisieren, bedarf es interner
Abbildung 6: Graphische Darstellung von
Regeln, um dieses Pseudo-Threading zu
Threads (Quelle:[SUN05])
ermöglichen.
Threads
können
u.a.
Prioritäten zugeordnet werden und ein Thread läuft solange bis:
• ein Thread mit einer höheren Priorität startet (präemptives Multitasking),
• er beendet wird,
• er freiwillig an den nächsten Thread weiter gibt,
• die Scheduler-Zeit dem Prozess die Prozessorzeit entzieht.
Jedoch ist nicht garantiert, dass immer der Thread mit der höchsten Priorität läuft, da bei der
Implementierung darauf geachtet wurde, das keine Threads ewig laufen, weil sie zu niedrig
priorisiert wurden. Die Priorisierung ist somit vielmehr ein Hinweis auf die Bevorzugung als
ein wirklicher Befehl.
Ein weiterer Aspekt ist die Möglichkeit, Threads untereinander zu synchronisieren. Sollten
mehrere Threads an denselben Daten arbeiten kann es wichtig sein, dass zunächst ein
Thread beendet wird bevor ein anderer startet, um die Konsistenz von Daten zu
gewährleisten. Dies ist zum Beispiel bei Programmen der Fall, die Daten im- und
exportieren. Ein Export ist erst dann möglich, wenn alle Daten korrekt eingelesen wurden.
Hierfür biete Java Funktionalitäten wie synchronized, wait oder notiyAll um eine
Kommunikation zwischen den einzelnen Threads zu ermöglichen.
Die Implementierung von Threads ist jedoch mit Sorgfalt zu verwenden, denn ein zu
prozessintensiver oder nicht beendeter Thread kann andere Prozesse blockieren oder zu
Systemabstürzen führen.
Christian Riekenberg
Matthias von der Grün
10 / 21
05.01.2006
Implementierung eines Java Runtime Systems
2.7. Just-In-Time Compiler
Bei der Just-In-Time-Kompilation ruft der Java Interpreter Methoden auf, die während dem
Abarbeiten des Programms den Bytecode in Maschinencode übersetzen. Der Ablauf gliedert
sich folgendermaßen:
Die erste Methode bildet den Stack auf dem Stack der Registermaschine ab und holt das
erste Element in den Akku. Diese Methode ist charakterisiert durch eine schnelle
Implementierung und kurze Compilierungszeit, aber auch durch unnötige Speicherzugriffe.
Anschließend wird der Code in einer zweiten Methode in Zwischencode mit unendlich vielen
Pseudoregistern übersetzt. Die Pseudoregister werden abgebildet auf den
Maschinenregistern. Diese Methode sorgt für schnellere Ausführung, die Kompilierung
dauert aber länger.
Am nachfolgenden Beispiel kann man die Kompilierung eines JIT-Compilers mit einem CCompiler vergleichen:
Beispiel: b=a+c*d
•
JIT-Compiler:
1. Iload a
MOV EAX,a
MOV EBX,c
2. Iload c
3. Iload d
MOV ECX,d
IMUL EBX,ECX
4. Imul d
5. Iadd
ADD EAX, EBX
6. Istore
MOVE b,EAX
•
Kompilierung mit einem C-Compiler:
1. MOV EAX,c
2. IMUL EAX, d
3. ADD EAX, a
4. MOV b, EAX
2.7.1. Vorteile der JIT-Kompilierung
Es wird die Plattformunabhängigkeit bewahrt, weil die Kompilierung erst zur Laufzeit
ausgeführt wird. Die Vorteile zusammengefasst:
•
•
plattformunabhängig
Schnellere Abarbeitungsgeschwindigkeit als reine Interpretierung
2.7.2. Nachteile der JIT-Kompilierung
Es enstehen allerdings auch Nachteile gegenüber einer "herkömmlichen" Kompilierung. Zum
einen ist die JIT-Kompilierung oftmals ineffizient, was die Folge einer schnellen Kompilierung
ist. Zum anderen besteht ein grundsätzlicher Nachteil darin, dass der kompilierte Code
verworfen wird und bei jedem Start neu erstellt werden muss. Weiterhin kann es vorkommen,
dass Methoden kompiliert werden, und anschließend eventuell nur einmal aufgerufen
werden.
Christian Riekenberg
Matthias von der Grün
11 / 21
05.01.2006
Implementierung eines Java Runtime Systems
2.7.3. Implementierungen von JIT Compiler
Es existieren unterschiedliche Implementierungen für den JIT-Compiler. Die bekanntesten
davon sind CACAO, KAFFE und TYA.
CACAO ist eine research Java Virtual Machine, die am Institut für Computersprachen der TU
Wien 1996 entwickelt wurde. Mit der Version 0.92 ist inzwischen eine relativ stabile Version
erreicht worden, mit der z.B. Eclipse oder Jakarta Tomcat laufen. CACAO kann auf
verschiedenen Betriebssystemen compiliert werden, wie z.B. Linus, FreeBSD, Darwin, IRIX.
Seit 2004 unterliegt CACAO der General Public License(GPL). Die offizielle Referenz für
diese Implementierung des JIT Compilers findet man unter [CAC05].
KAFFE wurde von einer freien Entwicklergemeinde implementiert, wird ständig
weiterentwickelt und ist kompatibel zum aktuellen Java-Standard. Das Projekt unterliegt der
GPL. Zahlreiche Produkte verwenden bereits KAFFE als Laufzeitsystem. KAFFE ist eine
eigenständige Entwicklung und baut nicht auf Suns Quellcode auf. Die Vorgaben zur Virtual
Machine werden jedoch eingehalten. Die offizielle Referenz für diese Implementierung findet
man unter [KAF05].
Ganz ohne Laufzeitsystem kommt TYA aus. Diese Implementierung linkt sich in den
Methodenaufruf eines bestehenden Java Interpreters ein. Wird eine Methode aufgerufen, so
tritt stattdessen TYA in Kraft. Ist die Methode noch nicht compiliert, wird dies nachgeholt,
ansonsten ausgeführt. Auch diese Implementierung ist frei erhältlich und kann jederzeit einund ausgeschaltet werden. Eine Download-Seite zu dieser Implementierung findet man unter
[TYA05].
3. Garbage Collector
Übersetzt man den Begriff Garbage Collector in das Deutsche, dann kann man den Begriff
Müllabfuhr benutzen. Der Garbage Collector ist folglich eine Art Müllabfuhr bei der
Speicherverwaltung. Er löscht alle Objekte einer Applikation, die nicht mehr benötigt werden,
oder anders ausgedrückt: Er muss möglichst schnell den Müll einsammeln, den der
Programmierer hinterlassen hat.
Damit wird dem Programmierer die Aufgabe der Speicherfreigabe abgenommen. Deshalb
gibt es in Java keinen Befehl zur Speicherfreigabe, sondern nur zum Erzeugen von
Objekten.
Im nachfolgenden werden wichtige Aufgaben, die Funktionsweise und unterschiedliche
Ausführungen des Garbage Collectors erläutert.
3.1. Aufgaben
Der Garbage Collector erfüllt drei wichtige Aufgaben:
•
•
•
Auffinden von nicht mehr referenzierten Objekten,
Löschen der nicht mehr referenzierten Objekte,
Speicherreorganisation.
Werden diese Aufgaben durch den Programmierer erledigt, steigt zwar die Effizienz der
Applikation, jedoch ist die Fehlerträchtigkeit, dass Speicher nicht freigegeben wird, sehr
hoch.
Ein Problem des Garbage Collector ist allerdings, dass er nicht, wie oftmals angenommen,
alle Objekte, die nicht mehr benötigt werden, freigibt, sondern nur die meisten erkennt,
nämlich alle die, die nicht mehr referenziert werden. Existieren also beispielsweise noch
Verweise auf nicht mehr benötigte Objekte, dann kann der Garbage Collector diese nicht
löschen.
Christian Riekenberg
Matthias von der Grün
12 / 21
05.01.2006
Implementierung eines Java Runtime Systems
Je nach Anwendung muss der Garbage Collector seine Aufgaben unterschiedlich erledigen,
bei zeitkritischen Anwendungen beispielsweise muss der Garbage Collector seine Aufgaben
in einem eigenen Thread nebenher erledigen, damit das Programm möglichst selten
unterbrochen wird.
3.1.1. "Old Object" - Collector
Der "Old Object"- Collector wird auch als Mark Compact-Collector bezeichnet. Er beinhaltet
zwei Phasen.
In der ersten Phase werden alle erreichbaren Objekte markiert. Als Wurzel steht hier
jedesmal ein Eintrag auf dem Stack. In der zweiten Phase werden Objekte verschoben, um
die Speicherlücken zu beseitigen (Defragmentierung).
3.2. Funktionsweise
Analog zu den Aufgaben lässt sich der Prozess des Garbage Collectors in drei Phasen
aufteilen.
Zuerst werden die Objekte auf dem Heap gesucht, die entfernt werden können. In der
zweiten Phase werden diese Objekte gelöscht. In einer letzten optionalen Phase wird der
Speicher defragmentiert.
Die Garbage Collection findet in zwei unterschiedlichen Teilen statt:
•
•
Minor Collection
Major Collection
Die Minor Collection kann parallel zur Applikation ausgeführt werden, für die Major Collection
hingegen muss die Applikation hingegen unterbrochen werden.
3.3. Algorithmen
Für die Art der Garbage Collection existieren eine Vielzahl verschiedener Algorithmen. Zu
den gängigsten Implementierungsalgorithmus gehört der "Mark-Sweep-Algorithmus".
3.3.1. Mark-Sweep- und Mark-Compact-Algorithmus
Die wichtigsten Schritte dieses Algorithmus können folgendermaßen zusammengefasst
werden:
1. "Mark-Phase":
Periodisches Durchsuchen des Speichers nach Objekten, die von laufenden
Programmen noch erreicht werden können. Dabei werden folgende Schritte
ausgeführt:
a.
b.
c.
d.
Wurzeln werden auf den Stack gelegt
Stack wird bearbeitet, bis er leer ist
Objektprüfung, wenn das Objekt unmarkiert ist, wird das "Mark-Bit" gesetzt
Wenn alle erreichbaren Objekte markiert wurden und der Stack leer ist, kann
vom restlichen Speicher angenommen werden, dass er frei ist
Christian Riekenberg
Matthias von der Grün
13 / 21
05.01.2006
Implementierung eines Java Runtime Systems
2. "Sweep-Phase":
a.
b.
c.
d.
e.
Durchlaufen des Heaps
Hinzufügen der unmarkierten Speicherbereiche zu einer Freispeicherliste
Zurücksetzen der markierten Bereiche für die nächste Garbage Collection
Nachfolgende Allokationen werden aus der Freispeicherliste bedient
Defragmentierung des Speichers, um einen möglichst großen freien
Speicherbereich zu erreichen
Die Wurzelzeiger befinden sich in lokalen
Variablen auf dem Stack, in globalen (statischen
Variablen) und in Registern. Alle Zeiger verweisen
auf Objekten, die auf dem Heap liegen. Hier wird
die Erreichbarkeit in Form eines Graphen
dargestellt. Ein interner Zähler zählt die Anzahl
der Referenzen, die auf ein Objekt verweisen.
Sollte dieser Zähler auf 0 dekrementiert werden,
deutet es auf ein verwaistest Objekt hin. Diese
werden in der Mark-Phase nicht mit dem Mark-Bit
versehen und somit in der Sweep-Phase wieder
freigegeben.
So entstehen jedoch Speicherfragmentierungen,
Abbildung 7: Mark-Phase (Quelle:[SUI05])
die sich beim Anlegen neuer Objekte störend
auswirken, da eine Suche nach einem passenden
Speicherbereich zwischen den Fragmentierungen zeitintensiv ist.
Um die Speicherfragmentierungen zu verhindern, geht der Mark-and-Compact-Algorithmus
noch einen Schritt weiter. Hierbei werden genau wie beim einfachen Mark-Sweep-Verfahren
die Heap-Objekte durch einen Graphen symbolisiert. Um bei der Garbage Collection direkt
auch eine Defragmentierung zu erzielen, werden die erreichbaren Objekte zusätzlich in
einen freien Speicherbereich kopiert. Die Wurzelzeiger werden anschl. auf den neuen
Speicherbereich angepasst.
Vor Garbage Collection
Abbildung 8: Sweep-Phase (Quelle:[SUI05])
Nach Garbage Collection
Abbildung 9: Sweep-Phase (Quelle:[SUI05])
Obwohl das Umbiegen der Wurzelzeiger aufwendig ist, spart es für neu angelegte Objekte
Zeit. Da der Speicher nach einem solchen Durchgang quasi defragmentiert ist, kann
Speicherbereich für neu anzulegende Objekte schnell allokiert werden. Die Objekte, welche
Christian Riekenberg
Matthias von der Grün
14 / 21
05.01.2006
Implementierung eines Java Runtime Systems
die Bereinigung „überlebt“ haben, befinden sich am Speicheranfang und neuer Speicher
kann am Ende dieser Obkjektkette allokiert werden.
Während der Garbage Collection befindet sich das System im unstabilen Zustand, was zur
Folge hat, dass die Applikation warten muss, bis die Garbage Collection abgeschlossen ist.
Das kann zu Performance-Einbußen führen, je größer der Speicherbereich ist, desto
drastischer fällt die Performance-Einbuße aus. Für Echtzeit-Anwendungen eignet sich dieses
Prinzip nur bedingt, da hierbei die Antwortzeit einer Anwendung sehr hoch sein kann.
Zusammenfassend lässt sich die Funktion des Mark-Sweep-Algorithmus wie folgt
beschreiben:
•
•
•
•
Erkennung von "lebenden" Objekten,
Entfernen von "toten" Objekten,
Erneuern von Referenzen,
Defragmentierung des Speichers.
3.3.2. Algorithmen zur Defragmentierung des Speichers
Zur Defragmentierung werden unterschiedliche Algorithmen eingesetzt. Hier die
bekanntesten:
•
•
•
Two-Finger-Algorithmus
Lisp 2 – Algorithmus
Haddon-Waite Algorithmus
Die verschiedenen Algorithmen haben in verschiedenen Einsatzgebieten ihre Stärken und
Schwächen. Einige sind auf Optimierung von Speichern getrimmt, die in gleich großen
Blöcken aufgeteilt sind, während andere auf selbst generierte Speicher-Tabellen zugreifen.
Im nachfogenden wird die Größe des Speichers mit M symbolisiert.
Grundsätzlich wird bei den Algorithmen zwischen gleitender und zufälliger Kompaktierung
unterschieden. Bei der gleitenden Kompaktierung wird die relative Anordnung der Knoten
beibehalten und an den Anfang des Speichers geschoben. Der Lisp2-Algorithmus und der
Haddon-Waite-Algorithmus sind für diese Kompaktierungsart Beispiele. Der Lisp2Algorithmus hat eine Laufzeit von O(M), wobei allerdings ein zusätzlicher Zeiger pro Knoten
notwendig ist. Der Haddon-Waite-Algorithmus hat eine Laufzeit von O(M log(M)), benötigt
dafür aber auch keinen zusätzlichen Speicher. Der Two-Finger-Algorithmus arbeitet auf der
Basis zufälliger Kompaktierung, er hat einen Laufzeitaufwand von O(M). Detailliert werden
die Verfahren in [PSU05] beschrieben.
3.4. Konfigurationen
Der Garbage Collector kann je nach Anwendungsbereich unterschiedlich konfiguriert
werden.
Folgende Konfigurationen sind möglich:
•
•
•
•
•
Standard (Mark Compact),
Parallel,
Concurrent Low Pause Collector
Parallel Scavenge Collector
Incremental Garbage Colletor (Train Collector)
Christian Riekenberg
Matthias von der Grün
15 / 21
05.01.2006
Implementierung eines Java Runtime Systems
3.4.1. Standard-Konfiguration
Die Standard-Konfiguration ist defaultmäßig bei jeder Applikation eingestellt. Bei dieser
Konfiguration sollten Pausen keine Rolle spielen. Für Echtzeitanwendungen ist diese
Konfiguration ungeeignet. Auf einer Mehr-Prozessor-Maschine ist dieser Algorithmus desto
ineffizienter je mehr Prozessoren im Spiel sind. Wird auf einer Mehr-Prozessor-Maschine
eine Garbage Collection mit Standard-Konfiguration ausgeführt, dann ist ein Prozessor damit
beschäftigt, die Collection auszuführen, alle anderen Prozessoren müssen warten. Die
Auslastung ist in solch einem Falle sehr gering. Für eine Ein-Prozessor-Maschine hingegen
ist derzeit kein effizienterer Algorithmus bekannt.
Zusammenfassend die Merkmale dieser Konfiguration:
•
•
•
derzeit effizienteste Konfiguration für Ein-Prozessor-Maschine
desto ineffizienter je mehr Prozessoren beteiligt
ungeeignet für Echtzeit-Anwendungen.
3.4.2. Parallel-Konfiguration
Wenn man Applikationen für Mehrprozessor-Maschinen implementiert, empfiehlt sich die
Parallel-Konfiguration. Sie eignet sich weniger für Ein-Prozessor-Systeme, sondern für MehrProzessor-Maschinen mit acht Prozessoren und aufwärts. Wird eine Garbage Collection
ausgeführt, dann wird nicht mehr nur ein Prozessor ausgelastet, sondern möglichst alle.
Standardmäßig ist die Zahl der Threads für die Garbage Collection gleich der ProzessorAnzahl. Das bedeutet, jeder Prozessor wird bei der Garbage Collection teilweise ausgelastet.
Der Entwickler kann die Anzahl der Threads jedoch nach Bedarf verändern. Die Pausen, die
durch den Garbage Collector entstehen, werden so minimiert.
Der parallel Copy Collector räumt in mehreren Threads gleichzeitig auf und nutzt so die
Prozessoren besser. Dadurch steigt die Auslastung der Prozessoren und die Effizienz nimmt
zu.
Zusammenfassend die Merkmale dieser Konfiguration:
•
•
•
•
•
•
ineffizienter für Ein-Prozessor-Maschinen als Standard-Konfiguration
effizient für Mehr-Prozessor-Maschinen
Minimierung der Pausen auf Mehr-Prozessor-Maschinen
arbeitet mit mehreren Threads parallel
standardmäßig so viele Threads wie Prozessoren, aber variabel veränderbar
ungeeignet für Echtzeit-Anwendungen.
3.4.3. Concurrent Low Pause Collector
Eine mögliche Konfiguration für Echtzeitanwendungen ist der Concurrent Low Pause
Collector. Hier werden die Pausen der Garbage Collection minimiert, indem alle
synchronisierbaren Schritte parallel zur Applikation ausgeführt werden. Lediglich für einige
nicht-synchronisierbare Schritte wird die Applikation kurz unterbrochen.
Parallel zur Applikation läuft ein Thread, der für das "Müll sammeln" verantwortlich ist. Dazu
braucht die Applikation nicht gestoppt werden. Die übrigen Schritte des Garbage Collectors,
wie beispielsweise die Defragmentierung werden in kurzen Pausen ausgeführt.
Für Mehrprozessor-Systeme kann eine Variante dieser Konfiguration gewählt werden, bei
der mehrere Sammel-Threads verwendet werden.
Christian Riekenberg
Matthias von der Grün
16 / 21
05.01.2006
Implementierung eines Java Runtime Systems
Zusammenfassend die Merkmale dieser Konfiguration:
•
•
•
•
Geeignet für Echtzeitanwendungen
Nur nicht-synchronisierbare Collection-Schritte werden in kurzen Pausen ausgeführt
parallel zur Applikation läuft ein Thread, der die meisten Schritte der Garbage
Collection ausführt
Variante für Mehrprozessorsysteme, bei der mehrere Threads parallel zur Applikation
ausgeführt werden.
3.4.4. Parallel Scavenge Collector
Diese Konfiguration eignet sich ähnlich wie der Concurrent Low Pause Collector für
Echtzeitanwendungen. Optimiert ist diese Konfiguration für Mehr-Prozessor-Systene mit
mindestens acht Prozessoren. Außerdem ist diese Konfiguration optimiert für die Verwaltung
von großen "Young Generation Heaps" im Gigabyte-Bereich. Diese Konfiguration arbeitet
ähnlich dem Concurrent Low Pause Collector.
Zusammenfassend die Merkmale dieser Konfiguration:
•
•
•
•
Geeignet für Echtzeitanwendungen
Arbeitsweise ähnlich dem Concurrent Low Pause Collector
optimiert für Mehr-Prozessor-Systeme mit mindestens acht Prozessoren
optimiert für die Verwaltung von großen "Young Generation Heaps" im GigabyteBereich.
3.4.5. Incremental Garbage Colletor (Train Collector)
Analog zur Konfiguration "Standard" gibt es auch bei den echtzeitfähigen Konfigurationen
eine ähnliche Variante, die als Incremental Garbage Collector bezeichnet wird. Sie hat ihre
Vorteile im Bereich der Ein-Prozessor-Maschinen.
Es wird versucht, alle Aufgaben in die Minor Collections zu verlagern, damit die Major
Collections, auch als "Full Garbage Collection bezeichnet", möglichst minimiert oder sogar
ganz vermieden werden können. Als Folge dieser Arbeitsweise verlängern sich die Minor
Collections.
Zusammenfassend die Merkmale dieser Konfiguration:
•
•
•
•
•
Geeignet für Echtzeitanwendungen
Arbeitsweise analog zur Konfiguration "Standard"
Verschieben der Aufgaben von Major Collections hin zu Minor Collections
Minimerung oder Vermeidung der Major Collections
längere Minor Collections.
Christian Riekenberg
Matthias von der Grün
17 / 21
05.01.2006
Implementierung eines Java Runtime Systems
4. Performance und Optimierung
Um die Performance einer Applikation zu beurteilen und sie zu optimieren, ist es wichtig,
zuerst festzustellen, welche Zeit eine Applikation für die einzelnen Teilabläufe benötigt.
Die benötigte Zeit für einen Programmablauf lässt sich in folgendem Diagramm darstellen:
Thread
Synchronisation :
19%
Native Methoden:
1%
1
2
Allocation und
Garbage
Collection: 20%
3
Interpretation:
60%
4
Abbildung 10: Teilablauf-Diagramm für eine Applikation
4.1. Hot-Spot-Optimierung
Durch die Hot-Spot-Optimierung wird versucht, die besten Eigenschaften von JIT Compiler
und Interpreter zu kombinieren. Außerdem werden Profiler eingesetzt, die wiederum
unterschiedliche Kompiliermethoden auswählen. Die Arbeitsweise kann man in folgende
Schritte unterteilen:
1. Interpretierung des Bytecode von der JVM
2. Optimierung des kompilierten Codes
a. Im Fehlerfall kann der kompilierte Code wieder neu interpretiert werden
3. Arbeit des Profilers:
a. Sammeln von Informationen über die Performance einer Funktion während
des Ablaufs
b. Wählen einer passenden Kompilier-Methode nach einer bestimmten Heuristik
c. Speichern der kompilierten Methoden im Cache des nativen Maschinencodes
d. Bei Aufruf der entsprechenden Methode wird im Cache nach der Methode
gesucht, wenn sie nicht gefunden wird, muss sie interpretiert werden
4.2. Kompiliermethoden
Der Profiler kann dabei zwischen zwei unterschiedlichen Kompiliermethoden unterscheiden.
Zum einen kann entweder den Client-Compiler oder den Server-Compiler wählen. Diese
beiden Compiler werden in den folgenden Unterkapiteln beschrieben.
Christian Riekenberg
Matthias von der Grün
18 / 21
05.01.2006
Implementierung eines Java Runtime Systems
4.2.1. Client Compiler
Diese Kompiliermethode eignet sich besonders für kurzlebige, interaktive GUI-Applikationen.
Für Applikationen und Applets ergibt sich eine verbesserte Laufzeit. Das hängt im
wesentlichen davon ab, weil weniger Optimierungen als beim Server Compiler durchgeführt
werden, und weil weniger Zeit für die Analyse benötigt wird. Dadurch reduziert sich die
Kompilierungszeit. Deshalb eignet sich diese Methode besonders für den interaktiven
Einsatz. Allerdings ist der Client Compiler ineffizienter in der Ausführung als der Server
Compiler.
4.2.2. Server Compiler
Diese Methode bietet bessere Optimierungsalgorithmen und ist effizienter als der ClientCompiler. Besonders für langlebige Funktionen eignet sich diese Kompiliermethode.
Die Register werden dabei durch Graph coloring allokiert, und polymorphe Aufrufstellen
werden durch vtables statt durch polymorphic inline caches realisiert. Im Vergleich zum
Client-Compiler zeichnet sich der Server-Compiler durch gründlichere Compilierung mit mehr
Analyse und mehr Optimierung aus. Das führt zu einer längeren Compilierungszeit, aber
auch zu einer effizienteren Ausführung, die besonders für langlebige Funktionen wichtig ist.
4.3. Optimierung der Garbage Collection
Um die Garbage Collection zu verbessern und performanter zu gestalten, wird der Speicher
zum einen in unterschiedliche Bereiche unterteilt (Garbage Collection-General Copying),
zum anderen wird manchmal der Quellcode einer Methode an die Aufrufstelle eingefügt
(Inlining frequently-called Methods). Diese beiden Optimierungsarten werden in den
nächsten beiden Unterkapiteln näher erläutert.
4.3.1. Garbage Collection – Generational Copying
Der Speicher wird bei dieser Optimierungsmethode in verschiedene Bereiche eingeteilt. Zum
einen gibt es einen Bereich für Objekte, die noch "jung" sind, also erst vor kurzem erzeugt
wurden. Dieser Speicherbereich wird oft gescannt, da die meisten dieser Objekte kurzlebig
sind. Nur ca. 5% dieser Objekte sind langlebig. Diese Objekte werden dann in einen
anderen Speicherbereich verschoben, der nicht mehr so oft gescannt wird. Hierbei gilt die
Regel, dass, je länger die Objekte existieren, desto kleiner die Scannfrequenz ist.
4.3.2. Inlining frequently-called Methods
In manchen Fällen kann es von Vorteil sein, wenn der Quellcode einer Methode nicht jedes
Mal aufgerufen werden muss. In diesen Fällen kopiert die "Inlining frequently-called Method"
den Quellcode an die entsprechende Stelle.
Christian Riekenberg
Matthias von der Grün
19 / 21
05.01.2006
Implementierung eines Java Runtime Systems
5. Zusammenfassung und Zukunftsaussichten
Der größte Vorteil von Java, seine Plattformunabhängigkeit, ist auch der größte Nachteil. Um
stabil auf unterschiedlichen Systemen laufen zu können, wird es dem Programmierer nicht
erlaubt systemnah zu agieren. Diese Abkapslung und die Realisierung von
Plattformunabhängigkeit durch Interpretation eines Codes, der von allen Systemen
verwendet werden kann, senkt die Performance bei der Ausführung von Programmen
merkbar. Algorithmen zur Speicherbereinigung müssen ebenfalls universell gehalten werden
und sind manueller Speicherverwaltung in der Laufzeit zumeist unterlegen. Anwendung mit
automatischer Speicherverwaltung sind jedoch weniger fehleranfällig und es kommt seltener
zu Runtime-Fehlern.
Auch in Zukunft werden sowohl die interpretierten und plattformunabhängigen, als auch die
kompilierten und systemabhängigen Sprachen parallel existieren. Der Wunsch nach
„Programm once, run everywhere“ kann nicht in allen Bereichen verwirklicht werden. Gerade
zeitkritische Echzeit-Anwendungen können nicht immer darauf verzichten auf
Systemressourcen zuzugreifen. Jedoch ist bei den wenigsten Programmen Echtzeit
gefordert. Die Nutzung von interpretierten Sprachen steht demnach einem großen Teil der
Anwendungen frei und wird aufgrund der überwiegenden Vorteile häufig verwendet.
Christian Riekenberg
Matthias von der Grün
20 / 21
05.01.2006
Implementierung eines Java Runtime Systems
6. Quellenangaben
6.1. Literatur- und Abbildungsverzeichnis
[CAC05] Offizielle Hompage der JIT-Implementierung CACAO: http://www.cacaojvm.org/,
Abruf November 05
[DAL05] Matthias Kalle Dalheimer: Java Virtual Machine,
O'Reilly Verlag, ISBN 3-930673-73-8
[DEV05] Developer : http://www.developer.com/lang/article.php/3390001, Abruf 17.10.2005
[DEV05] Jupitermedia Corporation: Hompage der Firma JupiterMedia.
http://www.developer.com/lang/article.php/3390001, Abruf November 05
[GAL05] Galileo Computing :
http://www.galileocomputing.de/openbook/javainsel3/javainsel_010002.htm, Abruf
12.10.2005
[JAV05] JavaWorld.com IDG company: http://www.javaworld.com/javaworld/jw-032000/images/java101_fig1.gif, Abruf November 05
[JEC05] www.jeckle.de/vorlesung/java/script.html, Abruf 15.10.2005
[KAF05] Offizielle Hompage der JIT-Implementierung KAFFE: http://www.kaffe.org/, Abruf
November 05
[LEM05] L.Lemay, C.Perkins : Java in 21 Tagen. Markt&Technik Verlag, 1999, ISBN
3827255783
[LIN05]
Linux New Media AG: Homepage der Firma Linux New Media AG,
http://www.linux-magazin.de/Artikel/ausgabe/1997/05/JVM/jvm1.html, Abruf
November 05
[LY05]
Tim Lindholm / Frank Yellin : The Java Virtual Machine Specification Second
Edition –
Java Virtual Machine – O’Reilly Verlag –1997
[PSU05] Jens Regenberg, Seminar Garbage Collection: www.ps.uni-sb.de/courses/gcws01/exams/MarkCompact.pdf, Abruf November 05
[STA05] UCLA Department of Statistics; Homepage des “Departement of statistic” der
Universität von Kalifornien, http://www.stat.ucla.edu, Abruf November 05
[SUI05] Mark-Sweep-Algorithmus: http://suif.stanford.edu, Abruf November 05
[SUN05] Sun Inc.; Homepage der Firma Sun. http://java.sun.com/, Abruf November 05
[TYA05] Download zu der JIT-Implementierung TYA, Version 1.8:
http://www.sax.de/~adlibit/, Abruf November 05
[WIK05] Wikipedia : http://www.wikipedia.de
Christian Riekenberg
Matthias von der Grün
21 / 21
05.01.2006
Herunterladen