Die Java Virtual Machine (JVM, KVM, CardVM) Stefan Menz stefan.menz@informatik.uni-ulm.de 09.07.2004
Vorwort Java is a simple, object-oriented, network savvy, interpreted, robust, secure, architecture neutral portable, high-performanced, multithreaded, dynamic language. Sun Microsystems Die Sprache Java entstand aus der Not heraus, als Sun Microsystems eine Reihe neuer elektronischer Endverbrauchergeräte (PDAs, Fernseh-Digitalempfänger und ähnliches) entwickelte und dafür eine neue Programmiersprache benötigte. Das sog. Green Team bei Sun begann daraufhin eine kompakte, objektorientierte Programmiersprache zu entwickeln welche sich in vielen Punkten an C++ anlehnte. Dies kann man heute noch an der nahezu gleichen Syntax der beiden Sprachen sehen. Java wurde jedoch im Gegensatz zu C++ von Grund auf objektorientiert entwickelt, wobei C++ lediglich objektorientierte Fähigkeiten auf C überträgt. Zusätzlich wurde Java prozessorunabhängig und netzwerkfreundlich entworfen, wobei netzwerkfreundlich eine sichere und effiziente Integration in Netzwerke bedeuten soll. Doch die Entwickler lenkten den Fokus von Java immer mehr auf das Internet (Applets) und Softwareentwicklung (Applications). So entwickelte sich Java mit der Zeit zu einer robusten, sicheren, portablen und performanten Sprache, welche sich mittlerweile Ihren Weg auch in die Marktsegmente Smartphones, Handys und Smartcards gebahnt hat. 1
Inhaltsverzeichnis 1 Einleitung 3 2 Die Java Virtual Machine 3 2.1 Portabilität........................................ 3 2.2 Sicherheit........................................ 4 2.3 Architektur........................................ 5 2.3.1 Datentypen................................... 6 2.3.2 Befehlssatz.................................... 6 2.3.3 Register..................................... 8 2.3.4 Stack....................................... 8 2.3.5 Heap....................................... 9 2.4 Das Class Dateiformat.................................. 10 3 Aktuelle VM-Typen 11 3.1 Die JVM......................................... 11 3.2 Die KVM......................................... 12 3.3 Die CardVM....................................... 12 4 Zusammenfassung & Ausblick 15 5 Anhang 16 5.1 Datentypen....................................... 16 5.2 opcodes......................................... 16 5.3 Abbildungs und Tabellenverzeichnis.......................... 17 6 Literatur / Quellenangaben 18 2
1 Einleitung Wenn man von Java spricht meint man damit nicht nur die reine Programmiersprache, sondern eigentlich das Gesamtkonzept. Java Programme sind nämlich nur auf Systemen lauffähig, welche die Java Plattform zur Verfügung stellen. Dies ist zum aktuellen Zeitpunkt überwiegend softwareseitig realisiert, wenngleich Sun Microsystems derzeit auch hardwaregestützte Lösungen entwickelt. Wir werden uns hier aber ausschließlich auf die Softwarevariante konzentrieren. Die Java Plattform setzt sich im Wesentlichen aus zwei Teilen zusammen, zum einen aus dem Java Application Programming Interface (API) und zum andern aus der Java Virtual Machine (JVM) 1. Die API ist aus dem Grundstudium, insbesondere aus dem SoPra, bereits bekannt und wird hier daher nicht weiter behandelt. Worum es sich bei der JVM handelt und wozu diese eigentlich dient wird dieses Essay schrittweise klären. 2 Die Java Virtual Machine Was bedeutet der Begriff Virtual Machine, insbesondere die Eigenschaft virtuell eigentlich? Unter einer realen Maschine können wir uns bereits etwas vorstellen, z.b. einen ganz normalen Desktop PC. Dieser arbeitet die eingegebenen Befehle und Daten schrittweise ab und rechnet auf den neu gewonnenen Daten weiter oder gibt diese aus. Eine virtuelle Maschine (virtuell meint in diesem Zusammenhang eher logisch) macht im Grunde genau dasselbe, man könnte daher auch von einer per Software emulierten Maschine sprechen. Bei der Java VM muss man aber generell zwischen zwei verschiedenen Bedeutungen unterscheiden. Zum einen ist sie als abstrakte Spezifikation zu verstehen, welche Richtlinien für eine mögliche Implementation vorgibt, und zum anderen ist eine implementierte Maschine selbst gemeint. Im weiteren Verlauf betrachten wir uns überwiegend die Spezifikation, bei den vorgestellten Beispielen beziehen wir uns jedoch immer auf die VM Implementation von Sun. Wir können also festhalten, dass die Java VM einen abstrakten Computer darstellt. Aber wozu braucht man diesen, wenn man doch eigentlich schon einen realen Computer zur Verfügung hat? Die Frage lässt sich mit zwei einfachen Wörtern beantworten Portabilität und Sicherheit. 2.1 Portabilität Versucht man nun einmal sich die VM anschaulich vorstellen, so trifft die Beschreibung einer Mischung aus Dolmetscher und Moderator wohl am ehesten zu. Dieser Vermittler interpretiert dabei das kompilierte Java Programm (realisiert als Java bytecode) und übersetzt dieses dann in die für die jeweilige Maschine ureigene Sprache (native machinecode). Zusätzlich regelt die VM den Datenaustausch zwischen Java Programm und der eigentlichen Umgebung (Maschinenarchitektur und Betriebssystem), auf welcher die VM ihrerseits ausgeführt wird (siehe Abb. 1). Die VM ist also das zentrale Element, welches Java seine Plattformunabhängigkeit verleiht, indem sie als Schnittstelle zwischen Java Applications bzw. Applets und dem eigentlichen Betriebssystem fungiert. Zwar kann jedes Java Programm mit jeder Virtual Machine kommunizieren, aber eine VM die für Windows geschrieben wurde kann z.b. nicht auf einem Macintosh laufen. Dies ist leicht nachzuvollziehen, wenn man sich zwei Dinge bedenkt. Zum einen unterscheidet sich der Aufbau bzw. die Befehle verschiedener Plattformen stark, da die einzelnen Systeme unabhängig voneinander entwickelt wurden. Zum anderen handelt es sich bei Java schließlich um eine interpretierte Sprache, die für die jeweilige Plattform übersetzt wird. Die Schnittstelle muss folglich für jede Plattform separat implementiert werden, um jeweils korrekt übersetzten zu können. Da sich aber nur relativ wenige Plattformen (z.b. ix86 / Windows bzw. Linux und PowerMac / Mac OS X) weit verbreitet haben, beschränkt sich dabei der Programmieraufwand. Zusätzlich überwiegt der Nutzen der Plattformunabhängigkeit den erstmaligen Programmieraufwand bei Weitem. Dieses Write Once, Run Anywhere Konzept führt somit zu einer wesentlich besseren Integration von einem Programm auf verschiedenen Systemen und reduziert so die Programmierkosten nachhaltig. 1 Im weiteren Verlauf bezieht sich die Abkürzung Vm immer auf die Java Virtual Machine 3
Abbildung 1: JRE vermittelt zwischen Java Programm und Betriebssystem [4] 2.2 Sicherheit Als weiteren wichtigen Aspekt muss man das Sicherheitskonzept von Java erwähnen. Dies ist, im Vergleich zu anderen Programmiersprachen, in Java tief verwurzelt integriert. Dies war auch wichtig, da Java überwiegend für netzwerkbasierte Anwendungen entwickelt wurde. Die Sicherheit in Java funktioniert nach dem Sandbox -Prinzip und ist dabei in drei Komponenten 2 unterteilt: den bytecode verifier, den class loader und den security manager. Im bytecode verifier wird zuerst geprüft, ob der Bytecode des auszuführenden Programms korrekt formatiert ist und sich nach der Sprachdefinition richtet. Im zweiten Schritt überprüft er den Code dann auf mögliche Sicherheitsverstöße. Dabei geht er generell davon aus, dass der gesamte Code schädlich ist und versucht die Sicherheitseinrichtungen des Systems auszuschalten bzw. zu umgehen. Dies geschieht durch einen speziellen Prüfalgorithmus, welcher im Einzelnen untersucht, ob Adressen falsch gesetzt, Zugriffsberechtigungen verletzt, Stack Über-bzw. Unterläufe erzeugt oder unzulässige Typumwandlungen vorgenommen werden. Hat der Code sämtliche Tests bestanden, so wird er an den class loader übergeben. Dieser verwaltet dabei wie und wann das Programm seine eigenen Klassen laden darf. Zunächst wird der Code geladen und die für die Ausführung nötige Namensraum-Hierarchie angelegt. Der Namensraum (sandbox) ist der Bereich in dem das Programm dann arbeiten darf. Um im Voraus Konflikte zwischen mehreren Programmen auszuschließen erhält jedes seinen eigenen Namensraum. Dadurch kann eine Anwendung nur auf ihre eigenen Klassen und die der Java API zugreifen. Zusätzlich stellt der class loader noch sicher, dass Programme nicht auf seine Methoden zugreifen können bzw. eigene class loader verwenden. Zur Laufzeit überwacht dann schließlich der security manager alle kritischen Methodenaufrufe (File I/O, Netzwerkzugriff, etc.). Dadurch verhindert er, dass unzulässige Zugriffe auf persönliche Dateien, Netzwerksockets oder Systemprozesse stattfinden können. 2 siehe Abbildung 2 für deren genaue Einbindung 4
Abbildung 2: Security Handling bei Java (links) und bei nativen Sprachen (rechts) [4] 2.3 Architektur Nachdem wir einen kleinen Einblick in die Vorzüge der Java VM gewonnen haben, wollen wir uns nun die eigentliche Funktionsweise bzw. Architektur der Virtual Machine näher betrachten. Da das Konzept der VM einem realen Mikroprozessor nachempfunden ist, finden sich dort ähnliche Komponenten (z.b. Register) wieder. Diese virtuelle Hardware der VM besteht aus fünf Hauptbestandteilen Befehlssatz, Register, Stack, Heap und Method-Area. Der Stack stellt dabei die zentrale Einheit zur Befehlsverarbeitung dar. Bevor wir aber auf die einzelnen Komponenten im Detail eingehen, ist es zunächst sinnvoll die interne Datenorganisation und repräsentation der VM zu verstehen. Abbildung 3: Architektur der Java Virtual Machine [6, 8, 9] 5
2.3.1 Datentypen Sämtliche Daten der Java VM sind typisiert, d.h. einem bestimmten Typ zugeordnet, welche von ihr direkt unterstützt werden und bis auf zwei Ausnahmen genau ein Wort lang sind. Ein Wort bezeichnet in diesem Zusammenhang die Basiseinheit für Datenwerte und muss eine Mindestlänge von 32 bit haben, welche zum jetzigen Zeitpunkt auf den meisten Rechnern Standard ist. Bei den Datentypen unterscheidet man zwischen zwei verschiedenen Arten den primitiven und den Referenz Datentypen. Die primitiven, numerischen Datentypen bestehen aus byte, short, int, long, char, float und double 3. Außer char sind alle numerischen Typen mit einem Vorzeichen versehen. Die Typen long und double bilden die eben angesprochen Ausnahmen, sie sind jeweils 64 bit breit bzw. zwei Wörter lang. Ein weiterer primitiver Datentyp ist der returnvalue Typ, welcher eine Adresse eines Befehls in der gleichen Methode speichert. Dieser Typ wird allerdings nur Javaintern verwendet und ist Programmierern daher nicht zugänglich. An diesem Punkt fällt auf, dass der boolean Typ bisher noch nicht erwähnt wurde, obwohl dieser als binärer Datentyp doch der primitivste sein müsste. Dies liegt daran, dass die VM im Gegensatz zur Sprache Java, keinen expliziten boolean Typ kennt. Dieser wird intern durch Abbildung auf einen int Wert realisiert, wobei die Zahl 0 für falsch und eine von 0 verschiedene Zahl für wahr steht. Bei den Referenz Datentypen unterscheidet man drei Arten von Referenzen: die class, die interface, die array Referenz. Diese stellen nur einen Verweis auf ein dynamisch erstelltes Objekt im Speicher dar, aber kein Objekt selbst. Die class Referenz verweist dabei auf eine Klasseninstanz, die interface Referenz hingegen auf alle Klasseninstanzen, welche ein Interface implementieren. Die array Referenz zeigt auf ein Array, welches in Java auch als vollwertiges Objekt behandelt wird. Zusätzlich gibt es noch die null Referenz, welche anzeigt, dass auf kein Objekt referenziert wird. Abbildung 4: Struktureller Aufbau der Datentypen der VM [10] 2.3.2 Befehlssatz Der Befehlssatz (instruction set) der VM ist, wie die Daten auch, typisiert 4. Es gibt also für die Addition zweier ints bzw. für die zweier floats zwei verschiedene Instruktionen (iadd bzw. fadd). Analog zum C++ Source Code, welcher bei der Kompilierung direkt in plattformabhängigen Maschinencode übersetzt wird, werden Java Programme in einen virtuellen Maschinencode den sog. Bytecode übersetzt. Es bietet sich daher an, den Befehlssatz als Assemblercode von Java Programmen anzusehen. Wie der Name Bytecode bereits vermuten lässt, ist jede Instruktion, auch opcode (operation code) genannt, genau ein byte lang. Derzeitig umfasst der Befehlssatz 203 der 3 Für einen genauen Überblick über den Wertebereich der Typen siehe Tabelle 3 4 Für den kompletten typisierten Befehlssatz siehe Tabelle 4 im Anhang 6
theoretisch 256 belegbaren Befehlsspeicherplätzte. Einem opcode folgen immer null oder mehrere operands (Operand bzw. Parameter), die genaue Anzahl hängt immer vom opcode selber ab. Die Länge der Operanden ist jedoch variabel und wird durch den opcode impliziert. Jeder opcode wird zusätzlich über einen vordefinierten Mnemonik angesprochen. Ein Mnemonik ist ein Pseudoname für eine Speicheradresse, welcher intuitiv zu verstehen und leicht zu merken ist. Dies ermöglicht es auch kompilierten Javacode nachzuvollziehen. Betrachten wir zum Beispiel den opcode goto (a7 hex), der eine 16 bit (=2 byte) Adresse erwartet, an die das Programm springen soll. Die Instruktion könnte folgendermaßen aussehen: 0xa7 4a f6 goto 4a f6 An dieser Stelle weiß die VM, dass die beiden folgenden bytes den Adress-Operanden darstellen und als ganzes eingelesen werden müssen. Operatoren, welche wie dieser länger als ein byte sind, werden standardmäßig in der Big-endian-Order gespeichert. Bei dieser Anordnung steht das byte mit der größten Ordnung an erster Stelle, das mit der zweitgrößten an zweiter Stelle, usw. In unserem Beispiel sieht die Adresse folgendermaßen aus: a4 * 256 + f6 Nach der Ausführung dieses Befehls muss wieder ein neuer Befehl kommen, da alle Operanden des vorherigen Befehls bereits eingelesen wurden. Die Abarbeitung der Befehle erfolgt also immer nach folgendem Schema [8] (Pseudocode): do { f e t c h an opcode ; i f ( operands ) f e t c h operands ; e x e c u t e the a c t i o n f o r the opcode ; } while ( t h e r e i s more to do ) ; Beispiel Bytecodeumsetzung einer simplen Methode [10] p u b l i c s t a t i c void domathforever ( ) { i n t i = 0 ; f o r ( ; ; ) { i = i + 1 ; i = i 2 ; } } Die Methode domathforever() ergibt nach dem Kompilieren folgenden Bytecodestream: opcode 03 3b 84 00 01 1a 05 68 3b a7 ff f9 offset 0 1 2 3 4 5 6 7 8 9 10 11 Diesen kann man wie folgt schrittweise analysieren 5 Offset Hex Code Mnemonic Bedeutung 0 03 iconst 0 Die Integer-Konstante 0 wird auf dem Stack abgelegt 1 3b istore 0 Speichert den aktuellen Wert auf dem Stack in die lokale Variable 0 2 84 00 01 iinc 0, 1 Inkrementiert die lokale Variable 0 um 1 5 1a iload 0 Lädt die lokale Variable 0 und legt diese auf den Stack 6 05 iconst 2 Legt die Integer-Konstante 2 auf den Stack 7 68 imul Holt beide ints vom Stak, führt die Integermultiplikation aus und legt das Ergebis auf dem Stack ab 8 3b istore 0 Speichert den aktuellen Wert auf dem Stack in die lokale Variable 0 9 a7 ff f9 goto 2 Springt zum Offset 2 der aktuellen Methode Tabelle 1: Analyse der opcode Umsetzung des Beispiels domathforever() 5 Eine Applet-Version ist unter [7] verfügbar 7
2.3.3 Register Da die VM stackbasierend konzipiert ist, gibt es lediglich einige Register, um genau zu sein insgesamt vier Stück pro Thread. Jedes dieser Register ist dabei genau 32 bit breit und dient zur Speicherung von Verweisen; die Parameterübergabe hingegen erfolgt ausschließlich über den Stack. Die Register sind im Einzelnen: pc der Programm Counter, welcher auf den nächsten auszuführenden Code verweist optop ein Verweis auf das oberste Element des Operanden-Stacks frame ein Verweis auf die Ausführungs-Umgebung der aktuell ausgeführten Methode vars ein Verwies auf die erste lokale Variable (Startindex 0) der aktuell ausgeführten Methode 2.3.4 Stack Der Stack ist das Kernstück der Virtual Machine. Er wird u.a. verwendet, um Parameter zu übergeben, Teilergebnisse zu speichen, Rückgabewerte zu empfangen und Rechenoperationen auszuführen. Den Stack können aber lediglich zwei Operationen direkt manipulieren, und zwar push (Daten ablegen) und pop (Daten abheben). Sämtliche opcodes die den Stackinhalt manipulieren arbeiten immer implizit mit diesen beiden Operationen. Jedes Mal wenn Tload (T steht für einen beliebigen Typen) einen Wert lädt wird dieser mit push auf den Stack geschoben. Sobald ein neuer Thread initialisiert wird, wird zeitgleich ein neuer Stack erstellt und diesem exklusiv zugewiesen. Auf diese Weise erhält jeder Thread seine private Arbeitsumgebung, welche vor den Zugriffen anderer Threads geschützt ist; die Java Stacks sind also thread-safe. Für jede Methode, die der Thread nun aufruft, wird ein separater Stack frame auf den Stack gelegt. Jedes dieser Stack frames stellt wiederum eine eigene Arbeitsumgebung für die darin aufgerufene Methode dar und verweist auf das zuvor oberste Frame, damit nach der Methodenausführung in das alte frame zurückgesprungen werden kann. Das oberste und somit aktuelle frame wird als current frame bezeichnet. Führt die VM nun Operationen auf dem Stack aus, so geschiet dies immer im current frame. Jeder einzelne Stack frame besteht wiederum aus drei Unterbereichen: den Lokalen Variablen (local variables) der Ausführungsumgebung (excecution environment) und dem Operanden Stack (operand stack) In ihnen werden alle nötigen Informationen gespeichert, um den aktuellen Zustand einer einzelnen Methode festzuhalten. Lokale Variablen Dies ist der Bereich in welchem die lokalen Variablen der Methode gespeichert werden. Da bereits beim Kompilieren bekannt ist mit wie vielen lokalen Variablen die Methode arbeitet, wird dieser frame-bereich immer mit der korrekten Größe initialisiert. Lokale Variablen sind dabei ein Wort (32 bit) lang und werden durch das vars-register adressiert. long- und double-werte nehmen daher zwei Variablenplätze in Anspruch, dabei wird aber nur der erste Teil adressiert. Werden z.b. Index n und n+1 von einem long belegt, so wird nur auf n adressiert; der nächste freie Platz für eine Variable wäre demnach n+2. Die Befehlssatz stellt die Befehle Tload bzw. Tstore zum laden von Variablen auf den Stack bzw. zum speichern von Ergebnisse in Variablen zur Verfügung. Beispiel Initialisierung lokaler Variablen [10] Betrachten wir uns die beiden folgenden Methoden und untersuchen wir wie die Parameter den lokalen Variablen zugeordnet werden: 8
p u b l i c s t a t i c i n t runclassmethod ( i n t i, long l, f l o a t f, double d, Object o, byte b ) { return 0 ; } p u b l i c i n t runinstancemethod ( char c, double d, short s, boolean b ) { return 0 ; } Abbildung 5: Lokale Variablen werden mit Methoden Parametern vorbelegt [10] Wir sehen, dass die Methodenparameter in der Reihenfolge, in der sie im Methodenkopf deklariert sind, in die lokalen Variablen geladen werden. bytes, shorts und chars werden dabei automatisch zu ints konvertiert. Da es sich bei der zweiten Methode um eine Objektmethode handelt wird implizit die this-referenz auf das Objekt an erster Stelle mitgespeichert (siehe Abbildung 5). Execution Environment Die excecution environment ist die Verwaltungsinstanz des Stack frames. Hier werden die Verweise auf den vorhergehenden Stack frame, auf die lokalen Variablen, das oberste Element des operand stacks und eine Liste mit Exceptions, welche die Methode werfen kann, gespeichert. Zusätzlich werden auch noch Informationen zum Debugging (z.b. Breakpoints) gespeichert. Operand Stack Auf dem operand stack werden letztendlich die Befehle zur Methodenausführung abgearbeitet. Dieser Stack ist dabei genau wie die lokalen Variablen auch 32 bit breit. Da sämtliche Argumente auf dem Stack typisiert sind, darf auch nur mit den dafür vorgesehenen Operationen gearbeitet werden, z.b. kann man nicht zwei ints mit fadd addieren. Auch hier werden die Typen byte, short und char generell zu ints konvertiert, bevor sie auf den Stack geladen werden. Nachdem alle Berechnungen ausgeführt wurden werden sie wieder zurückkonvertiert. Beispiel Addition zweier Integer [10] Dieses Beispiel wird zusätzlich in der folgenden Abbildung 6 veranschaulicht. Zuerst lädt iload 0 den int 100 auf den Stack, danach lädt iload 1 den int 98 auf den Stack. Danach holt iadd beide Werte vom Stack addiert diese und legt das Ergebnis zurück. istore 2 holt das Ergebnis vom Stack und speichert dieses in der lokalen Variablen 2. 2.3.5 Heap Der Heap (engl.: Haufen ) ist der dynamische Speicherbereich der Virtual Machine, den sich alle laufenden Threads teilen. Auf ihm werden alle Objekte, d.h. Klasseninstanzen und Arrays, erstellt und gespeichert. Für die Speicherbereitstellung gibt es spezielle opcodes (beginnend mit new). Der Heap wächst dabei dynamisch mit der Menge an Objekten mit. Zur Speicherbereinigung gibt es jedoch keine Befehle, um nicht mehr benötigte Objekte explizit zu löschen. Diese Aufgabe übernimmt der im Heap implementierte garbage collector, welcher dabei einen sogennanten mark & sweep-algorithmus 9
Abbildung 6: Beispiel für die Stackverarbeitung einer Addition [10] verwendet. Zuerst werden alle Objekte überprüft, ob sie noch direkt oder indirekt referenziert werden. Gibt es keine Referenzen mehr werden diese markiert (mark), danach werden alle markierten Objekte gelöscht (sweep). Außerdem wird pro JVM-Instanz jeweils ein separater, abgegrenzter Heap angelegt. Dadurch wird verhindert, dass zwei parallel ausgeführte Java Anwendungen auf den selben Speicher zugreifen können. Die Threads hingegen können aber sehr wohl parallel auf die gleichen Daten zugreifen. Aus diesem Grund ist auf die Synchronisierung von Threads besonderen Wert zu legen. Zwei weitere wichtige Teile des Heaps stellen die Method-Area und der Konstanten-Pool dar. Method-Area Sie bildet den statischen Bereich des Speichers. Hier werden alle relevanten Klassen und Methodeninformationen als kompilierter Bytecode abgespeichert. Informationen dieser Art sind u.a. die Symboltabellen für das dynamische Binden von Methoden, die Werte von Klassenvariablen und der Methodencode selbst. Konstanten-Pool Ein weiterer Bestandteil der Method-Area ist der Konstanten-Pool. Jeder erfolgreich geladenen Klasse wird ein eigener constant pool zugewiesen. In diesen Bereich werden sämtliche Informationen geladen, die für die Aufschlüsselung von symbolischen Verweisen benötigt werden. Beispielsweise die Namen aller Instanzvariablen und Methoden, den Namen der Klasse, etc. 2.4 Das Class Dateiformat In *.class Dateien werden sämtliche Metainformationen über die interne Namensgebung und Struktur der Klasse und der kompilierte Quellcode als Bytecodestom gespeichert. Für Klassendateien ist dabei ein genau definiertes Format festgelegt, um die Kompatibilität von Klassen und Plattformen zu garantieren. Die ersten vier bytes einer Klassendatei müssen, wenn es sich um eine gültige Datei handelt, den Hexadezimalwert CAFEBABE haben. Danach wird die Compilerversion gespeichert, um eventuelle Kompatibilitätsprobleme bereits von Anfang an auszuschließen. Beispielsweise können neue Standards hinzugekommen sein, die ein alter Compiler noch nicht unterstützt hat. Des Weiteren wird ein constant pool-array samt Länge gespeichert. In diesem sind alle Konstanten der Klasse, wie z.b. String-Konstanten, Namen von Instanzvariablen und andere Referenzen, gespeichert. In drei separaten Feldern werden der Name der Klasse, falls vorhanden der Name der Oberklasse (mit Verweis in das constant pool-array) und die Zugriffsrechte der Klasse festgehalten. Zusätzlich werden in drei weiteren Arrays die implementierten Interfaces und Informationen über alle Instanzvariablen und Methoden gespeichert. Jeder Eintrag im Interface-Array beinhaltet einen entsprechenden Verweis in den Konstanten-Pool. Die im Instanzvariablen- bzw. Methoden-Array gespeicherten Einträge stellen genaue Beschreibungen der Variable bzw. der Methode dar. Diese umfasst den genauen Namen, evtl. Wert und Signatur des Eintrages. Dabei gibt es verschiedene Signaturen für primitive Datentypen, Objekte und Arrays. Die Basistypen werden durch Großbuchstaben repräsentiert, z.b. char (C), boolean (Z) und Objekte durch einen String mit folgendem Schema: L<voller Klassenname>; Beispiel: Ljava.util.Stack; Arrays werden über öffnende eckige Klammern [ für die Dimension die Anzahl pro Dimension und den verwendeten Datentyp definiert, z.b. ist [20[10D eine 20 10 Doublematrix. 10
3 Aktuelle VM-Typen Nun da wir mit dem internen Aufbau der VM vertraut sind, ist es an der Zeit sich die unterschiedlichen Implementierungen genauer zu betrachten. Jede Implementierung ist fu r bestimmte Endgera te (siehe Abbildung 7) ausgelegt und muss die dafu r spezifischen Anforderungen erfu llen. Man teilt dabei die VM in drei verschiedene Kategorien ein: die Java Virtual Machine (JVM), welche eine vollsta ndig implementiert ist die Kilobyte Virtual Machine (KVM), welche leicht eingeschra nkt ist und die Card Virtual Machine (CardVM), welche auf die Wesentlichen Features reduziert ist Auf die genauen Spezifikationen, Eigenschaften und Einschra nkungen der einzelnen VMs wird im folgenden na her eingegangen. Abbildung 7: U bersicht der derzeit verfu gbaren Virtual Machines [12] 3.1 Die JVM Eine JVM ist die leistungsfa higste Virtual Machine und findet ihren Einsatz auf Servern, Workstations und Desktop PCs bzw. Notebooks. Auf diesen Systemen stehen ausreichend Ressourcen bereit, sodass eine vollsta ndige Implementierung der JVM Spezifikation, welche im vorherigen Teil besprochen wurde, mo glich ist. In diesem Anwendungsbereich gibt es zwei verschiedene Implementierungen, und zwar die Java Standard Edition (J2SE) und die Java Enterprise Edition (J2EE). Die J2EE baut auf der Standard Edition auf, ist aber durch einige fu r Firmen ausgelegte Komponenten, wie z.b. Java ServerPages oder Java Servlets, erweitert. Der Grundaufbau ist doch beides Mal der gleiche. Die VM kann auf solchen Systemen i.a. mehrere hundert Megabyte RAM fu r ihren Heap verwenden und eine Prozessorleistung von bis zu einigen Gigahertz sorgt fu r eine flu ssige und schnelle Befehlsverarbeitung. Dies ist fu r eine zur Laufzeit interpretierte Sprache sehr beachtlich. Auf 64-bit Systemen ist sogar eine Implementation von long und double Datentypen in nur ein Wort mo glich, alle anderen Typen sind dabei dann auch 64 bit breit. 11
3.2 Die KVM Im Gegensatz zur JVM ist die Kilobyte VM für vernetzte Informationsgeräte ausgelegt. Diese Geräte werden, je nach Speicherumfang und Rechenleistung, in zwei Kategorien unterteilt: mehrfach genutzte, lokale Endgeräte bilden die Connected Device Configuration (CDC) private, mobile Endgeräte bilden die Connected Limited Device Configuration (CLDC) Set-Top Boxes, InternetTV, Navigationssysteme und high-end PDAs gehören in die CDC-Kategorie, Handys, Pager und low-end PDAs in die CLDC-Kategorie. Die Java Micro Edition (J2ME) stellt für beide Konfigurationen unterschiedliche Implementierungen bereit. Für CDC-Geräte wird eine abgeänderte Version der J2SE verwendet. Dabei wurden die Klassenbibliotheken für Umgebungen mit wenig Speicher optimiert und einige unnötige Bibliotheken, wie z.b. die Swing- und Applet Bibliotheken, weggelassen. Im Endeffekt wird daher mit einer JVM gearbeitet, welche alle wichtigen Features wie Fließkommaberechnungen, dynamisches Laden von Klassen, die Verwaltung von Threads und den Sicherheitsmanager beinhaltet. Damit die JVM aber lauffähig ist, benötigt sie aber mindestens 1-2MB Arbeitsspeicher und 2MB ROM. Beim ROM (Read Only Memory) handelt es sich um den statischen Speicher, in welchem die Klassenbibliotheken und die JVM gespeichert sind. CLDC-Geräten steht aber wesentlich weniger Speicher zur Verfügung, deshalb kann die modifizierte JVM hier nicht mehr verwendet werden. Für die besonderen Speicheranforderungen wurde die KVM entworfen, diese ist bereits bei 32-512kB RAM und 40-80kB ROM lauffähig. Aufgrund des eingeschränkten Speicherplatzes müssen natürlich auch Abstriche bei den verwendbaren Datentypen und den eingebundenen Klassen gemacht werden. floats und doubles werden nicht unterstützt, da in den meisten Geräten sowieso keine Fließkomma-Recheneinheit (FPU) verbaut ist. Es werden auch nur die wichtigsten Klassen aus den Packages java.lang, java.io und java.util von der J2SE API geerbt, wodurch zugleich die Handhabung von Fehlern und Exceptions eingeschränkt ist. Bei nicht laufzeitbezogenen Fehlern wird entweder die Anwendung beendet oder das Gerät neugestartet, dies ist jedoch implementationsabhängig. Das Package javax.microedition, welches CLDC spezifisch ist, sorgt für die Vernetzung des Gerätes. Für jeden CLDC-Gerätetypen kann auch zusätzlich ein Mobile Information Device Profile (MIDP) definiert werden, welches speziell auf die Bedürfnisse des jeweiligen Typs zugeschnitten ist, und nur definierte Klassen bzw. Packages beinhaltet. Ein weiteres Mittel, um den Speicherbedarf zu reduzieren ist, dass Programme statt als Classdatei auch als Java Archiv (JAR) gespeichert werden können. Dies ermöglicht eine um 30 bis 50% höhere Kompression. Als weiteren wichtigen Punkt sollte man noch anmerken, dass der bytecode verifier nicht mehr in der Virtual Machine integriert ist. Der Prüfprozess ist nämlich recht zeit- und ressourcenaufwändig und daher nicht für geeignet. Stattdessen findet der Hauptteil der Überprüfung auf dem Rechner, auf welchem das Programm kompiliert wird, statt. In diesem Kontext spricht man auch von preverification. Die KVM muss dann lediglich mit ihrem eigenen verifier prüfen, ob die Classdatei vorüberprüft wurde und immer noch gültig ist. 3.3 Die CardVM Auch für Smartcards, den sog. Java Cards, ist eine Virtual Machine implementiert. Smartcards sind scheckkartengroße Plastikkarten mit integriertem Speicher und optionalem Prozessor. Der Speicher setzt sich dabei aus 1kB RAM, 16kB EEPROM (Electrically Erasable Programmable Read Only Memory) und 24kB ROM zusammen. Beim Prozessor, sofern vorhanden, handelt es sich um eine 8- oder 16-bit CPU mit 3,7MHz. Professionelle Smartcards für sicherheitssensitive Anwendungen verfügen über einen 32-bit Prozessor mit separatem Verschlüsselungs-Chip. Um mit diesen extremen Speichereinschränkungen umzugehen wurde eine äußerst sorgfältig getroffene Auswahl an Datentypen und Spracheigenschaften übernommen. Es werden nur boolean, byte, short, optional int und eindimensionale Arrays unterstützt. Auch alle für die Objektorientierung von Java notwendigen Features (z.b. Vererbung, Überladung von Methoden und dynamische Objekterstellung) wurden implementiert. Die CardVM verfügt aber nicht mehr über den Class Loader, den 12
Security Manager, Garbage Collector (meistens jedenfalls) und kennt auch keine Strings und Threads mehr. Auf einigen professionellen Java Cards ist jedoch ein Garbage Collector implementiert, welcher die Lebenszeit einer Karte deutlich erhöht. Karten ohne Garbage Collector können unbrauchbar werden, wenn irgendwann lauter tote Objekte den Speicher belegen und keine neuen Objekte mehr erstellt werden können. Zusätzlich wurde die Card VM in zwei Teile zerlegt. Der Teil der sich auf der Karte befindet, umfasst u.a. den Bytecode Interpreter. Auf einem Desktop-Rechner befindet sich die zweite Komponente, der Java Card Converter. Dieser wandelt die bereits kompilierten Klassendateien in ein spezielles Format, sog. Converted Applet (CAP) Dateien, um. Bei einer CAP-Datei handelt es sich um eine Reihe von Dateien die in einer umbenannten JAR-Datei gespeichert sind. Bei der Umwandlung übernimmt der Converter dabei Aufgaben, wie das Laden und Überprüfen von Klassen, das Binden von Methoden, die Initialisierung von Klassenvariablen und weitere Optimierungsprozesse. Die daraus entstandenen Daten werden in der kompakten CAP-Datei abgelegt. Neben den CAP-Dateien werden auch export-dateien erstellt, diese werden aber nur für den Prüf- bzw. Bindungsprozess benötigt und werden daher auch nicht auf der Karte verwendet. Abbildung 8: Die Zusammensetzung der CardVM [17] Nachdem ein CAP erfolgreich erstellt wurde, wird es mit Hilfe eines Installationsprogramms und eines Kartenlesers (Card Acceptance Device) auf die Karte geladen. Auf der Karte selber befindet sich ein separater Installer, welcher das übergebene Applet in den Speicher schreibt und alle nötigen Verknüpfungen zu anderen Applets und der Java Card Runtime Environment (JCRE) anlegt. Abbildung 9 zeigt den genauen Aufbau der JCRE. Es lässt sich deutlich erkennen, dass Applets und Laufzeit-Komponenten strikt voneinander getrennt sind. Durch diese klare Trennung wird sichergestellt, dass sich Applets auf Karten von verschiedenen Anbietern aufspielen und ausführen lassen. Verknüpft man ein Applet direkt mit den JCRE Komponenten, so könnte es ausschließlich auf dieser speziellen Implementation der Komponenten ausgeführt werden. Die Applets sind aber zusätzlich noch durch Firewalls voneinander getrennt, die abgegrenzten Bereiche werden Execution Context genannt. Applets die sich im selben Package oder Execution Context befinden haben vollen Zugriff aufeinander. Möchte ein Applet auf ein Applet aus einem anderen Ausführungsbereich zugreifen, so stellt es zunächst eine Anfrage an die JCRE. Die JCRE leitet diese an das Ziel Applet weiter. Stimmt das Applet der Anfrage zu, so erhält das anfragende Applet die Berechtigung auf den Datenzugriff. Andernfalls wird der Zugriff abgeblockt. Dies entspricht dem Sandkasten-Konzept, das wir bereits aus der JVM Spezifikation kennen. 13
Abbildung 9: Die JCRE-Architektur [17] Die Systemklassen stellen den Systemkern dar, welcher für die Verwaltung der Applets, des Speichers und der internen Kommunikation zuständig ist. Native Methoden unterstützen sie bei ihren Aufgaben, z.b. regeln sie die Kommunikation mit einem verbauten Kryptographie-Chip. Der Lebenszyklus der JCRE beginnt mit der Herstellung und Initialisierung der Karte und endet, wenn die Karte vernichtet wird. In der Zwischenzeit ist die Karte immer betriebsbereit. Kommt es während der Ausführung eines Applets einmal zu einem Stromausfall oder Schreibfehler, wechselt die Karte in einen StandBy Zustand. Dabei bleiben alle wichtigen Daten im EEPROM erhalten, lediglich temporäre Daten aus dem RAM gehen verloren. Beim nächsten Zeitpunkt an dem wieder Strom zur Verfügung steht, werden die verbliebenen Daten bereinigt und die Karte in einen vordefinierten Anfangszustand zurückversetzt. Bleibt noch die Frage, wie Applets eigentlich genau auf einer Java Card ausgeführt werden. Wie bereits beschrieben sorgt der on-card Installer für die Einbindung des Applets in die Karte. Danach registriert sich das Applet bei der Laufzeitumgebung und zeigt so an, dass es ab jetzt zur Verfügung steht. Soll ein bestimmtes Applet nun ausgeführt werden, so wird es von der JCRE selektiert. Alle Befehle die nun vom Computer / Kartenleser an die CardVM ankommen, werden automatisch an das selektierte Applet weitergeleitet. Nach Beendigung des Applets kann es explizit abgewählt werden. Die Karte befindet sich dann wieder in einer Warteposition, bis das nächste Applet ausgewählt und ausgeführt werden soll. Es besteht aber auch die Möglichkeit ein Standard Applet zu definieren. Dieses wird automatisch ausgewählt, wenn das zuletzt aktive Applet fertig ist. Abbildung 10: Die Benutzung eines Applets [16] 14
4 Zusammenfassung & Ausblick Wie wir gesehen haben lässt sich selbst auf den kleinsten Geräten noch ein lauffähiges Javasystem realisieren (siehe Tabelle 2), wenngleich auch eine aktuelle Java Card bereits schneller als ein IBM XT vor 15 Jahren ist. Heutzutage werden Java Cards schon für eine Vielzahl an Anwendungen benötigt. Modelle mit einem Verschlüsselungs-Chip regeln den Zugang zu Hochsicherheits-Bereichen, andere Modelle dienen als digitaler Ausweis oder digitale Geldbörse. CardVM-gestützte Geräte kommen aber nicht nur in Kartenform vor, es kann beispielsweise auch ein elektronisches Patientenarmband sein, auf welchem die jeweiligen Daten und die Krankengeschichte gespeichert ist. Merkmal JVM (Hotspot) JVM(CDC) KVM CardVM Prozessor 64/32bit 32 bit 16/32bit 8/16bit 166MHz 25MHz 16 32MHz 1 5MHz Speicher 32MB - 1-16MB 160-512kB 24,5kB - davon RAM min. min. 256kB min. 32kB min. 512B - davon ROM min. min. 512kB min. 128kB min. 24kB Hintergrundspeicher 1GB 1 5MB 1 2MB 16kB EEPROM Größe des VM-Codes 100-200kB 100-200kB 128kB 6 8kB ohne Bibliotheken Tabelle 2: Gegenüberstellung JVM, KVM und CardVM Die Performanz von Java wird aber immer weiter steigen, da viele Hersteller auf Grund der Marktentwicklung immer schnellere Prozessoren und immer mehr Speicher verbauen. So werden viele mobile Geräte wie PDAs und Mobiltelefone immer leistungsstärker und können somit auch immer aufwändigere Anwendungen steuern. Doch über kurz oder lang werden viele Geräte Java nicht mehr interpretieren müssen, sondern direkt per Hardware ausführen können. Da das zeitraubende Interpretieren des Bytecodes entfällt, kann mehr Leistung aus den Geräten gezogen werden. Als Seiteneffekt der Umstellung wird dann aber auch keine Virtual Machine mehr von Nöten sein. Dies wird sich in nächster Zeit wohl eher nur auf den Firmensektor ereignen, da diese auf eine sichere und zuverlässige Plattform für den Serverbetrieb angewiesen sind. Doch die Möglichkeiten sind unbegrenzt. Abbildung 11: Eine JavaCard 15
5 Anhang 5.1 Datentypen Datentyp Beschreibung Wertebereich (inklusiv) byte 8-bit 2er Komplement Integer 2 7 bis 2 7-1 short 16-bit 2er Komplement Integer 2 15 bis 2 15-1 int 32-bit 2er Komplement Integer 2 31 bis 2 31-1 long 64-bit 2er Komplement Integer 2 63 bis 2 63-1 char 16-bit Unicode Zeichen (ohne Vorzeichen) 0 bis 2 16-1 float 32-bit IEEE 754 einfachgenaue Fließkommazahl 2 31 bis 2 31-1 double 64-bit IEEE 754 zweifachgenaue Fließkommazahl 2 63 bis 2 63-1 returnvalue 32-bit Adresse eines Befehls 0 bis 2 31-1 reference 32-bit Adresse eines Objektes 0 bis 2 31-1 Tabelle 3: Datentypen der Virtual Machine 5.2 opcodes opcode byte short int long float double char reference Tipush bipush sipush Tconst iconst lconst fconst dconst aconst Tload iload lload fload dload aload Tstore istore lstore fstore dstore astore Tinc iinc Taload baload saload iaload laload faload daload caload aaload Tastore bastore sastore iastore lastore fastore dastore castore aastore Tadd iadd ladd fadd dadd Tsub isub lsub fsub dsub Tmul imul lmul fmul dmul Tdiv idiv ldiv fdiv ddiv Trem irem lrem frem drem Tneg ineg lneg fneg dneg Tshl ishl lshl fshl dshl Tshr ishr lshr Tushr iushr lushr Tand iand land Tor ior lor Txor ixor lxor i2t i2b i2s i2l i2f i2d l2t l2i l2f l2d f2t f2i f2l f2d d2t d2i d2l d2f Tcmp lcmp Tcmpl fcmpl dcmpl Tcmpg fcmpg dcmpg if TcmpOP if icmpop if acmpop Treturn ireturn lreturn freturn dreturn areturn Tabelle 4: Typisierter Befehlssatz der JavaVM 16
5.3 Abbildungs und Tabellenverzeichnis Abbildungsverzeichnis 1 JRE vermittelt zwischen Java Programm und Betriebssystem [4]........... 4 2 Security Handling bei Java (links) und bei nativen Sprachen (rechts) [4]....... 5 3 Architektur der Java Virtual Machine [6, 8, 9]..................... 5 4 Struktureller Aufbau der Datentypen der VM [10]................... 6 5 Lokale Variablen werden mit Methoden Parametern vorbelegt [10].......... 9 6 Beispiel für die Stackverarbeitung einer Addition [10]................. 10 7 Übersicht der derzeit verfügbaren Virtual Machines [12]................ 11 8 Die Zusammensetzung der CardVM [17]........................ 13 9 Die JCRE-Architektur [17]............................... 14 10 Die Benutzung eines Applets [16]............................ 14 11 Eine JavaCard...................................... 15 Tabellenverzeichnis 1 Analyse der opcode Umsetzung des Beispiels domathforever()............ 7 2 Gegenüberstellung JVM, KVM und CardVM...................... 15 3 Datentypen der Virtual Machine............................ 16 4 Typisierter Befehlssatz der JavaVM........................... 16 17
6 Literatur / Quellenangaben Literatur [1] Prof. Aaron Walsh, Boston College Introduction to Java and OOP http://www.mantiscorp.com/bc/mt36501/1.html [2] Sun Microsystems, Inc. 1999 The Java Language: An Overview http://java.sun.com/docs/overviews/java/java-overview-1.html [3] Sun Accessibility Team A Primer on the Java Platform and Java Accessibility http://www.sun.com/access/articles/wp-caped/ [4] Avishek Chaudhuri, Ioannis Zapitis, 1998. Final report - JAVABEANS http://www.iis.ee.ic.ac.uk/ frank/surp98/report/ac10/ [5] Sun Microsystems, Inc. 1997 Secure Computing with Java: Now and the Future http://java.sun.com/security/javaone97-whitepaper.html [6] Java Architecture Survey Code Examples and VM Arcitecture http://twins.ee.nctu.edu.tw/ ilionyl/java/welcome.html [7] Artima Software, Inc. 2004 Eternal Math A Simulation of the Java Virtual Machine http://www.artima.com/insidejvm/applets/eternalmath.html [8] Jens Nüßeler, 1997 Java Virtual Machine Seminar Java und Script-Sprachen, Universität Ulm http://www-vs.informatik.uni-ulm.de/lehre/seminar Java/ausarbeitungen/JavaVM/ [9] I. Arweiler, E. Haase, 1998 Die JAVA VIRTUAL MACHINE Überarbeitete Fassung http://www.inf.fu-berlin.de/lehre/ws98/nsp/jvm/jvm.html [10] The McGraw-Hill Companies, Inc. 1997 Inside the Java Virtual Machine http://mail.phys-iasi.ro/library/computing/jvm/toc.html [11] Tim Lindholm, Frank Yellin The Java Virtual Machine Specification Second Edition http://java.sun.com/docs/books/vmspec/2nd-edition/html/vmspectoc.doc.html [12] Sun Microsystems, Inc. 2003 CDC WhitePaper http://java.sun.com/products/cdc/wp/cdc-whitepaper.pdf [13] Sun Microsystems, Inc. 2000 The K virtual machine (KVM) White Paper http://java.sun.com/products/cldc/wp/ [14] Qusay Mahmoud, 2001 J2ME APIs: Which APIs come from the J2SE Platform? http://developers.sun.com/techtopics/mobility/midp/articles/api/index.html [15] Sun Microsystems, Inc. Smart Card Overview http://java.sun.com/products/javacard/smartcards.html [16] C. Enrique Ortiz, 2003 An Introduction to Java Card Technology - Part 1 http://developers.sun.com/techtopics/mobility/javacard/articles/javacard1/ [17] Zhiqun Chen, 2000 Technology for Smart Cards: Architecture and Programmer s Guide http://java.sun.com/developer/books/consumerproducts/javacard/ch03.pdf 18