Eine Java - VM in Python



Ähnliche Dokumente
Die Programmiersprache Java. Dr. Wolfgang Süß Thorsten Schlachter

Einführung in die Java- Programmierung

J.5 Die Java Virtual Machine

Grundlagen von Python

Objektorientierte Programmierung

Einführung in Java. PING e.v. Weiterbildung Andreas Rossbacher 24. März 2005

Vorkurs Informatik WiSe 15/16

Verhindert, dass eine Methode überschrieben wird. public final int holekontostand() {...} public final class Girokonto extends Konto {...

Übung: Verwendung von Java-Threads

ObjectBridge Java Edition

Vorkurs C++ Programmierung

Prinzipien Objektorientierter Programmierung

Einführung in Eclipse und Java

Installation und Inbetriebnahme von Microsoft Visual C Express

.NET Code schützen. Projekt.NET. Version 1.0

Java Kurs für Anfänger Einheit 5 Methoden

Vorlesung Objektorientierte Softwareentwicklung. Kapitel 0. Java-Überblick

Einführung in die Programmierung

SEP 114. Design by Contract

Einführung in die objektorientierte Programmierung mit Java. Klausur am 19. Oktober 2005

Objektorientierte Programmierung für Anfänger am Beispiel PHP

schnell und portofrei erhältlich bei beck-shop.de DIE FACHBUCHHANDLUNG mitp/bhv

1 Vom Problem zum Programm

Java Virtual Machine (JVM) Bytecode

Objektorientierte Programmierung. Kapitel 12: Interfaces

Es sollte die MS-DOS Eingabeaufforderung starten. Geben Sie nun den Befehl javac ein.

5 DATEN Variablen. Variablen können beliebige Werte zugewiesen und im Gegensatz zu

Datensicherung. Beschreibung der Datensicherung

C# im Vergleich zu Java

Der Aufruf von DM_in_Euro 1.40 sollte die Ausgabe 1.40 DM = Euro ergeben.

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren

Tutorium Informatik 1. Aufgabe 2: Formatierte Ein- und Ausgabe

4D Server v12 64-bit Version BETA VERSION

Diplomarbeit. Konzeption und Implementierung einer automatisierten Testumgebung. Thomas Wehrspann. 10. Dezember 2008

Software Engineering Klassendiagramme Assoziationen

II. Grundlagen der Programmierung. 9. Datenstrukturen. Daten zusammenfassen. In Java (Forts.): In Java:

Softwaretests in Visual Studio 2010 Ultimate Vergleich mit Java-Testwerkzeugen. Alexander Schunk Marcel Teuber Henry Trobisch

Programmierkurs Java

Programmierung für Mathematik (HS13)

Einführung in PHP. (mit Aufgaben)

4 Objektorientierte Programmierung mit Java 4.1 Java-Grundlagen

Robot Karol für Delphi

CADEMIA: Einrichtung Ihres Computers unter Windows

Qt-Projekte mit Visual Studio 2005

Anleitung zum Arbeiten mit Microsoft Visual Studio 2008 im Softwarepraktikum ET/IT

Klassenentwurf. Wie schreiben wir Klassen, die leicht zu verstehen, wartbar und wiederverwendbar sind? Objektorientierte Programmierung mit Java

Programmieren in Java

Willkommen zur Vorlesung. Objektorientierte Programmierung Vertiefung - Java

Programmieren I. Strategie zum Entwurf von Klassen. Beispiele. Design von Klassen. Dr. Klaus Höppner. Beispiel: Bibliothek

Installation einer C++ Entwicklungsumgebung unter Windows --- TDM-GCC und Eclipse installieren

Handbuch ECDL 2003 Basic Modul 5: Datenbank Grundlagen von relationalen Datenbanken

Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen

Session Beans & Servlet Integration. Ralf Gitzel ralf_gitzel@hotmail.de

M. Graefenhan Übungen zu C. Blatt 3. Musterlösung

Testen mit JUnit. Motivation

In 12 Schritten zum mobilen PC mit Paragon Drive Copy 11 und Microsoft Windows Virtual PC

Binärdarstellung von Fliesskommazahlen

Diplomvorprüfung in Datenverarbeitung EBS Sommersemester 2002

Step by Step Webserver unter Windows Server von Christian Bartl

Zahlensysteme: Oktal- und Hexadezimalsystem

Objektbasierte Entwicklung

Vorlesung Informatik II

3 Objektorientierte Konzepte in Java

Outlook. sysplus.ch outlook - mail-grundlagen Seite 1/8. Mail-Grundlagen. Posteingang

Inhalt. 1 Einleitung AUTOMATISCHE DATENSICHERUNG AUF EINEN CLOUDSPEICHER

Arbeiten mit UMLed und Delphi

Computeria Solothurn

Drei-Schichten-Architektur. Informatik B - Objektorientierte Programmierung in Java. Vorlesung 16: 3-Schichten-Architektur 1 Fachkonzept - GUI

Vererbung & Schnittstellen in C#

Java Einführung Programmcode

Kompilieren und Linken

Ordner Berechtigung vergeben Zugriffsrechte unter Windows einrichten

Er musste so eingerichtet werden, dass das D-Laufwerk auf das E-Laufwerk gespiegelt

In 15 einfachen Schritten zum mobilen PC mit Paragon Drive Copy 10 und Microsoft Windows Virtual PC

Übung 9 - Lösungsvorschlag

Java Kurs für Anfänger LMU SS09 Einheit 1 Javaumgebung

Objektorientierte Programmierung OOP

Programmierkurs Java

Windows 8 Lizenzierung in Szenarien

Kostenstellen verwalten. Tipps & Tricks

C++ Grundlagen. ++ bedeutet Erweiterung zum Ansi C Standard. Hier wird eine Funktion eingeleitet

SafeRun-Modus: Die Sichere Umgebung für die Ausführung von Programmen

Das Typsystem von Scala. L. Piepmeyer: Funktionale Programmierung - Das Typsystem von Scala

Der lokale und verteilte Fall

Upgrade auf die Standalone Editionen von Acronis Backup & Recovery 10. Technische Informationen (White Paper)

Einführung in die Java- Programmierung

Daniel Warneke Ein Vortrag im Rahmen des Proseminars Software Pioneers

Nutzung von GiS BasePac 8 im Netzwerk

Professionelle Seminare im Bereich MS-Office

Übung 8: Semaphore in Java (eigene Implementierung)

Client-Server-Beziehungen

Zeichen bei Zahlen entschlüsseln

Matrix42. Use Case - Sicherung und Rücksicherung persönlicher Einstellungen über Personal Backup. Version September

Übungen zu Einführung in die Informatik: Programmierung und Software-Entwicklung: Lösungsvorschlag

Lizenzierung von System Center 2012

Diplomarbeit Antrittsvortrag

Task: Nmap Skripte ausführen

Virtueller Seminarordner Anleitung für die Dozentinnen und Dozenten

Klausur WS 2006/07 Programmiersprache Java Objektorientierte Programmierung II 15. März 2007

Klausurteilnehmer. Wichtige Hinweise. Note: Klausur Informatik Programmierung, Seite 1 von 8 HS OWL, FB 7, Malte Wattenberg.

Transkript:

INSTITUT FÜR INFORMATIK Softwaretechnik und Programmiersprachen Universitätsstr. 1 D 40225 Düsseldorf Eine Java - VM in Python John Witulski Masterarbeit Beginn der Arbeit: 18. Mai 2009 Abgabe der Arbeit: 18. November 2009 Gutachter: Prof. Dr. Michael Leuschel Prof. Dr. Martin Mauve

Erklärung Hiermit versichere ich, dass ich diese Masterarbeit selbstständig verfasst habe. Ich habe dazu keine anderen als die angegebenen Quellen und Hilfsmittel verwendet. Düsseldorf, den 18. November 2009 John Witulski

Zusammenfassung In dieser Arbeit wurde eine Java Virtual Machine als Teil des PyPy Projekts in Python implementiert. Diese umfasst einen Classparser, Classloader und Bytecodeinterpreter. Um den vollen Umfang der Sprache Java zu unterstützen wurde die Java-VM mit Hilfe der freien Klassenbibliothek GNU Classpath sowie der Implementierung des Java-Native- Interfaces erweitert. Bei der Entwicklung der Java-VM wurde testgetrieben vorgegangen. Es wurden dabei fast alle Java-Bytecodes und JNI Funktionen implementiert sowie ein kleiner Teil der Java-Klassenbibliothek zugänglich gemacht. Die Performancetests ergaben das die hier implementierte Java-VM im Vergleich zur Java-VM von Sun deutlich langsamer ist.

INHALTSVERZEICHNIS 1 Inhaltsverzeichnis Inhaltsverzeichnis 1 1 Einleitung 3 1.1 Die Programmiersprache Python........................ 4 1.2 Das PyPy Projekt................................. 5 1.3 Test-driven Development............................ 6 1.4 Die Programmiersprache Java.......................... 8 1.5 Die Java-Virtual-Machine............................ 9 1.6 Andere Java -VMs................................. 10 1.6.1 Apache Harmony............................. 10 1.6.2 Jikes RVM................................. 11 1.6.3 Kaffe.................................... 11 1.6.4 OpenJDK................................. 11 1.7 Gnu Classpath................................... 12 2 Funktionsweise einer Java-VM 13 2.1 Vorbereitungen zum Programmablauf einer Java-VM............ 13 2.2 Aufbau einer Java-VM.............................. 14 2.3 Java-Bytecode................................... 15 2.4 Aufbau einer Java-Classdatei.......................... 19 2.5 Unterstützung von nativen Code........................ 20 3 Implementierung einer Java-VM in Python 22 3.1 Parsen und Laden von Java-Classdateien................... 23 3.2 Repräsentation von primitiven Datentypen und Objekten.......... 24 3.3 Der Bytecodeinterpreter............................. 25 4 Erweiterung der Java-VM 27 4.1 Implementierung der GNU Classpaths Hooks................ 27 4.2 Anpassung der Java-VM an RPython...................... 29 4.3 Implementierung von JNI............................ 31 5 Ergebnisse 33

INHALTSVERZEICHNIS 2 5.1 Performance.................................... 34 5.1.1 Benchmark Durchführung........................ 34 5.1.2 Benchmark Ergebnisse und Auswertung............... 34 5.2 Funktionalität................................... 36 6 Fazit 36 6.1 Ausblick...................................... 37 6.1.1 Vollständige Einbindung von GNU Classpath............ 37 6.1.2 Übersetzung mit Hilfe von PyPy.................... 37 6.1.3 Unterstützung von Threading..................... 37 6.1.4 Optimierung der Performance..................... 37 6.1.5 Verifikation von Bytecode........................ 38 6.2 Schlusswort.................................... 38 Literatur 39 Abbildungsverzeichnis 41

1 EINLEITUNG 3 1 Einleitung Hat eine Programmierer heutzutage den Wunsch ein Computerprogramm zu erweitern oder von Grund auf neu zu entwickelen, so steht dieser einer Vielzahl von Problemen gegenüber. Eines davon sind die vielen unterschiedlichen Arten von Computersystemen und Programmiersprachen die im Laufe der Zeit entstanden sind. Dabei muss mit Computersystemen nicht unbedingt ein Desktop-PC gemeint sein. Sondern es kann sich um ein beliebiges System, auf dem Software ausgeführt wird handeln. Will man nun für ein neues System Software entwickeln so will man auch nicht unbedingt das Rad neu erfinden sondern möglichst auf bereits existierende Software zurückgreifen und diese so wenig wie möglich anpassen. Da jedoch jedes System anders (z.b. was die Hardware, den Instruktionssatz des Prozessors oder das Betriebssystem angeht) ist, ist dies oft nicht möglich. Eine Strategie diesem Problem entgegenzuwirken sind die sogenannten virtuellen Maschinen (VMs). Programmcode wird mit diesem Ansatz nicht direkt auf die Hardware des Endsystems zugeschnitten entwickelt, sondern für eine Zwischensprache, den Bytecode, geschrieben. Dieser kann dann von jeder virtuellen Maschine ausgeführt werden. Bei der virtuellen Maschine handelt es sich um ein Stück Software das auf jeden Endsystem zur Verfügung steht und den besagten Bytecode ausführen kann. Dies ermöglicht eine plattformunabhängige Entwicklung von Software, da Programmcode nur einmal geschrieben werden muss und dann überall dort ausgeführt werden kann, wo eine virtuelle Machine zur Verfügung steht. Jedoch wäre es wünschenswert hier noch einen Schritt weiter zu gehen. Denn es ist immer noch notwendig für jedes Endsystem eine virtuelle Maschine zu entwickeln. Müssen einmal Änderungen oder Erweiterungen an dieser virtuellen Maschine vorgenommen werden so ist dies ein großer Arbeitsaufwand. Besser wäre es eine Möglichkeit zu entwickeln bei der die virtuelle Maschine nur ein einziges mal implementiert werden muss und dann automatisch für alle Endsysteme erzeugt werden kann. Auch wäre es eine Vision wenn man diesem Übersetzungsvorgang einfach neue Zielsysteme hinzufügen könnte. So wäre es möglich für einmal geschriebene virtuelle Maschinen neue Endsysteme zu unterstützen ohne den Quellcode dieser VMs überhaupt betrachten zu müssen. Diese Arbeit beschäftigt sich mit der Implementierung einer virtuellen Maschine für die Sprache Java als Teil des PyPy Projektes, was genau oben genannte Ziele verfolgt und zum Teil realisiert. Im weiteren Text werden die Grundbegriffe dieser Thematik geklärt und dann auf die Funktionsweise der VM sowie deren konkrete Implementierung eingegangen.

1 EINLEITUNG 4 1.1 Die Programmiersprache Python Python[21] ist eine kostenlose, objektorientierte, dynamische Programmiersprache, entwickelt von Guido van Rossum. Python unterscheidet sich von anderen objektorientierten Sprachen wie Java vor allem durch seine dynamischen Eigenschaften. Ein Aspekt einer dynamischen Programmiersprache ist die dynamische Typisierung zur Laufzeit. Der Typ einer Variable oder eines Objekts ist also nicht statisch vor der Ausführung des Programms festgelegt (so wie es z.b. im Falle der Sprachen C oder Java ist), sondern wird erst zur Laufzeit bestimmt. Das Binden eines Objektes an einen Typ findet so spät wie nötig statt. Wenn eine Variable oder ein Objekt bestimmte Eigenschaften erfüllen (z.b. das man eine Zahl zu ihm addieren kann oder es eine Länge besitzt) wird mit diesem weitergearbeitet unabhängig davon welchen Typ es hat. Diese Art der Typisierung nennt man auch Ducktyping (if it walks like a duck and quaks like a duck - it is a duck). Eine andere dynamische Eigenschaft von Python ist die Fähigkeit zur Laufzeit neuen Programmcode (z.b. in Form von Methoden oder Funktionen) zu erzeugen und auszuführen. Dies bezeichnet man als Metaprogrammierung. Der Vorteil bei der Entwicklung von Programmen mit Hilfe von Metaprogrammierung und dynamischer Typisierung kann darin liegen das man schnell zu einem Ergebnis kommt und das sehr ähnliche Aufgaben mit einem allgemeinen Fall gelöst werden können, statt mit ähnlichen aber in Detail unterschiedlichen Lösungen. Konkret ist es also unnötig, die selben Methoden mehrmals zu schreiben, nur weil deren Typen oder einige wenige Programmzeilen unterschiedlich sind. Richtig angewendet kann eine dynamische Programmiersprache nicht nur die Entwicklungszeit sondern auch die Wartbarkeit eines Programms sehr verbessern. Nachteil kann es sein, dass ohne Typisierung, vor allem bei fremden Programmcode, unklar ist welche Art von Variablen erwartet wird oder es könnte schwierig sein Methoden nachzuvollziehen die ausgeführt werden, weil diese nirgendwo niedergeschrieben sind sondern automatisch erzeugt wurden. Es gibt jedoch auch Gemeinsamkeiten zwischen dynamischen Sprachen wie Python und Sprachen wie Java. Gemeinsam haben Python und Java z.b. die Fehlerbehandlung mit Hilfe von Exceptions oder die Plattformunabhängigkeit durch ihre Übersetzung in Bytecode. Eine andere wichtige Gemeinsamkeit ist die umfangreiche Klassenbibliothek (bzw. Modulsammlung). Es gibt für alle häufig anfallenden Probleme bereits mitgelieferte Lösungen. Beispiele sind die Einbindung von Programmcode anderer Sprachen oder mathematischen Funktionen. Auch dies kann die Entwicklung von Programmen positiv beeinflussen. Man spricht hier im Fall von Python auch von batteries included Pythons Anwendungsgebiete sind vielseitig. Unter anderem als Scriptsprache z.b. in Webapplikationen. Python steht heute bei Version 3.x und ist standardmäßig in den meisten Linuxdistributionen enthalten. Die hier implementierte Java-

1 EINLEITUNG 5 Listing 1: Dynamische Erzeugung einer Methode 1 def JNI_NewXArray ( default_item ) : 2 def array_method ( _JNIEnv, _ j s i z e ) : 3 a s s e r t i s i n s t a n c e ( _ j s i z e, i n t ) 4 return Arrayref ( [ default_item ] _ j s i z e, default_item ) 5 return array_method VM wurde mit Hilfe von RPython (einer Teilmenge von Python) geschrieben. Die Implementierung in RPython ist für die Interaktion mit dem PyPy Projekts notwendig. Listing 1. zeigt ein Beispiel für Metaprogrammierung und dynamische Typisierung. Ziel der Funktion ist es ein Array einer bestimmten Länge und eines bestimmten primitiven Typs zurückzugeben. Die Einrückungen sind in Python Teil der Syntax und legen fest welche Blöcke zusammengehören. Hierbei ist der eingerückte Teil jeweils der Rumpf der Funktion. Die Funktion JNI_NewXArray erzeugt eine Funktion und hat diese auch als ihren Rückgabewert. Die Funktion array_method kann hierbei die Argumente von JNI_NewXArray nutzen und führt vor der Erzeugung des Arrayobjekts in der vorletzten Zeile noch eine Prüfung ihrer Parameter durch. Diese Python Funktion wird von einer C Funktion aufgerufen (siehe Implementierung von JNI) welche eine festgelegte Signatur hat. Deshalb steht unter den Argumenten ein weiterer Parameter (_JNIEnv) der jedoch in diesem Fall nicht benötigt wird. Die Funktion JNI_NewXArray liefert je nach dem Typ von default_item eine andere Funktion zurück. Die Alternative zu dieser Funktionen wären eine Reihe von anderen Funktionen je für die Typen boolean, int, short usw. gewesen. Dies hätte nicht nur mehr Aufwand und einer größeren Fehleranfälligkeit als in dieser Lösung zur Folge gehabt sondern evtl. auch eine größere Wartungsarbeit wenn eine Änderung nötig würde z.b. eine Andere Parameteranzahl des Objekt Arrayref. 1.2 Das PyPy Projekt Die hier entwickelte Java-VM ist Teil des PyPy-Projekts[15][18]. Bei dem PyPy- Projekt handelt es sich einerseits um eine Pythonimplementierung in Python (RPython) sowie andererseits um ein Übersetzungswerkzeug zur automatischen Interpretergenerierung. Einen Python Interpreter in Python (RPython) zu schreiben hatte vor allem den Vorteil das Pythonprogrammierer im Stande waren diese Implementierung gut zu verstehen und zu verbessern. Auch ist eine Implementierung in einer Hochsprache besser für Forschungszwecke an virtuellen Maschinen geeignet. Durch Forschung z.b. im Falle der Just-in-Time-Compilierung wurde hier auch die Geschwindigkeit von PyPy erhöht. Jedoch war diese Implementierung von Python bis auf wenige Ausnahmen wie zu erwarten um einiges langsamer als die C Im-

1 EINLEITUNG 6 plementierung CPython. Genau genommen wurde dieser Interpreter in einer echten Teilmenge von Python, RPython[5] (Restricted Python) geschrieben. RPython ist weniger dynamisch als das eigentliche Python, was z.b. zur Folge hat das Metaprogrammierung nur zur Importzeit und nicht zur Laufzeit möglich sind und das dynamische Typisierung nicht uneingeschränkt möglich ist. Der interessantere Teil des PyPy Projekts ist jedoch die Möglichkeit mit Hilfe von diesem automatisch eine virtuelle Maschine bzw. einen Interpreter zu generieren. Es ist also möglich einen Interpreter in RPython zu implementieren und diesen mit Hilfe von PyPy für eine andere Zielplattform zu erzeugen. Bei diesen Vorgang kann sogar das Threading-Model und die Art des Garbage-Collectors gewählt werden. Auch besteht die Möglichkeit einer Performanceerhöhung durch Just-in-Time-Compilierung. Es kann also aus einem Interpreter der in RPython geschrieben wurde, ein Java-,.Net- oder C-Interpreter für verschiedene Plattformen erzeugt werden ohne das für den Programmierer zusätzliche Arbeit entsteht. Die Vision ist es das virtuelle Maschinen für Sprachen nur einmal geschrieben werden müssen und dann für eine große Anzahl von Zielplattformen ohne zusätzliche Arbeit automatisch erzeugt werden können. Insbesondere wenn Änderungen an diesem VMs vorzunehmen sind, z.b. weil sich eine Sprachspezifikation oder der Umfang einer Sprache ändert, ist mit diesem Ansatz der Arbeitsaufwand sehr viel geringer als bei der Anpassung vieler unterschiedlicher Implementierungen auf unterschiedlichen Systemen. Die hier entwickelte Java-VM hat in diesem Zusammenhang die Rolle des Eingabeinterpreters, der in verschiedene Sprachen auf verschiedenen Plattformen übersetzt werden soll. Dieses Ziel ist aufgrund der Unterstützung der umfangreichen Java-Klassenbibliothek, d.h die Einbindung von GNU Classpath jedoch nur eingeschränkt möglich, da GNU Classpath zwar für viele aber nicht für beliebige Plattformen zur Verfügung steht. 1.3 Test-driven Development Testgetriebene Entwicklung[9] (engl. Test-driven Development) ist eine agile Methode der Softwareentwicklung, deren Hauptziel es ist Fehler in Software zu finden und zu vermeiden. Bei der Entwicklung der hier implementierten Java-VM wurde testgetrieben vorgegangen. Bei einem Test handelt es sich um ein Programm das ein anderes Programm ausführt und dabei bestimmte Erwartungen des Programmierers, d.h. Bedingungen oder Ergebnisse, auf ihre Richtigkeit überprüft. Dieser Vorgang läuft in der Regel mit einer Vielzahl von Tests automatisiert ab. Dabei werden Test nicht nach der Entwicklung des Programms geschrieben sondern es wird nach einem anderen Muster vorgegangen:

1 EINLEITUNG 7 Listing 2: Castingtest 1 def t e s t _ d o u b l e _ c a s t ( s e l f ) : 2 c l s = s e l f. g e t c l a s s ( 3 c l a s s DoubleCast { 4 public s t a t i c void main ( S t r i n g [ ] bla ) 5 { 6 double d = 4 0. 0 ; 7 i n t i = ( i n t ) d ; 8 long l = ( long ) d ; 9 f l o a t f = ( f l o a t ) d ; 10 System. out. p r i n t l n ( i +1) ; 11 System. out. p r i n t l n ( l +1) ; 12 System. out. p r i n t l n ( f +1.0 f ) ; 13 System. out. p r i n t l n ( d + 1. 0 ) ; 14 } 15 } 16 ) 17 s e l f. run ( c l s, [ ], " 41\n41\n41.0\ n41.0\n" ) Ausgangssituation ist ein zu erweiterndes Programm sowie (möglicherweise) eine Menge von Tests die alle erfolgreich durchlaufen werden. Als Erstes wird ein Test geschrieben, der eine neue Funktion des Programms testen soll. Dieser Test wird vor der Erstellung des zu testenden Programmcodes geschrieben und sollte fehlschlagen. Danach wird die eigentliche Funktion in das Programm eingebaut und genau dann die Weiterentwicklung der Programmfunktionalität unterbrochen wenn dieser Test sowie alle bereits existierenden Tests erfolgreich durchlaufen werden. Zuletzt findet ein Refactoring (Umgestaltung) des Codes statt und der Zyklus wird mit dem Schreiben eines neuen Tests von vorn begonnen, wobei jeder Durchlauf des Zykluses möglichst kurz sein sollte. Mit dem Schreiben des Tests vor dem Schreiben der eigentlichen Funktionalität sowie dessen Fehlschlagen wird sichergestellt, dass die Funktion wirklich neu und noch nicht Teil des Programms ist. Auch zwingt dieses Vorgehen den Programmierer über das Verhalten des zu implementierenden Codes nachzudenken. Die Tests sind so im besten Fall eine lauffähige Spezifikation des Programms und das versehentliche vorbei testen eines Programmierers weil er die interne Arbeitsweise seines Codes kennt, wird dadurch unwahrscheinlich. In der Implementierungsphase der eigentlichen Funktion wird auch nur diese geschrieben, da sonst die Gefahr besteht Programmcode in das Programm einzufügen, der von keinem Test abgedeckt wird. Auch ist es notwendig, dass nach dem Hinzufügen der neuen Funktionalität alle bereits existierenden Tests erfolgreich durchlaufen werden, um sicher zu gehen dass durch den neuen Programmcode keine Nebeneffekte entstanden sind, die andere bereits existierende Programmfunktionalität beschädigt haben. Listing 2 zeigt einen Test für die Castingbytecodes (siehe Abschnitt 2.3) der Java- VM. Dieser Test besteht aus zwei Methodenaufrufen. Die Methode getclass

1 EINLEITUNG 8 benutzt Suns Java-Compiler um aus dem Java-Quelltext eine Classdatei zu erzeugen und liefert anschließend mit Hilfe des Classloaders (siehe Abschnitt 3.1) ein Objekt zurück was eine geparste Java-Classdatei repräsentiert. An die Methode run werden dann die Argumente des Programms sowie das zu erwartende Ergebnis übergeben. Dieses wird innerhalb von run gegen die originale Java- VM getestet (um sicherzustellen, dass keine falschen Ergebnisse erwartet werden) und anschließend gegen die hier implementierte Java-VM getestet. Liefert diese das erwartete Ergebnis so ist der Test erfolgreich. Testgetriebene Entwicklung kann die Qualität zu entwickelnder Software verbessern und sogar eine Hilfe für den Programmierer beim Erstellen des Programmverhaltens sein. Jedoch birgt diese Vorgehensweise nicht nur mehr Overhead sondern auch die Gefahr einer trügerischen Sicherheit. Nur weil ein Programm viele Tests durchläuft muss es nicht fehlerfrei sein. Und die Entwicklung guter Tests erfordert Erfahrung. 1.4 Die Programmiersprache Java Java[12] (ursprünglich Oak) ist eine objektorientierte Programmiersprache (sowie eine Laufzeitumgebung) der Firma Sun, die seit ihrer Entstehung 1995 an Beliebtheit gewonnen hat. Java ist ein plattformunabhängige Sprache die mit dem Ziel entwickelt wurde, dass ein Programm einmal geschrieben auf einer Vielzahl von Systemen (wie es z.b im Internet der Fall ist) läuft, ohne das hardwarespezifische Anpassungen des Quellcodes nötig werden. Daher auch das Java-Motto: write once, run anywhere. Um dieses Ziel zu erreichen werden alle Java-Programme in eine assemblerartige Zwischensprache, den Bytecode übersetzt. Dieser Bytecode wird dann auf den Endsystemen auf denen die Java-Applikation laufen soll mit Hilfe einer virtuellen Maschine ausgeführt. Auch wenn man sich eine virtuelle Maschine als einen virtuellen Rechner vorstellen kann, der auf einem System emuliert wird, genügt auch die Vorstellung der VM als Bytecodeinterpreter mit Zusatzfunktionen wie automatischer Freispeicherbereinigung (Garbage Collection) und Sicherheitsprüfung von Programmcode (Sandbox) Neben der Plattformunabhängigkeit sind weitere Vorteile der Sprache Java die Unterstützung von Nebenläufigkeit des Programmcodes, die Robustheit von Java im Vergleich zu anderen Sprachen wie C/C++ (keine Zeigerarithmetik) sowie die umfangreiche Klassenbibliothek. Vor allem diese Klassenbibliothek ist im Laufe der Zeit stark angewachsen. So gibt es heute vorgefertigten Programmcode zu fast allen Standardproblemstellungen, wie grafischen Oberflächen, verteilten Anwendungen, Algorithmen und Datenstrukturen, Verschlüsselung und vielem mehr. Ein Zurückgreifen auf andere Bibliotheken wie es z.b bei C/C++ der Fall ist kommt also deutlich seltener vor.

1 EINLEITUNG 9 Java Quellcode Java Compiler Java Bytecode Java VM Java VM Java VM Java VM Linux Windows Solaris Mac Abbildung 1: Funktionsprinzip von Java-VMs (Vereinfachung) Java hat seit seiner Entstehung bis heute stark an Verbreitung gewonnen. Laut Umfragen gehört Java zu den Top Ten [6] Programmiersprachen im Einsatz. Nach Angaben von Sun steckt Java in schätzungsweise 4.5 Milliarden Geräten. Darunter nicht nur normale Heimcomputer sondern auch Mobiltelefone, Smart Cards, medizinische Geräte und vielem mehr. Nach Angaben vom Sun nutzen 6.5 Millionen Softwareentwickler Java.[4] Die Java-Laufzeitumgebung gibt es in drei verschiedenen Varianten[7]: Der Standard Edition (Java SE), um die es im folgenden gehen wird. Der erweiterten Enterprise Edition (Java EE) sowie der Java-Micro-Edition (Java ME) bei der es sich um eine Teilmenge der Standardedition handelt, welche z.b. für mobile Endgeräte genutzt wird. Um auch im Bereich Open-Source und Linuxdistributionen eine wichtige Rolle zu spielen wurden große Teile der Java-Quellen 2006/2007 unter dem Begriff OpenJDK offen gelegt. 1.5 Die Java-Virtual-Machine Um ein in der Programmiersprache Java geschriebenes Computerprogramm auf einem Computer ausführen zu können, werden dessen Quellcodedateien (*.java) mit einem Javacompiler[8] (z.b. javac) in Classdateien (*.class) übersetzt um dann von einer Java-Virtual-Machine (Java-VM) die auf diesem System vorliegen muss ausgeführt zu werden. Eine Classdatei ist eine für eine Java-VM verständ-

1 EINLEITUNG 10 liche Repräsentation einer Java-Klasse und enthält neben Informationen über die Klasse vor allem Java-Bytecode. Eine Java-VM kann man sich als Schicht zwischen Betriebssystem und Bytecodeebene vorstellen. Sie abstrahiert die Architektur und Besonderheiten des darunter liegenden Systems insbesondere seiner Hardware (siehe Abbildung 1). Eine andere Möglichkeit ist es, sich eine Java-VM als virtuellen Rechner vorzustellen. Dieser Rechner wäre eine Stackmaschine mit einem Register, dem Instructionpointer oder Programcounter (pro Thread). Dabei würde jede Instruktion der Instruktionsmenge (Menge alle Java-Bytecodes) auf diesem Stack (Stapelspeicher) durchgeführt. Wobei jedoch zu beachten wäre, dass diese Instruktionen sowie deren Operanden stark typisiert sind. Auf das Thema Bytecodes, Classdateien und Classloading wird in den Abschnitten 2.3. 2.4 und 3.1 noch einmal detaillierter eingegangen. Der Vorteil dieses VM Ansatzes ist die Plattformunabhängigkeit und Sicherheit. Mit Sicherheit ist hier gemeint, dass es schwieriger für ein fehlerhaftes Programm ist, ein Gesamtsystem zu schädigen, da es durch die Java-VM vom System abgeschirmt wird. Der Nachteil sind der größere Speicherverbrauch und die schlechtere Performance, die jedoch durch Just in Time Compiling sowie dynamische Optimierung (z.b im Falle von Suns Hotspot Java-VM) wieder ausgeglichen werden können. Außerdem muss ein Nutzer über eine Java-VM für sein System verfügen um Java- Programme auszuführen. 1.6 Andere Java -VMs.Es gibt eine Reihe anderer Java-VMs in verschiedenen Programmiersprachen, die unterschiedlich leistungsfähig und vollständig vor allem im Bezug auf die Unterstützung der Java-Klassenbibliothek sind. Dieser Abschnitt soll einen Überblick über vergleichbare Arbeiten geben. 1.6.1 Apache Harmony Apache Harmony[13] ist eine Open-Source Java-VM die unter der Apache Lizenz veröffentlicht wurde. Ziel des Projekts war ein eine Community-übergreifende Implementierung von der Java SE 5 zu schaffen. Dabei wurde aufgrund von Lizenzbedenken nicht GNU Classpath als Bibliothek benutzt, sondern auf das Re- Implementieren sowie Quelltextspenden gesetzt um die Java-VM mit der Standardklassenbibliothek auszustatten, was auch gelang. Diese Klassenbibliothek deckte heute über 90% von Java SE 5 ab und wird von mehreren Java-VMs genutzt.

1 EINLEITUNG 11 1.6.2 Jikes RVM JikesRVM (Jikes Research Virtual Machine)[16] ist eine Java-VM die in der Sprache Java selbst implementiert wurde. Ursprünglich als IBM Forschungsprojekt 1997 gestartet, wurde Jikes RVM 2001 als Open-Soure veröffentlicht. JikesRVM unterstützt Java SE 5 insbesondere Garbage Collection und Threading. Als Klassenbibliothek kann die Apache Klassenbibliothek sowie GNU Classpath genutzt werden. JikesRVM war in der Vergangenheit oft Thema von Publikationen und Dissertationen und ist nicht zu verwechseln mit dem Javacompiler Jikes. Die Motivation eine Java-VM in Java zu schreiben sind die Möglichkeit die Vorteile der Sprache Java zu nutzen, die Wiederverwendbarkeit von Java-Tools sowie die Vereinfachung der Forschung an virtuellen Maschinen. JikesRVM unterstützt eine Vielzahl von Plattformen, also Betriebssystemen und Hardware, darunter IA 32, (IA 64) und PowerPC sowie alle Betriebssysteme die GNU Classpath unterstützen wie Windows, Linux, OSX und Solaris. Obwohl JikesRVM eine Java SE 5 kompatible Java-VM benötigt um ein Bootimage zum Start zu erzeugen läuft die Java-VM einmal gestartet unabhängig von anderen Java-VMs. Da JikesRVM Techniken wie dynamische Compilierung (z.b. JIT Compilierung) und adaptive Optimierung nutzt bleibt hier der Performanceverlust um Faktoren wie hundert oder tausend aus. 1.6.3 Kaffe Kaffe (schwedisch: Kaffee)[3] ist eine freie Implementierung einer Java-VM. Die Arbeiten an Kaffe wurden im Januar 1996 von Tim Wilkinson begonnen und dauern bis heute an. Motivation des Projektes war unter anderem die Tatsache, dass nicht freie Java-VMs nicht so portabel sind. Es wurde auch versucht aufgrund der geringen Größe der VM mit Kaffe im Bereich der eingebetteten Systeme Geld zu verdienen was 2002 endgültig scheiterte. Nach einer Wiederbelebung des Projekts 2002 und der Entscheidung für die Nutzung von GNU Classpath erweiterten sich die Möglichkeiten von Kaffe. Obwohl Kaffe nicht zu 100% Javakompatibel ist unterstützt Kaffe nach eigenen Angaben über 50 Plattformen und große Teile der Standard Java-Klassenbibliothek. Kaffe ist unter der GPL erhältlich für Ubuntu-, Debian-, und Fedora Linuxdistributonen. 1.6.4 OpenJDK OpenJDK[1] ist eine offene Java-VM der Firma Sun und die aktuell genutzte Java- Version in Linuxdistributionen wie Ubuntu oder Fedora. Dabei bezeichnet die Abkürzung JDK das Java-Development-Kit, also Werkzeuge und Bibliotheken die zur Java-Programmentwicklung nötig sind. Anfangs war Java nicht frei. Jedes freie Java-Programm war abhängig von der un-

1 EINLEITUNG 12 freien Laufzeitumgebung und ihren Klassenbibliotheken. Dies wurde von Seiten der Open-Source-Gemeinde wie z.b. Richard Stallman als Java-Falle[17] kritisiert und führte zur Entwicklung von Java-Alternativen wie den GNU Java-Compiler oder GNU Classpath. Als diese Alternativen mit der Zeit immer besser wurden setzte man im Bereich freier Software zunehmend nicht mehr auf Suns Java. Den Anfang nahm das OpenJDK mit Suns Offenlegung der Java-SE Quellen unter der GPLv2 Lizenz im Jahre 2006. Sun selbst bezeichnet dies als größten Betrag der Open-Source Geschichte. Da jedoch die Rechte an einigen wenigen Teilen der Klassenbiblothek bei Dritten, die ihren Code nicht freigeben wollten und nicht bei Sun selbst lagen, mussten diese von der Open-Source Gemeinschaft durch einige Alternativimplementierungen ersetzt werden, was zum Entstehen des Projekts IcedTea führte. Um weiterhin sicher zu stellen, das Java-Versionen dem Java-Motto Write Once, Run Anywhere folgten, d.h. zueinander nicht inkompatibel würden, stellte Sun das Technology Compatibility Kit TCK zur Verfügung. Dabei handelte es sich um eine Reihe von Kompatibilitätstests. Jede Implementierung die diese Tests erfolgreich durchläuft kann sich als Java-kompatibel bezeichnen. Seit 2008 existieren für einige Linuxdistributionen nun 100% kompatible Java-Implementierungen. 1.7 Gnu Classpath Anfangs war Java zwar eine für Anwender und Programmierer kostenlos nutzbare, jedoch keine quelloffene Sprache. Vor allem im Open-Source Bereich entstand relativ schnell der Wunsch nach freien Java-VMs. Auch wenn man im Besitz einer Java-VM-Spezifikation war, so war das Ziel einer offenen Java-VM mit der Implementierung eines Bytecodeinterpreters und Classloaders allein nicht erreichbar. Es war ebenfalls notwendig, die umfangreiche Java-Klassenbibliothek zur Verfügung zu stellen, da ohne diese so gut wie keine Java-Anwendung lauffähig wäre. Also setzte man sich zum Ziel, eine offene Implementierung der Java- Klassenbibliothek unter der GPL zu veröffentlichen. Dieses Projekt war GNU Classpath[11]. GNU Classpath ist also eine freie Implementierung der Java- Klassenbibliothek, die bewusst dem Zweck dienen soll, in andere Java-VM Implementierungen integriert zu werden. Dabei sind große Teile von GNU Classpath in Java implementiert und müssen nur mit einer Java-VM geladen und ausgeführt werden. Für eine VM-Implementierer gibt es zwei wichtige Aufgaben zu erledigen, bevor die Java-VM GNU Classpath nutzen kann. Die eine ist die Implementierung von Programmierschnittstellen, so genannten Hooks zwischen Java-VM und GNU Classpath. Die andere ist die Implementierung des JNI Interfaces, das zur Nutzung von nativem Code von Java aus notwendig ist. Auf beide wird später noch im Detail eingegangen (siehe Abschnitt 4.1 und 4.3). GNU Classpath ist Teil einer Vielzahl freier Java-VMs und auch für die hier entwickelte Java-VM wurde es als Klassenbibliothek genutzt.

2 FUNKTIONSWEISE EINER JAVA-VM 13 2 Funktionsweise einer Java-VM Dieser Abschnitt gibt einen Einblick in die Funktionsweise einer Java-VM. Dabei wird auf deren Aufbau sowie Startverhalten eingegangen. Auch wird auf das Java-Classfile-Format, den Java-Bytecode sowie die Ausführung von nativen Code eingegangen. Im darauf folgenden Abschnitt wird dann die konkrete Implementierung vorgestellt. 2.1 Vorbereitungen zum Programmablauf einer Java-VM Bei der Ausführung eines Java-Programms hat eine Java-VM verschiedene Arbeitsschritte durchzuführen. Diese Schritte sind das Laden, Linken und Initialisieren der beteiligten Klassen[19]. Der Ablauf dieser Arbeitsschritte soll im folgenden beschrieben werden. Zur Ausführung wird eine binäre Repräsentation aller Java-Klassen benötigt. Diese aus einer Java-Quellcodedatei zu erzeugen ist nicht die Aufgabe der Java- VM sondern eines Java-Compilers. Eine solche Repräsentation kann auch aus einer anderen Sprache als Java erzeugt wurden sein. Diese zu laden ist Aufgabe des ClassLoaders. Java-Klassen liegen hierbei in einem binären Format, dem Java-Classfile-Format vor (was später beschrieben wird). Der Classloader ist ein Teil der Java-VM Software und wurde hier implementiert. Nachdem der Ladeprozess erfolgreich war findet das Linking statt. Linking meint hier die Zuordnung zu einem Typ (einer Klasse oder einem Interface). Hierbei kann auch eine Verifikation des Codes auf Wohlgeformtheit und interne Vorbereitungen innerhalb der VM sowie ein Überprüfung von Referenzen innerhalb der geladenen Klasse (oder Interface) stattfinden, was die hier implementierte Java-VM nicht tut. Ob Linking Ahead of Time oder zur Laufzeit durchgeführt wird ist, hier der Java VM überlassen. Im Falle der hier implementierten Java-VM werden Referenzen von Klassen, Feldern und Methoden erst zur Laufzeit aufgelöst. Insbesondere Klassen werden erst geladen, wenn diese im Programm auch benötigt werden. Der nächste Schritt nach dem Linken ist die Initialisierung. Hierbei werden alle statischen Variablen der Klasse und ihrer Basisklassen gesetzt sowie der optionale statische Codeblock ausgeführt (falls vorhanden). Ein Konstruktor der Klasse wurde jedoch noch nicht unbedingt aufgerufen. Erst nachdem die drei Schritte Laden, Linken und Initialisieren abgeschlossen sind, kann die Java-VM ihre Arbeit aufnehmen d.h. Java-Bytecode ausführen. Ein statischer Codeblock (der Bytecode enthält) bildet hier also die Ausnahme.

2 FUNKTIONSWEISE EINER JAVA-VM 14 2.2 Aufbau einer Java-VM Folgender Abschnitt soll einen Überblick geben wie eine Java-VM (nach der Sun Spezifikation) aufgebaut sein muss. Eine Java-VM besteht hierbei aus einem Bytecode Interpreter der ein Register (ein Programcounter pro Thread), einige Stacks, einem Heap und einige Codeblöcke verwaltet. Die Java-VM erzeugt und verwaltet zu ihrer Laufzeit mehrere Datenbereiche. Einige davon existieren nur einmal pro VM, andere je einmal pro Thread. Es existiert pro Java-VM nur ein Heap und jeder Code Block einer Methode ist ebenfalls nur einmal vorhanden. Jeder Thread verwaltet aber seinen eigenen Stack und seine eigenen Register. Jeder Thread verwaltet ein PC (Programcounter) Register sowie genau einen Stack von Frames. Das PC Register zeigt auf die nächste zu interpretierende Instruktion. Jeder Thread führt immer genau eine Methode zur gleichen Zeit aus. Bei der Methodenausführung wird das PC Register immer um eins erhöht um die nächste Anweisung auszuführen oder konkret durch einen Sprungbefehl gesetzt. Handelt es sich bei der auszuführenden Methode um keine native Methode enthält dieses Register also eine Adresse zu einer Instruktion. Der Stapelspeicher oder Stack jedes Threads besteht aus einzelnen Frames, welche Blöcke im Speicher sind. Diese enthalten Übergabeparameter, lokale Variablen und Zwischenergebnisse. Der Stack muss nicht als Ganzes zusammenhängend im Speicher liegen. Für jeden Methoden-Aufruf wird eine neues Frame erzeugt welches dann das aktuelle Frame (current Frame) ist. Wird der Methodenaufruf beendet (z.b. um ein Ergebnis zurückzugeben) wird dessen Frame als aus dem Speicher entfernt angesehen und das darunterliegende Frame wird zum aktuellen Frame. Jedes Frame enthält ein Array von lokalen Variablen sowie einen Operandenstack. Das Array lokaler Variablen enthält bei einem Methodenaufruf dessen Parameter, der Operandenstack ist der Stapelspeicher auf dem die Methode arbeiten kann. Da die verschiedenen Variablentypen jedoch unterschiedlich viel Speicher verbrauchen, belegen die Typen long und double zwei Arrayplätze auf dem Operandenstack. Außerdem ist der erste Eintrag des Arrays lokaler Variablen eine Referenz auf das Objekt welches die Methode aufgerufen hatte, außer es handelt sich um eine statische Methode. Es handelt sich also um die this Referenz. Der Stack von Frames pro Thread und der Inhalt von Frames d.h. das Array von lokalen Variablen und der Operandenstack sind hier also etwas unterschiedliches was es zu unterscheiden gilt. Der Heap ist ein dynamischer Speicher, den sich alle Threads teilen. Hier kann zur Laufzeit Speicher (z.b. für Objekte) reserviert werden. Dieser Speicher wird jedoch nicht explizit durch den Programmierer freigegeben, sondern durch automatische Freispeicherbereinigung (Garbage Collection). Der Heap muss wie der Stack nicht zwingend zusammenhängend im Speicher liegen. Der Code der Methoden wird in der Method Area verwaltet. Pro Klasse wird

2 FUNKTIONSWEISE EINER JAVA-VM 15 hier der Code der Methoden sowie der Inhalt von Konstanten und Feldern gespeichert. Dieser Code wird von einem Bytecodeinterpreter ausgeführt. Von Suns Java-VM Spezifikation wird keine VM interne Repräsentation von Objekten oder die Verwendung von bestimmten Datenstrukturen vorgegeben. Dies ist dem VM- Implementierer freigestellt und hat z.b. Einfluss auf das Java-Native-Interface (JNI). 2.3 Java-Bytecode Ein compiliertes Java-Programm besteht neben einigen anderen Informationen aus Java-Bytecode. Dieser Bytecode ist eine Abfolge von ein Byte langen Instruktionen, die zusammen das Java-Programm ergeben. Man kann sich diesen Bytecode als Assemblersprache für eine Stackmachine vorstellen. Die Aufgabe einer Java-VM ist es, diese Instruktionen auszuführen. Hierbei gibt es verschiedene Arten von Instruktionen: Es gibt Bytecodes zum Lesen und Schreiben auf bzw zum Verschieben zwischen den Array lokaler Variablen und dem Operandenstack. Wobei es hier eine Version gibt, die ein weiteres Byte als Parameter für den Index im Array lokaler Variablen erwartet und eine kleine Menge von Bytecodes die bereits einen konkreten Index adressieren. Es gibt Bytecodes für arithmetische und logische Operationen wie Addition, Subtraktion, Multiplikation, Division sowie AND, OR, und XOR Verknüpfungen. Diese Operationen werden in einem Java-Programm direkt mit Bytecodes, die auf dem Stack operieren ausgeführt. Es gibt Bytecodes zur Objektmanipulation, wie Objekterzeugung, Objektvergleich und Insanztypfeststellung, sowie Bytecodes um auf deren Felder oder Methoden zuzugreifen. Es gibt Bytecodes zur Regelung des Kontrollflusses wie bedingte und unbedingte Sprünge und Rücksprunganweisungen. Mit solchen Anweisungen werden Schleifen, Abfragen in If-Blöcken, das Beenden von Methoden und die anschließende Zurücklieferung eines Ergebnisses realisiert. Eine Besonderheit der Java-VM ist, dass alle Bytecodes typisiert sind. Ein Bytecode, der z.b. zwei Fließkommawerte addieren soll, erwartet diese und nur diese auf dem Operandenstack. Dies hat die Konsequenz das es Casting-Bytecodes zur Umwandlung von Daten auf dem Operandenstack in Daten eines andern Typs gibt. Nach der Java-VM-Spezifikation ist es erlaubt, dass Daten verschiedenen Typs übereinander auf dem selben Stack liegen. Möchte man jedoch typisierte Stacks die z.b. eine Konsequenz des Übersetzungsvorgangs einer in RPython geschriebenen Java-VM nach C sind haben, so muss man Java-VM-intern für jeden Typ einen eigenen Stack verwalten. Dies wird vor allem bei Bytecodes wie Pop (oberstes Element vom Stack entfernen) oder Swap (die beiden obersten Elemente des

2 FUNKTIONSWEISE EINER JAVA-VM 16 1 c l a s s AJavaClass { Listing 3: Ein simples Java-Programm 2 public s t a t i c void main ( S t r i n g [ ] args ) { 3 i n t i = 2 0 ; 4 i n t j = 2 1 ; 5 System. out. p r i n t l n ( i + j ) ; 6 System. out. p r i n t l n ( " Hello World " ) ; 7 } 8 } Stacks tauschen) problematisch. Listing 3. zeigt ein einfaches Java-Programm mit zwei Zuweisungen an die Variablen i und j in der statischen main Methode und den anschließenden zwei Ausgaben deren Summe auf der Konsole sowie des Textes Hello World. Listing 4. zeigt eine mögliche Darstellung der Classdatei dieses Java-Programms welche mit Hilfe das Java-Classfile-Disassemblers javap erzeugt wurde. Hier sind die Bytecodes des Klassenkonstruktors sowie der Mainmethode zu sehen. Nicht zu sehen ist unter anderem der constant pool der mit einer Symboltabele vergleichbar ist. Beim Nachvollziehen dieser Bytecodes sollte man stets einen sich verändernden Stack im Kopf haben. Man betrachte dazu Abbildung 2. die je das resultierende Array lokaler Variablen sowie den Operandenstack nach der Ausführung eines Bytecodes in der main Methode bis zur Ausgabe der Summe zeigt. Im Konstruktor wird mit aload_0 eine Objektreferenz (also insbesondere auf keinen Fall ein primitiver Datentyp) aus dem Array lokaler Variablen geholt und auf den Operandenstack gelegt. Es handelt sich um die this Referenz. Dann wird der Konstruktor der Basisklasse mit invokespecial aufgerufen und der Konstruktor mit return verlassen. In der Mainmethode werden die Zahlen 20 und 21 mit bipush auf den Operandenstack gelegt und mit den store Instruktionen an die Stelle 1 und 2 des Arrays lokaler Variablen geladen. Ein direktes Schreiben in dieses Array ist nicht möglich und damit der Umweg über den Operandenstack obligatorisch. Mit getstatic wird über einem Index in den constant pool der Classdatei (in der Abbildung nicht zu sehen in der Classdatei aber vorhanden) auf das statische PrintStream Objekt auf den Operandenstack gelegt. Danach werden die zwei Variablen wieder aus dem lokalen Variablen Array auf den Stack gelegt und dann mit iadd addiert. Bei dieser Instruktion werden die obersten Werte des Operadenstacks unter der Annahme, dass es sich um Integers (Ganzzahlen) handelt gepopt. Nach Ausführen dieser Operation liegt das Ergebnis wieder auf dem Operandenstack. Der Bytecode invokevirtual erwartet auf dem obersten Stackelement ein Objekt auf dem die Methode (die ebenfalls als Index in den constant pool der Klasse gegeben ist) println aufrufbar ist und führt diesen Aufruf durch. Dies gibt die

2 FUNKTIONSWEISE EINER JAVA-VM 17 Listing 4: Bytecode eines Java-Programms 1 c l a s s AJavaClass extends java. lang. Object { 2 AJavaClass ( ) ; 3 Code : 4 0 : aload_0 5 1 : i n v o k e s p e c i al # 1 ; //Method java/lang/object." < i n i t > " : ( )V 6 4 : return 7 8 public s t a t i c void main ( java. lang. S t r i n g [ ] ) ; 9 Code : 10 0 : bipush 20 11 2 : i s t o r e _ 1 12 3 : bipush 21 13 5 : i s t o r e _ 2 14 6 : g e t s t a t i c # 2 ; //Field java/lang/system. out : Ljava/io/ PrintStream ; 15 9 : iload_1 16 1 0 : iload_2 17 1 1 : iadd 18 1 2 : i n v o k e v i r t u a l # 3 ; //Method java/io/printstream. p r i n t l n : ( I )V 19 1 5 : g e t s t a t i c # 2 ; //Field java/lang/system. out : Ljava/io/ PrintStream ; 20 1 8 : ldc # 4 ; // S t r i n g Hello World 21 2 0 : i n v o k e v i r t u a l # 5 ; //Method java/io/printstream. p r i n t l n : ( Ljava /lang/ S t r i n g ; ) V 22 2 3 : return 23 }

2 FUNKTIONSWEISE EINER JAVA-VM 18 bipush 20 istore_1 bipush 21 Stack: 20 21 Array lokaler Variablen: 0: null 1: 0 2: 0 0: null 1: 20 2: 0 0: null 1: 20 2: 0 istore_2 getstatic iload_1 Stack: PrintStream 20 PrintStream Array lokaler Variablen: 0: null 1: 20 2: 21 0: null 1: 20 2: 21 0: null 1: 20 2: 21 iload_2 iadd invokevirtual Stack: 21 20 PrintStream 41 PrintStream Array lokaler Variablen: 0: null 1: 20 2: 21 0: null 1: 20 2: 21 0: null 1: 20 2: 21 Abbildung 2: Änderung des Stacks und des Arrays lokaler Variablen

2 FUNKTIONSWEISE EINER JAVA-VM 19 Zahl 41 auf der Konsole aus. Analog sind die nächsten drei Zeilen der Mainmethode zu verstehen, wobei der Bytecode ldc eine Konstante Hello World aus dem constanten pool ausliest. Der Bytecode return beendet in diesem Beispiel das Programm. 2.4 Aufbau einer Java-Classdatei Jede Java-VM muss im Stande sein, Java-Classdateien[20] zu parsen. Das heißt eine Java-VM muss eine Classdatei einlesen und verarbeiten können. Dabei werden zur Laufzeit der Java-VM interne Strukturen angelegt, die zur weiteren Verarbeitung des Java- Programms notwendig sind. Wie eine solche Classdatei aufgebaut ist wird nun im folgenden Abschnitt dargestellt. Eine Java-Classdatei ist mehr als eine Ansammlung von Java-Bytecodes. Informationen wie Parameter und Rückgabetypen vom Methoden, Klassen und Feldern der Klasse, die Basisklasse sowie implementierte Interfaces und Attribute wie zu werfende Exceptions lassen sich besser als Zusatzdaten, als als Bytecode darstellen. Diese Daten werden im folgenden als Elemente bezeichnet. Jede Java-Classdatei beginnt mit der Magicnumber 0xCAFEBABE sowie einer Versionsnummer. Dies soll signalisieren, dass es sich um eine Java-Classdatei handelt die ein Java-Programm (oder ein Teil eines Java-Programms) in einer von dieser Java-VM unterstützten Java-Version codiert. Gefolgt werden diese Informationen von einem Element variabler Länge, dem Constant_pool[]. Darauf folgen einige Flags über Klassenattribute wie public, abstract, final oder im Falle eines Interfaces interface. Danach folgen der Name der Klasse und ihrer Basisklasse in Form von Referenzen in den Constant_pool[]. Am Ende der Datei finden sich dann noch vier Elemente variabler Länge: interfaces[], field_info[], method_info[] und attribute_info[]. Der Constant_pool[] kann, wie alle anderen Elemente einer Java-Classdatei, als Array oder Liste von Elementen gesehen werden. Diese Elemente haben aber unterschiedlichen Typ und Länge. Der Constant_pool[] enthält natürlich Konstanten der Java-Classdatei. Jedoch handelt es sich nicht nur um Zahlenwerte und UTF8-Stringkonstanten, sondern auch um Methoden-, Feld-, und Klassensignaturen. Elemente des Constant_pool[] werden innerhalb einer Java-Classdatei nicht nur referenziert um Werte von mit final deklarierten Bezeichnern auszulesen, sondern auch beim Aufruf von Methoden oder Variablen innerhalb der Klasse genutzt. Der Constant Pool einer Java-Classdatei ist mit einer Symboltablle zu vergleichen. Die Elementmengen interfaces[], field_info[] sowie method_info[] enthalten je Elemente die die implementierten Interfaces, Felder und Methoden der Java- Classdatei beschreiben. Alle diese Elemente haben Referenzen in den Constant_pool[]. Method_info[] und field_info[] Elementmengen haben neben ihrem Namen noch Zusatzinformationen wie access modifier (Zugriffsbestimmer), Deskriptoren (die Signatur einer Methode oder eines Feldes) und eine Menge von

2 FUNKTIONSWEISE EINER JAVA-VM 20 Attributen. Attribute sind dabei Eigenschaften von Feldern, Methoden, Programmcode oder der Klasse selbst. Es gibt folgende Attribute: Das Codeattribut, welches den Bytecode enthält Das Exceptionsattribut, welches mögliche Exceptions, die die Funktion werfen kann angibt Das Constantvalueattribut d.h. den Wert eines konstanten Feldes Das InnerClassattribut, welches innere Klassen beschreibt Das LineNumberTableattribut, welches angibt welcher Bytecode zu welcher Zeile im Ursprungsprogramm gehört Das LocalVariableTableattribut, was den Wert einer Variable während der Methodenausführung angibt Selbst definierte Attribute Die Verarbeitung von Attributen ist in den meisten Fällen (mit der Ausnahme des Codeattributes) optional kann aber beim Debugging genutzt werden. Die letzte Elementmenge attribute_info[] enthält die Attribute der Klasse. 2.5 Unterstützung von nativen Code Eine Java-VM kann nicht völlig unabhängig von der Hardware sein auf der sie läuft. Es ist für einige Aufgaben wie z.b den Zugriff auf die Hardware oder das Betriebssystem notwendig, nativen Code, d.h. hardwarenahen Code der auf die Plattform zugeschnitten ist, auszuführen. Hier endet auch die uneingeschränkte Plattformunabhängigkeit. Denn wenn ein Programmierer nativen Code ausführen will, so ist dieser natürlich je nach Plattform unterschiedlich. Eine Möglichkeit, und zugleich der Standardansatz C/C++ Code von einem Java-Programm aus aufzurufen, ist das Java-Native-Interface[14] (JNI). Wenn ein Java-Programmierer auf Programmcode einer anderen Programmiersprache zugreifen will, so ist dies über das Java-Native- Interface (JNI) möglich. Für den Aufruf von z.b. C oder C++ Programmcode aus einem Java-Programm kann es mehrere Gründe geben. Zum einen gibt es Programmfunktionalität die durch Java und seine umfangreiche Klassenbibliothek nicht zur Verfügung gestellt werden kann, wie erwähnt das Aufrufen von Betriebssystemfunktionen oder den Direktzugriff auf Hardware. Ein anderer Grund kann es sein, dass durch die Nutzung von nativen Code (also systemspezifischen Code) eine große Steigerung der Effizienz/Performance eines Programms erreicht werden kann. Oder der Programmierer möchte eine Bibliothek in einer anderen Sprache nutzen. Neben der generellen Unterstützung von JNI war die Motivation vor allem, dass für

2 FUNKTIONSWEISE EINER JAVA-VM 21 Listing 5: Ein Java-Programm mit nativem Methodenaufruf 1 public c l a s s Add{ 2 i n t number ; 3 public native void c a l c ( i n t j ) ; 4 public s t a t i c void main ( S t r i n g [ ] args ) 5 { 6 System. loadlibrary ( " anativelib " ) ; 7 Add a = new Add ( ) ; 8 a. number = 4 0 ; 9 a. c a l c ( 1 ) ; 10 } 11 } 1 # include < j n i. h> 2 # include "Add. h" 3 # include < s t d i o. h> 4 Listing 6: Nativer C Code zu nativer Methode in änativelib " 5 JNIEXPORT void JNICALL Java_ Add_ calc 6 ( JNIEnv env, j o b j e c t t h i s, j i n t num) 7 { 8 j c l a s s c l s = ( env ) >GetObjectClass ( env, t h i s ) ; 9 j f i e l d I D f i d = ( env ) >GetFieldID ( env, c l s, " number ", " I " ) ; 10 j i n t i = ( env ) >G e t I n t F i e l d ( env, t h i s, f i d ) ; 11 p r i n t f ( "%i \n",num+ i ) ; 12 } die Integration von GNU-Classpath nativer Code ausgeführt werden muss. z.b. für Sockets, IO-Streams oder grafische Oberflächen. Das JNI selbst ist eine API, die von Sun spezifiziert wurde. Es in seiner heutigen Form nicht mehr exakt das JNI der Java-Version 1.0. Hier wurden einige Änderungen vorgenommen. Bei dem Design von JNI in seiner heutigen Form wurden folgende Ziele verfolgt: Es sollte unnötig sein verschiedene native Schnittstellen zu unterstützen und zu warten. Das heißt es ist unnötig für Entwickler von nativen Code diesen je nach VM anzupassen. Dies wird dadurch erreicht, dass die heutige Schnittstelle im Unterschied zu ihren Vorgängerversionen keine Annahmen über die Implementierungsdetails der virtuellen Maschine macht. Dies wird dadurch erreicht das alle Datentypen mit der Ausnahme von primitiven Datentypen aus der Sicht des nativen Codes Black-Boxes sind. Werden an nativen Code also Objekte übergeben, so ist es nicht möglich, auf diese direkt zu zugreifen. Natürlich ist Java-Code der nativen Code ausführt nicht mehr plattformunabhängig. Auch kann ein Fehler im nativen Code zum Absturz der ganzen VM führen. Listing 5. zeigt ein einfaches Java-Progamm was zur Addition zweier Zahlen eine native Methode nutzt. Dieses Beispiel wurde aufgrund seiner Einfachheit gewählt und nicht weil es einen sinnvollen Einsatz von JNI demonstriert. Deswei-

3 IMPLEMENTIERUNG EINER JAVA-VM IN PYTHON 22 teren werden Grundkenntnisse des Leser der Sprache C und Java im folgenden vorausgesetzt. Zu beachten sind einerseits das Laden der dynamischen Bibliothek anativelib mit dem Aufruf der Methode loadlibrary sowie das Schlüsselwort native vor der Methode calc. An diese Methode werden aus Java-VM Sicht zwei Parameter übergeben, die this Referenz auf das Add Objekt a und ein Integer j bzw. die Zahl 1. In Listing 6. ist die Implementierung der nativen Methode calc in C zu sehen. Sie befindet sich in der dynamischen Bibliothek anativelib. Diese fügt drei Headerdateien ein: jni.h um die JNI Environmentfunktionen und Datentypen zu nutzen, stdio.h für die Funktion printf sowie Add.h eine Headerdatei die den Prototypen der hier implementieren Funktion sowie die Macros JNIEXPORT (garantiert die Zugänglichkeit der Methode von Außen) und JNICALL (garantiert die Übergabe der Parameter nach der richtigen Calling Convention) enthält. Die Headerdatei Add.h kann mit dem Programm javah automatisch aus einer Classdatei erzeugt werden. Weiterhin ist zu beachten das der Name der Funktion sich von calc zu Java_Add_calc geändert hat. Dies ist eine Konvention um Namenskonflikte zu vermeiden. Die Funktion hat drei Parameter: env vom Typ JNIEnv, this vom Typ jobject und num vom Typ jint. Bei JNIEnv handelt es sich um das JNI Environment, eine Struktur voller C Funktionszeiger mit Hilfe der native Code in der Lage ist, mit der Java-VM zu interagieren. JObjekt ist ein gutes Beispiel dafür das keine Annahmen über die Implementierung der Java-VM gemacht werden. Auf dieses Objekt wird nicht direkt zugegriffen, sondern es kann nur an die Environmentfunktionen weitergegeben werden. Dies geschieht mit dem ersten Aufruf einer JNIEnv Funktion: GetObjectClass. Diese gibt ein anderes für den nativen Code nicht einsehbares Objekt zurück: cls, ein Klassenobjekt was die Klasse des Objektes repräsentiert. Mit dieser Klasse ist es möglich über die Environmentfunktion GetFieldID ein jfieldid Objekt zu erhalten mit Hilfe deren man auf ein Feld auf Java-Ebene zugreifen kann. Das Feld mit dem Namen number vom Typ Integer (Descriptor I) enthält den Wert 40 (vgl. Listing 5). Dieses wird mit j, einem Parameter von Typ jint addiert und mit Hilfe der Funktion printf ausgegeben. Primitive Java-Typen werden also direkt auf ihre C-Äquivalente abgebildet. Führt man das Java-Programm Add aus, so ist die Ausgabe 41. 3 Implementierung einer Java-VM in Python Nach der Erläuterung der Grundbegriffe und den Eigenschaften einer Java-VM, wird dieser Abschnitt anhand der hier implementierten Java-VM darstellen wie solche Software konkret implementiert werden kann. Dabei bezieht sich dieser Abschnitt auf die Implementierung von Classloader, Classfileparser und Byte-