Java Prozessoren Referat Labor Computertechnik WS 2004 Bernd Reiners Mat.Nr.: 49621622 Einführung Es gibt zwei Möglichkeiten Java direkt auszuführen: Die spezielle Softwareimplementierung einer virtuellen Javamaschine (JVM) für Mikrocontroller oder eine Implementierung des JavaStandards direkt in Hardware. Hier soll die zweite Möglichkeit näher betrachtet werden. Java Prozessoren sind auf die direkte Verarbeitung von Java Bytecode optimierte CPU's. Sie können den Bytecode direkt auf der Hardware ausführen, es kann auf das langwierige Umsetzen des Bytecodes in native, dem Prozessor angepasste Befehle verzichtet werden wodurch die langen Ausführungszeiten von softwarebasierten VM's entfallen Java Prozessoren könnten als in Silizium gegossene Java Virtual Machines bezeichnet werden. Realisierungsmöglichkeiten Die direkte Ausführung von Bytecode in Hardware kann auf verschiedene Weise realisiert werden. der Java Beschleuniger nutzt einen 'Koprozessor', der zusätzlich zum Prozessor aktivierbar ist. Dieser nimmt der CPU die Verarbeitung von Java Bytecode ab. Er unterstützt die (allen JVM's gemeinsame) StackArchitektur. Ein Beispiel hierfür ist Nazomis Java Koprozessor JA108 [9] (4k -2fach assoziativer Bytecode-Cache, 2K Data Cache etc). zusätzlicher Befehlsatz bei diesem Konzept unterstützt die CPU zusätzlich einen (Java-VM) Befehlssatz, wodurch durch einfaches Umschalten die CPU zu einer Java Virtual Machine wird. Ein Beispiel hierfür ist die 'ARM Jazelle Technology' [8a], die in den ARMxxJx Prozessoren zum Einsatz kommt. 'reine' Java Prozessoren Von welchen dies Referat handelt und die im Folgenden näher untersucht werden Einsatzgebiete Der Einsatz von Java-Prozessoren ist in vielen Bereichen möglich. Insbesondere eignen sie sich in ressourcenkritischen Umgebungen wie z.B. embedded Systems oder Mikrokontroller in denen die Java Features genutzt werden sollen. SmartCards Mobiltelefone pagers Drucker SetTop Boxen Navigations Systeme PDA‘s IP-Phones, IP-TV, etc Die meisten dieser Systeme arbeiten mit weniger als 100MHz Taktfrequenz und 2 MB Speicher, wodurch Leistungsverbrauch und Hauptspeicherverwendung schnell zu Knock-out Kriterien bei Java Prozessoren werden. Einsatzgebiete von Java Prozessoren { SEQ Einsatzgebiete_von_Java_Prozessoren \* ARABIC } Übersicht: Java Prozessoren Context Der Java Prozessor Kontext von Sun [1] besteht neben der Spezifikation picoJava [6,7] aus zwei Mikroprozessorlinien – microJava und ultraJava: picoJava PicoJava (picoJava Core Specification) ist die Spezifikation für einen minimalen Java Prozessor. Die Java Prozessor Linien von Sun basieren auf dieser Spezifikation. Da die picoJava Technology Lizenz anderen Chip-Herstellern zur Verfügung gestellt wird, dient picoJava auch diesen Prozessoren als Grundlage. Lizenznehmer von picoJava sind unter anderen Firmen wie Fujitsu und IBM . picoJava ist speziell für den Einsatz in Mobiltelefonen, PDA's, Set-Top-Boxen und anderen Kleingeräten ausgelegt. microJava 'MicroJava' ist die 'customized Version' und der erste real existierende Mikroprozessor von Sun. MicroJava erweitert picoJava um anwendungsspezifische E/A Einheiten, sowie Speicher-, Kommunikations- und Steuerfunktionen. Der Preis von microJava lag 1997 zwischen 25 und 100 Dollar, was microJava für den Einsatz in einer Vielzahl von elektronischen Geräten, der Telekommunikation und anderen nicht vernetzten Anwendungen wie Drucker, Spielkonsolen, etc. auszeichnet. ultraJava UltraJava ist Sun’s High-End-Java-Prozessor. UltraJava unterstützt Sun's Visual Instruction Set (VIS), einer performanten Hardwaregraphikerweiterung. Die ultraJava Prozessorlinie zielt hauptsächlich auf High-End-3D-Graphik- und Multimediaanwendungen ab. Der Preis lag bei c.a 100 Dollar und darüber. Pico Java Die Spezifikation eines Java Prozessors, picoJava soll nun näher untersucht werden. Da wie bereits erwähnt der Java Prozessor eine Implementierung der JavaVirtualMachine plus Erweiterungen zur Hardwarekontrolle ist, mag einiges aus der Darstellung der JVM bereits bekannt sein. Zunächst Darstellung der JVM, sofern sie von picoJava implementiert ist Überblick Durch die Spezialisierung des Prozessors auf die Sprache Java können beim Entwurf bereits Optimierungen entwickelt werden, die zu einer Leistungssteigerung führen (s.u.). Besonderheiten der JVM sind z.B. die stack-basierte Verarbeitung von Bytecode und die Speicherbereinigung (GC). In picoJava wird daher statt eines frei wählbaren Registersatzes ein Stackregistersatz mit 64 Einträgen genutzt. Die Java Virtual Machine (im folgenden VM genannt) ist eine 32-Bit Stack-Architektur. Das bedeutet, Operanden einer Instruktion werden vor Ausführung der Instruction, z.B. durch LOAD/STORE, auf den Stack geholt, durch die Instruktion bearbeitet und das Ergebnis wieder auf dem Stack abgelegt wo es dann für weitere Operationen zur Verfügung steht. Der Stack ist in Analogie zu einer Registerdatei aus der RISC-Architektur zu sehen, Operationen können nur Objekte auf dem Stack referenzieren. [10 Kap. 3.1] Ein weiteres Merkmal aus der herkömmlichen RISC-Architektur ist der Speicherzugriff, der bei der VM nur durch Load/Store Befehle möglich ist. Die einzige Ausnahme hierbei stellt die IINC-Instruktion dar die einen Speicher-zu-Speicher Zugriff darstellt und vornehmlich zum Inkrementieren von Schleifenvariablen gedacht ist. Die VM hält eine Reihe von Registern, die meist zur Kontrolle spezielle Funktionen im Core dienen oder die Adressen verschiedener Bereiche im Stack enthalten (Flagregister, Stackpointer o.ä.) Darüber hinaus besitzt die VM noch 32-Bit Register: − PC (program counter) − VARS (points to base of the current set of local variables) − OPTOP (current top of evaluation stack) − FRAME (points to the base of the current execution environment in memory). vars Local Variable Pointer Register enthält Basis Adresse (erste, unterste) des 'Lokale Variable' Bereichs der akt. Methode, sprich Adresse der lokalen Variablen 0. Variable 1 wird dann an 'Vars-4' (4=1Word in Byte) abgelegt Frame – Frame Pointer Register Startadresse des Aufrufstacks der akt. Methode optop – Top-of-Stack Pointer Register Adresse des akt. obersten (noch leeren) Stackeintrags. Nach Push wird Registerwert um 4 inkrementiert. Aktuell oberster Eintrag mit gültigem Wert ist demnach 'iptop+4' oplim – Minimum Value of Top-of-Stack Register enthält minimalen wert den Top-of-Stack Register optop annehmen kann. Begrenzt das stack Wachstum (von oben nach unten) Form: Enable Flag wird bei PowerOn Resettet ( Software setzt es, HW setzt es zurück) sc_bottom – 'address of deepest Stack Cache Entry' register zeigt den momentan 'tiefsten gültigen Eintrag des Operanden Stacks' im Stackcache Speicherorganisation der Laufzeit Umgebung Der Prozessor einer (virtuellen) Java Machine ist wie erwähnt eine Stackmaschine mit mehreren Stacks 1 -jeweils ein JVM Stack pro Thread 2 - und einem von allen Threads gemeinsam genutzten Heap. Im Heap werden sämtliche Objekt-Instanzen sowie Klassen und Interfaces abgelegt, wobei sich Interfaces noch einmal in einem speziellen Bereich, der sog. Method Area befinden. Der Heap wird durch den Garbage Collector automatisch verwaltet / bereinigt. Zur Verwaltung von Prozeduraufrufen und Rücksprungadressen dient der Aufruf-Stack. Er enthält Frames (Aktivierungssätze), die bei jedem Unterprogrammaufruf erzeugt, auf dem Aufruf Stack abgelegt werden und die nötigen Rücksprungadressen, Zellen für lokale Variablen, Parameter, Operanden etc. enthalten. Der JVM-Stack ist eine Vereinigung von Aufruf- und Operanden Stack (AufrufStack, Parameter, lokale Variablen) (s.u.) Zu jedem Stack existiert ein PC Register (Programmzähler), das auf die Adresse des gerade ausgeführten Codes verweist. Der class-constant-pool ist der Daten- und Codebereich eines Java Programms. Er ist ein Index-Mapping Mechanismus für kompilierte Methoden in class-Dateien. Jede Klasse hat einen zugehörigen Konstanten Pool 1 hat den Vorteil eines kompakteren Befehlssatzes, da Operanden weggelassen werden können (Operanten Stack) 2 für Aufrufe nativer Methoden kann es dazu noch einen Native-Stack pro Thread geben Mikroarchitektur E/A Bus- und Speicherschnittstelle stellt die Schnittstelle zwischen dem picoJava Kern, dem externen Speicher und anderen E/AEinheiten dar. Sie nimmt Daten an und liefert diese an den Instruktions- und den Datencache weiter. Datencache besteht aus dem, in seiner Größe konfigurierbaren Datencache und dem Datencache-Kontroller. Die Cachegröße ist zwischen 0 Kbyte, 1 Kbyte, 2 Kbyte, 4 Kbyte (default), 8 Kbyte und 16 Kbyte konfigurierbar. Die Datencache-Einheit arbeitet die Anfragen des Dribble Managers (s. u.) und der Pipeline ab (wobei der Pipeline Priorität eingeräumt wird) Der DatenCache ist ein '2-fach assoziativer' Cache mit Schreibstrategien write-back und write allocate [10 Kap. 6.1] Instruktionscache besteht aus dem, in seiner Größe konfigurierbaren Instruktionscache und dem Instruktionspuffer. Die Cachegröße ist zwischen 0 Kbyte, 1 Kbyte, 2 Kbyte, 4 Kbyte (default), 8 Kbyte und 16 Kbyte konfigurierbar. Der Instructionscache cached eingegangene Instruktionen und liefert sie an die Decodiereinheit Um den Rest der Pipeline von der Fetch-Stufe zu trennen, wird ein 16 Byte Instruktionspuffer eingesetzt, um die, aus dem Speicher geholten Instruktionen solange vorzuhalten, bis sie von der Integer-Einheit (bzw. Decodier Einheit) verarbeitet werden können. Der InstructionCache ist ein 'direct-mapped' Cache mit 16-byte großen 'Cache-Lines' (64 Einträge pro kb Cachegröße) [10 Kap. 6.1] Stackcache besteht aus dem Stackmanager, einen 3-Lese, 2 Schreib Port Stackcache (64 Einträge à 32 bit) und dem Dribble Manager(s.u.) Er ist für die Kommunikation zwischen Stack und Integer-Einheit (Operanden) verantwortlich und kontrolliert Lesen bzw. Schreiben von Daten zwischen Datencache und Stackcache. Der Stackcontroller sorgt für die nötigen Steuersignale an der Integer-Unit (2 Lese- 1 Schreibport zum Stackcache) um Operanden zu empfangen. Er kontrolliert ebenfalls das Lesen von Daten aus dem Datencache in den Stackcache und umgekehrt Der sog. Dribble Manager (to dribble = tröpfeln) - ein Hardware-Mechanismus der den aktuellen Füllstand des Stacks überwacht - sorgt bei einem drohenden Stack-Überlauf dafür dass Daten vom Stackcache in den Datencache ausgelagert und umgekehrt bei Stack-Unterlauf vom Speicher in den Stackcache zurück geschrieben werden. [6a] Dafür verfügt er über einen einzelnen Schreib- Lese Port des Stackcaches der exklusiv vom Dribbler genutzt wird (s.u.) Da diese Transaktionen im Hintergrund stattfinden während der Core weiterhin seinen Hauptaufgaben nachgeht ist der Begriff 'Dribbler' (to dribble = tröpfeln) hier so zu verstehen dass durch die ständigen Transaktionen kleine Datenmengen vom Cache in den Speicher tröpfeln und umgekehrt. Höchst- bzw. Tiefststand des Stacks sind durch 'High- bzw Low Watermark' in den 3-Bit Feldern PSR.DBH und PSR.DBL bestimmt. In jedem Taktzyklus wird nun die Anzahl der vorhandenen Stack-Einträge durch Vergleich von SC_BOTTOM und OPTOP bestimmt. Abhängig vom Ergebnis wird eine Über- oder Unterlauf Transaktion ausgelöst: Ist die Anzahl der gültigen Einträge größer als die 'High Watermark' startet der Core eine Überlauf Transaktion welche den obersten Wert, den an der Stelle 'SC_BOTTOM' herausschreibt und 'SC_BOTTOM' mit der Adresse des nun gültigen obersten Stackeintrags aktualisiert. Dieser Vorgang wiederholt sich bis die Anzahl der Stackeinträge unter die 'High Watermark' sinkt. Liegt die Anzahl der beschriebenen Stackeinträge unter der 'Low Watermark' wird eine 'Fülltransaktion aus dem Speicher' getriggert indem der Wert an der Stelle 'SC_BOTTOM+4' angefordert wird und sobald dieser vorliegt ' SC_BOTTOM' mit der Adresse des neuen nächsten gültigen Stackeintrags aktualisiert. Auch dieser Vorgang wiederholt sich bis die Anzahl der Stackeinträge die 'low Watermark' erreicht. Integer-Unit ist der 'Pförtner' für alle Instruktionen die ausgeführt werden. Sie holt Instruktionen aus der Instruktionscache-Einheit( InstructionFetch), holt bzw. speichert Daten aus/in der Datencache- Einheit, leitet Floating-Point-Instruktionen an die Floating-Point-Einheit weiter und führt alle Nicht-Floating-Point Instruktionen der JVM und die erweiterten Bytecodes (s.u.) aus. Da picoJava eine stackbasierte Architektur ist sind für den Programmierer hier keine General Purpose Register sichtbar. Außerdem enthält die IU microcodeROM welcher verschiedene JVM-.Befehle enthält Die Floating-Point-Einheit führt alle mathematischen Funktionen aus. Sie ist optional. Signale (vollständige Listung der Signale in [6a],[6b] ) pj_reset the processor at address 0x00000000. setup R1 Resets and starts pj_reset_out extended bytecode was executed. valid R2 Indicates the reset pj_clk clock. In R0 CLK picoJava core pj_clk_out external interfaces. valid R1 picoJava’s clock to pj_irl [3:0] signals. In Interrupt exception pj_nmi interrupt input to the picoJava core. In Nonmaskable pj_boot8 instruction cache fetches. static R0 Controls the size of pj_standby_out that the processor is in standby mode. valid R1 Notifies the system pj_no_fpu Floating Point Unit. static R0 Disables the internal pj_scan_out facility. valid R0 Provides basic scan pj_scan_mode in the core to serial shift. setup R1 Switches flip-flops pj_scan_in processor core’s scan chain. setup R1 Input to the pj_data_in[31:0] setup R1 Used during reads. pj_data_out[31:0] valid R1 Used during writes. pj_address[31:0] with nonmultiplexed 32-bit address bus. valid R1 Used to interface pj_size[1:0] requested data. valid R1 Indicates the size of pj_type[3:0] of transaction requested by the Integer Unit. valid R1 Indicates the type pj_tv transaction to the memory controller. valid R1 Starts a new pj_ack[1:0] will be driving in same cycle on pj_data_in. setup R0 Indicates that data pj_ale[1:0] latching. valid R0 Enables address pj_halt fetching. setup R0 Halts instruction pj_resume fetching. setup R0 Resumes instruction pj_brk1_sync detected by the core. valid R2 Breakpoint 1 pj_brk2_sync detected by the core. valid R2 Breakpoint 2 pj_in_halt 1 mode (not fetching instructions). valid R0 Processor is in halt pj_inst_complete valid retrieved (when this signal is high). R2 An instruction was just der Befehlssatz Jede Anweisung der Java VM besteht typischerweise aus einem ein Byte großem Op-Code (256 Instruktionen) plus null oder mehreren Operanden verschiedener Länge. Die Anzahl oder Größe der Operanden wird dabei implizit durch den Op-Code bestimmt. Da der Befehlssatz Java's stackbasiert ist, werden bei vielen Befehlen die Operanden vom Stack geholt und das Ergebnis wieder auf den Stack gelegt. Diese Befehle brauchen keine zusätzlichen Bytes für Operanden und haben somit eine Länge von einem Byte. Befehle mit Operanden haben dagegen eine Länge von zwei, der oder mehr Bytes (s.u.) Beispiel: iadd definiert eine Integer-Addition komplett, es muss nicht explizit angeben werden was addiert, oder wo das Ergebnis abgelegt werden soll. Die zwei obersten Elemente werden vom Stack geholt, addiert und das Ergebnis wieder zurück auf den Stack gelegt. [10] Befehlsfomat Der Befehlssatz der JavaVM ist typisiert, für jeden Operandentyp gibt es einen speziellen Befehl (z.B. Addition von Integer-Zahlen: iadd). Sind Operanden länger als ein Byte, werden sie in einer "Big-endian"-Order gespeichert. opcode byte short Tipush Tconst bipush sipush Tload Tstore Tinc Taload Tastore Tadd Tsub Tmul Tdiv Trem Tneg Tshl Tshr Tushr Tand Tor Txor i2T l2T f2T d2T Tcmp Tcmpl Tcmpg if_TcmpOP Treturn baload bastore saload sastore i2b i2s int long float double iconst iconst iload Istore Iinc iaload iastore iadd isub imul idiv irem ineg ishl ishr iushr iand ior ixor lconst fconst dconst aconst lload lstore fload fstore Dload dstore aload astore laload lastore ladd lsub lmul ldiv lrem lneg lshl lshr lushr land lor lxor i2l l2i f2i d2i faload fastore fadd fsub fmul fdiv frem fneg fshl daload dastore dadd dsub dmul ddiv drem dneg dshl i2f l2f f2l d2l lcmp fcmpl fcmpg i2d l2d f2d d2f freturn dreturn if_icmpOP ireturn lretrun char reference caload aload castore aastore asd dcmpl dcmpg if_acmpOP areturn Funktionale Gruppierung Die 226 Befehle der JVM können in 15 funktionale Gruppen aufgeteilt werden. Um die decodierung möglichst effizient zu halten, bzw. um geringe Codedichte zu gewährleisten gibt es variable Codelängen, wobei häufig genutzte Instructions kürzer sind. Die Meisten Instruktionen (62%) sind 1 Byte groß. Die restlichen Befehle sind zumeist 2 Byte (20%) oder 3 (15%) Byte lang. Nur 6 Befehle (3%) sind größer als 3 Byte. Die durchschnittliche Befehlslänge beträgt 1,8 Byte (14-15 Bit). Befehle > 3Byte: Opcode Mnemonic Size Description Cycles 185 (0xb9) invokeinterface 5 Call an interface method. Trap 4/6 Extend local variable index by additional bytes. Trap 4 Allocate new multidimensional array. 196 (0xc4) 197 (0xc5) wide multianewarray Trap 200 (0xc8) goto_w 5 Branch always (wide index). 201 (0xc9) jsr_w 5 Jump subroutine, 4-byte offset. 4 Befehlstyp Konstante auf Stack legen Laden/Speichern lokaler Variabeln Stackverwaltung Arithmetik Schiebe- und Logikoperatoren Verzweigungen und Vergleiche Ausnahmebehandlung Arrayverwaltung Methodenrücksprünge Switch-Table Verzweigungen Konvertierungen Monitore Änderungen von Objektfeldern Methodenaufrufe Sonstige Objektbehandlung Gesamtzahl Anteil (in %) 4 Gesamtzahl Anzahl von Befehlen mit der Länge: 2 Bytes 2 41 3 Bytes Mehr als 3 Bytes 13 82 1 Byte 8 40 3 1 - 10 24 12 27 1 20 7 2 15 2 4 4 3 226 100 10 24 12 5 1 17 7 15 2 141 62 1 1 45 20 19 1 4 3 3 34 15 2 1 2 1 6 3 226 Instruktionen insgesamt teilen sich wie folgt: 141 Instruktionen 1 Byte Länge (62%) 45 Instruktionen 2 Byte Länge (20%) 34 Instruktionen 3 Byte Länge (15%) 6 Instruktionen >3 Byte Länge (3%) Vergleicht man den Befehlssatz von Java z. B. mit dem eines RISC-Prozessors, erscheint der Befehlssatz von Java unvollständig. Es fehlen beispielsweise alle Befehle, die mit den Zielen von Java nicht vereinbar sind. Das JSM 3 fordert, dass es für den Programmierer nicht ersichtlich sein darf, wo Objekte im Speicher liegen. Deshalb ist es mit Java-Instruktionen nicht möglich direkt und willkürlich auf den Speicher zuzugreifen. JVM-Instruktionen arbeiten stattdessen auf Objektreferenzen, welche keine Rückschlüsse auf die tatsächliche Position der Objekte im Speicher zulassen. Der Speicher ist für den Java Programmierer wie eine Black-Box. Es ist die Aufgabe der JVM die Position der referenzierten Objekte im Speicher zu bestimmen. Befehle, die man im Befehlssatz von Java außerdem vermisst sind Befehle zur Hardwarediagnose oder zu low-lewel Hardwaremanagement, Befehle um den CPU-Status zu lesen oder zu schreiben oder die On-Chip-Caches zu verwalten. Diese Befehle haben im Befehlssatz von Java keinen Platz, sie hängen von der Beschaffenheit der Hardware ab. Außerdem sollte picoJava auch mit Code umgehen können der nicht in Java geschrieben wurde wie C oder C++. Im Befehlssatz der JVM fehlen somit Befehle, die für einen real existierenden Prozessor unabdingbar sind. Da man den JVM Befehlssatz nicht einfach verändern kann, ohne damit auch Java zu verändern, hat man in picoJava den Java Befehlssatz erweitert. erweiterte Bytecodes Die folgende Tabelle fasst 115 Befehle, um die der Befehlssatz der JVM erweitert worden ist, in fünf funktionalen Gruppen zusammen: Befehlstyp Gesamtzahl Diagnose Lesen/Schreiben von Registern Beliebiges Laden/Speichern Unterstützung anderer Programmiersprachen System Software Unterstützung Gesamtzahl Anteil (in %) 8 49 35 6 Anzahl von Befehlen mit der Länge: 1 Byte 2 Bytes 3 Bytes 8 49 26 9 5 1 17 115 100 2 2 2 10 98 85 5 15 13 Für die technische Realisierung dieser Erweiterung stellte sich als Problem: 3 JSM – Java Security Model Mit einem Opcode von 8 Bit wie ihn die JVM verwendet, können maximal 28=256 Instruktionen codiert werden. Die Basis Instructions belegen 226 Instruktionen, also ist für weitere 30 Befehle noch Platz. picoJava benötigt allerdings 115 weitere Befehle. Dieses Problem wird durch so genannte Escape Bytes gelöst. Beim Design der JVM wurden zwei ByteCodes für implementations-spezielle Angelegenheiten reserviert. Diese Bytecodes wurden als EscapeBytes definiert. Stößt die Dekodierlogik beim Dekodieren eines Befehls auf einen dieser EscapeByte Codes, so weiß sie, dass es sich bei diesem Befehl um einen erweiterten Befehl handelt da diese Bytekombination von der JVM nicht benutzt wird. Die Dekodierlogik schaut dann auf das zweite Byte im Befehl, um herauszufinden um welchen Befehl es sich handelt, bzw. was dieser Befehl macht. Fast alle erweiterten Befehle beginnen mit einem EscapeByte Diese 341 Instruktionen, die 226 Basis-, und 115 erweiterten Befehle ergeben den kompletten Befehlssatz von picoJava. Der Befehlssatz von picoJava kann nun mehr als der JVM Befehlssatz. picoJava kann Hardware kontrollieren, kann auf Speicher zugreifen und es kann effizient Code ausführen, der nicht in Java geschrieben wurde. Dadurch ist der Befehlssatz von picoJava nicht mehr länger der Befehlssatz einer Virtual Machine, es ist der komplette Befehlssatz einer reellen Maschine. Per Definition kann kein Programm, das in Java geschrieben und auf herkömmlichem Wege in Bytecode kompiliert wurde, erweiterte Bytecodes enthalten. Oder anders: Ein Programm, das erweiterte Bytecodes enthält, kann kein normal kompiliertes Java Programm sein. Stattdessen sind die erweiterten Bytecodes von picoJava für Programme da, die nicht in Java, sondern z.B. in C oder C++, geschrieben wurden und auf picoJava portiert werden sollen. Da nur Plattformen, die auf picoJava basieren die erweiterten Bytecodes verstehen, läuft ein Programm, das erweiterte Bytecodes enthält nicht auf anderen Plattformen. Und da einige der erweiterten Bytecodes Programme ermöglichen, die direkten Zugriff auf den Speicher haben – und damit das JSM unterlaufen – kann kein Programm, das erweiterte Bytecodes enthält ( im Sinne des JSM ) als sicher bezeichnet werden. Nicht-Java-Code, der auf einem Java-Prozessor läuft, verhält sich also genau wie Nicht-Java-Code, der auf einem anderen Prozessor läuft, er ist plattformabhängig und nicht sicher. Ein RISC-Prozessor, hat ca. 100 Befehle. Der Befehlssatz von picoJava hat mit seinen 341 Befehlen mehr als drei Mal so viele Befehle. Um mit dieser Menge an Befehlen effektiv arbeiten zu können, wurden die Befehle in drei Gruppen eingeteilt. Gruppierung nach Komplexität picoJava ist speziell für den Einsatz in eingebetteten Systemen gedacht, und soll deshalb klein und preiswert sein. Dieses Ziel steht in Konflikt mit dem großen Befehlssatz von picoJava. picoJava's Designer lösten dieses Problem, indem sie die Instructions nach der 'Schwierigkeit die Befehle zu implementieren' in drei Gruppen einteilten: einfache, relativ schwierige und sehr schwierige Befehle. Einfache Instruktionen: In diese Kategorie fallen die meisten Befehle der JVM und auch die meisten der erweiterten Befehle. Die Befehle dieser Kategorie sind RISC-ähnlich in dem Sinne, dass sie 'hardwired' –hart verdrahtet sind. Sie werden in nur einem Taktzyklus ausgeführt. Beispiele für solche Instruktionen sind „quick load“ von Objektfeldern und alle Integer Arithmetikoperationen. Gruppe 'einfach (RISC)' Kürzel Operanden iadd Opcode Funktion 0x60 Ganzzahl Addition iload 8 bit O_set 0x15 lade lokale Variable(Ganzzahl) fload 8 bit O_set 0x17 lade lokale Variable(Flie_komma.) bipush 8 bit Konst. 0x10 lege eine Byte-Konstante ab ifeg 16 bit O_set 0x99 Springe, wenn 0 Relativ schwierige Instruktionen: In diese Kategorie fallen ca. 30 der Befehle der JVM und die restlichen der erweiterten Befehle. In einem typischen Java-Programm kommen wenige Befehle dieser Kategorie vor. Diese Befehle sind eher CISC-ähnlich in dem Sinne, dass sie Komplexität im 'Cisc-Style' handhaben – durch Mikrocode. Vom Hardware -Standpunkt gesehen, sind die Kosten von Microcode relativ gering. Ein kleiner Mikrocode-ROM kann die nötigen Kontrollsignale für diese Instruktionen enthalten. Der picoJava Kern benützt zwei ungefähr 2 Kilobyte große ROMs: einen in der Integer Einheit und einen in der optionalen Floating-Point-Einheit. Mikrocodierte Instruktionen benötigen zwischen 3 (z. B. iaload) und 21 (z.B. invokesuper_quick) Taktzyklen. Sie bieten eine akzeptable Balance zwischen der Notwendigkeit die Hardwareimplementation einfach zu halten und guter Performance. Sehr schwierige Instruktionen In diese Kategorie fallen die letzten 30 Befehle der JVM. Die Instruktionen in dieser Kategorie sind entweder sehr schwierig, oder benötigen Dienste vom Betriebssystem, oder beides. Zum Beispiel treffen auf den Befehl new, der zur Erzeugung neuer Objekte benutzt wird, beide Fälle zu: Er ist relativ schwierig da nachgeschaut werden muss ob die Klasse des neuen Objekts in der Liste der bereits geladenen Klassen existiert und es muss Speicher alloziert werden, was eine Koordination mit dem Betriebssystem und eine gewisse Flexibilität in der Art wie diese Allokation implementiert ist erfordert. Außerdem muss die neue Klasse, wenn sie bisher noch nicht geladen wurde, über ein Netzwerk oder das lokale Filesystem geladen werden. Da die Instruktionen in dieser Kategorie also entweder sehr komplex sind und/oder eine flexible Implementierung (wegen ihrer Abhängigkeit vom Betriebssystem) benötigen, sind sie in Software implementiert. Wenn ein Programm einen dieser Bytecodes erfordert, ruft die CPU einen sog. 'instruction emulation' Trap (IET). Abhängig davon welcher Bytecode die IET ausgelöst hat, ruft der Exception-Handler eine spezielle Softwareroutine auf, welche die getrappte Instruktion durch eine Abfolge von mikrocodierten und hart-verdrahteten Instruktionen abarbeitet. Kürzel Operanden Op-Code Funktion instanceof Klasse 16b. 0xC1 Klassenzugehörigkeit 0xBF Ausnahmebehandlung athrow new Klasse 16b. 0xBB ein neues Objekt newarray atype 8b. 0xBC eine neue Tabelle monitorenter 0xC2 Erwerben der Kontrolle über ein Objekt monitorexit 0xC3 Abgeben der Kontrolle Eine solche Verarbeitung von Instruktionen durch Software ist natürlich sehr langsam: Eine Instruktion dieser Kategorie benötigt mehrere hundert-, bis sogar mehreren tausend Taktzyklen. Allerdings werden diese Instruktionen auch von Interpretern oder dynamischen Compilern anderer Prozessoren durch ähnliche Sequenz kleinerer Routinen ausgeführt. picoJava hat hier also im Vergleich keinen Nachteil. Im Gegenteil, ist picoJava im Vergleich zu anderen Prozessoren bei der Ausführung im Vorteil da die emulations Routinen nicht nur über HardwareTraps sehr schnell erreichbar, sondern auch bereits vorgeladen sind. Befehle dieser Kategorie kommen -im Vergleich zu Befehlen der anderen Kategorien- in einem typischen Java-Programm jedoch nicht oft vor. Stackorganisation Wie bereits beschrieben ist die JVM und damit auch picoJava / der Befehlssatz von picoJava stackbasiert. Dies bedeutet, dass bei vielen Befehlen implizit angenommen werden kann, dass Operanden vom Stack geholt und das Resultat des Befehls wieder auf den Stack gelegt wird. Ein stackbasierter Befehlssatz ermöglicht kleine, sichere und portable Programme, hat aber den Nachteil, dass stackbasiertes Verarbeiten von Befehlen sehr ineffizient ist. Eine Stackmaschine benötigt Zeit (Taktzyklen) um Operanden auf den Stack zu legen, wo Rechenoperationen sie dann verarbeiten können. Nachdem die Rechenoperationen ihr Ergebnis wieder auf den Stack gelegt haben, benötigt die Stackmaschine wieder Zeit, um die Resultate vom Stack zu nehmen und sie abzuspeichern. Eine Registermaschine braucht keine Zeit um Operanden hin und her zu bewegen, da hier direkter Zugriff auf die jeweiligen Register möglich ist. Im Vergleich zu Registermaschinen benötigen Stackmaschinen für die gleiche Anzahl von Berechnungen 30 Prozent mehr Operationen4 (durch die zusätzlichen Stackzugriffe). Beispiel / Vergleich (siehe auch [10]): Registerbasierte Addition Stackbasierte Addition ADD R3, R2, R1 ILOAD_1 ILOAD_2 IADD ISTORE_3 Bei einer registerbasierten RISC-Maschine kann eine Addition in einer einzigen Instruktion ausgeführt werden. In den Registern R1 und R2 sind die beiden Summanden gespeichert, während im Register R3 die Summe gespeichert werden soll. Die Operation kann in einem Taktzyklus ausgeführt werden. Die stackbasierte Addition benötigt vier Instruktionen, die jeweils einen Taktzyklus benötigen. Die ersten beiden Befehle bewegen die Summanden zum Stack. Diese werden mit dem dritten Befehl addiert. Das Resultat wird dann schließlich mit dem vierten Befehl Wie ist dies aufzulösen? picoJava hat bereits einen stackbasierten Befehlssatz, ein registerbasierter Befehlssatz kommt also nicht mehr in Frage. Trotzdem wollte man auf die Effizienz einer Registermaschine nicht verzichten. Die Lösung dieses Problems ist eine Registermaschine, die so organisiert ist, dass sie ein stackbasiertes Verarbeiten der Befehle unterstützt. 4 W. Wulf et al., 'The Design of an Optimizing Compiler', American Elsevier, New York, 1973. picoJava's Stackorganisation { SEQ picoJava's_Stackorganisation \* ARABIC } Die picoJava Registermaschine hat 64 Register, welche genutzt werden um die obersten 64 Einträge des Stacks vorzuhalten, zu cachen. picoJava behandelt diese Register als „circular Buffer“, Ring Puffer, was bedeutet, dass das unterste Element und das oberste Element im Stack benachbart sind. Die jeweilige akt. Grenze ist aus den Stackverwaltungsregistern optop (Top-of-Stack Pointer Register ) , oplim (Minimum Value of Top-of-Stack Register) und sc_bottom ('address of deepest Stack Cache Entry' register) ermittelbar. picoJava hält sich damit auch einen Zeiger auf das oberste Element im Stack. (Register optop) Wenn die Ausführungseinheit Elemente vom Stack holt, schrumpft der Stack und das StackPointer Register wird inkrementiert. Wenn die Ausführungseinheit dagegen Elemente auf den Stack legt, wächst der Stack und das StackPointer Register wird dekrementiert. Wie die meisten Stacks wächst der picoJava Stack von höherwertigen Adressen zu niederwertigeren. Legt man 65 Elemente auf den Stack, überschreibt dies das erste Element im Stack. Hier ist dann der DribbleManager gefordert. Der Stackcache hat drei Lese- und zwei Schreibports. Rechenoperationen können gleichzeitig zwei Operanden lesen und ein Resultat zurück schreiben. Die restlichen Ports (ein Lese- und ein Schreibport) belegt der Dribble Manager, der durch ständige im Hintergrund laufende Spill- und Fill-Operationen 5 den Stackcache mit dem Speicher konsistent hält. High- und Low Watermarks Einträge, die (geändert aber) noch nicht in den Datencache geschrieben wurden, werden als 'dirty' bezeichnet. Überschreitet bei wiederholtem Ablegen von Elementen auf dem Stack die Anzahl der 'dirty' Einträge eine obere Grenze (High-WaterMark), schreibt der Spill-Mechanismus (im Hintergrund) diese Einträge in den Datencache heraus – beginnend mit dem Ältesten. Bereits in den Cache kopierte Werte werden als 'clean' betrachtet. Obwohl der Stackcache nur 64 Einträge hat können - indem clean'e, bereits in den Datencache geschriebene Werte überschrieben werden - viel mehr Elemente auf dem Stack liegen. Wenn dagegen der Stack durch mehrmaliges Auslesen von Daten schrumpft und unter die untere Grenze (Low-Water-Mark) fällt, holt der Fill-Mechanismus des Dribbe-Managers (im Hintergrund) Einträge aus dem Datencache und füllt den Stack damit wieder auf. Die Spill- und Fill-Mechanismen erwecken so den Eindruck, dass der Stackcache eine unendliche Anzahl von vollen Registern hat, die belesen und beschrieben werden können. . Instruction Folding Zusammenfassen von Befehlen Eine wichtiges Merkmal des picoJava Caches ist, eine Lösung für das klassische Problem des ineffektiven Stackzugriffs anzubieten (gegenüber Registermaschine) Die Ursache für den Performanceverlust liegt in dem 'Operanden und Ergebnisse' vom Stack holen bzw. auf den Stack schreiben. Hier setzt das InstructionFolding an: 5 Spill – schreibt Einträge vom Register File in den Datencache un schafft so Platz für neue Werte Fill – liest Einträge vom Datencache in das Register File und sorgt so für die nächsten Berechnungen vor Da der Stack in Wirklichkeit ein Register File mit Random Access ist, hat picoJava's Pipeline nicht nur Zugriff auf die obersten zwei, sondern Zugriff auf die obersten 64 Stackelemente, was das Instruction Folding – das Zusammenfassen von Instruktionen – ermöglicht. Register: Quelle und Ziel in EINEM Befehl angegeben, Stack: für das gleiche Ergebnis wird Sequenz aus mehreren Befehlen benötigt In Wirklichkeit liegen die beiden benötigten Operanden bereits auf dem Stack (im Parameterund lokale Variablen-Bereich der akt. Methode). Das Problem ist, daß die ExecutionUnit jeweils einen Takt benötigt um sie an die Spitze des Stacks zu transportieren, Ebenso schreibt der 'localStore' Befehl das Ergebnis nur an eine andere Stelle des Stacks. Solange also alle Bewegungen innerhalb der obersten 64 Elemente des Stacks stattfinden geschieht dies komplett innerhalb des Stacks. Daher ist es möglich diese verschiedenen seriellen Befehle in einer einzelnen, RISC-artigen Operation zu kombinieren. Datenbewegungen innerhalb dieses Bereichs bringen keinen Vorteil hinsichtlich Performance, da in Wirklichkeit ein wahlfreier Zugriff auf die obersten 64 Stack Plätze bereits möglich ist. Die IntegerUnit kann auf Operanden im lokalen Bereich der aktuellen Methode über die beiden lese-Ports des StackCaches direkt zugreifen. Genauso kann sie Ergebnisse in den lokalen Bereich über den Schreib Port des StackCaches direkt zurückschreiben picoJava geht also folgendermaßen vor: Basierend auf folgenden Gruppierungsregeln scannt picoJava's Instruction Decoder den eingehenden Bytecode-Strom und untersucht ihn nach Befehls-Sequenzen welche zu einer einzelnen Instruktion zusammengefasst werden können. Diese 'faltbaren' Sequenzen können innerhalb von bis zu 4 Bytecode Instructions − lokale Daten direkt vor den Rechenoperationen welche diese Daten nutzen oben auf den Stack bewegen und/oder − Berechnungen direkt gefolgt vom lokalen Speichern der Berechnungsresultate enthalten. Wenn solch eine Sequenz gefunden wird, wird daraus eine einzelne, Register-basierte RISCartige Operation erzeugt, in welcher die − auszuführende Rechenoperation und − ihre Operanden, sprich o die Adresse der lokalen zu ladenden Variablen (Quellregister) und o die Adresse der lokal zu Speichernden Variablen (Zielregister) kombiniert werden. Einfacher gesagt kombiniert picoJava die in Stackarchitekturen für eine Rechenoperation nötigen zusätzlichen Stackmanipulationen mit der Rechenoperation selbst. Dies vermindert den rechentechnischen Overhead einer Stackmaschine für die 'gefalteten' Operationen drastisch. Es wird eine 'Single Cycle' Execution erreicht und damit die, von der Risc Architektur bekannte Effektivität bei der Ausführung, da sich unter der Stackarchitektur eine Risc ähnliche Architektur komplett mit einem 64 Einträge großen Register-File und '3 Operanden, register-basierten' Operationen befindet. Beispiel: Instruction folding Das, aus dem Abschnitt 'Stackorganisation' bereits bekannte Beispiel : ILOAD_1 ILOAD_2 IADD ISTORE_3 iload_1 und iload_2 bewegen lokale Daten auf den Stack. Darauf folgt direkt die Rechenoperation iadd, die nur die bewegten Daten benutzt. Dies ist Gruppierungsregel 1. Mit istore_3 wird das Resultat aus iadd lokal abgespeichert. Dies ist Gruppierungsregel 2. Damit können diese vier Befehle zu einem Befehl der Art ADD R3, R2, R1 zusammengefasst werden. R1 ist der, mit iload_1 auf den Stack bewegte, erste Summand, R2 ist der, mit iload_2 bewegte, zweite Summand und R3 ist das Register, zu der istore_3 die Daten bewegt. Der zusammengefasste Befehl benötigt statt vier nur noch einen Taktzyklus. Hardwareunterstützung der Laufzeitanforderungen Um die Ausführung von Java Programmen zu beschleunigen, wurde beim Entwurf von picoJava darauf geachtet, dass bestimmte Laufzeit-Anforderungen von Java Programmen, wie zum Beispiel die Thread-Verwaltung, das Object-Handling, und die Garbage Collection, durch die Hardware unterstützt werden. Dies soll für den Garbage Collection Mechanismus näher untersucht werden Garbage Collection Wie erwähnt, fordert das JSM, dass nicht ersichtlich sei wo Objekte im Speicher liegen. Ein Java Programm kann nur über Objektreferenzen 6 , die keine Rückschlüsse auf die tatsächliche Position der Objekte zulassen, auf den Speicher zuzugreifen. Ein Java Programm kann also selbst keine Speicherverwaltung durchführen, diese Aufgabe übernimmt die JVM: Sie verwaltet den Speicher eines Programms automatisch, indem sie dynamisch Speicherbereiche auf dem Heap 6 Datentypen der Java VM: byte, short, int, long, float, double, char, object, returnAddress alloziert, wenn er benötigt wird und ihn wieder frei gibt, wenn er nicht mehr benötigt wird. Der von der JVM verwendete Mechanismus zur Speicherbereinigung ist Garbage Collection. Die JVM hat einen HEAP Bereich, in dem Objektinstanzen, Klassen u. Interfaces liegen (s.o. [{ REF _Ref68058838 \h \* MERGEFORMAT }]). Heap ist der einzige Speicherbereich, der durch automatische Speicherbereinigung (GC) verwaltet wird. Neben der Bytecode-Ausführung ist der andere entscheidende Aspekt für die Performance einer VM die Garbage Collection. Weder in der Java Sprachspezifikation noch in der Java VM Spezifikation sind jedoch genaue Vorgaben für die Garbage Collection zu finden. Es steht den Herstellern somit größtenteils frei, wie sie die Speicherverwaltung implementieren. picoJava's Garbage Collection Mechanismus ist ein Zusammenspiel der folgenden Verfahren Mark & Sweep Eine der einfachsten Formen der Garbage Collection ist der so genannte Mark & SweepAlgorithmus. Bei diesem Verfahren wird ein Speicherbereich periodisch nach Objekten durchsucht, die von laufenden Programmen aus noch erreicht werden können. Die so gefundenen Objekte werden mit einem Bit markiert. Wenn alle noch erreichbaren Objekte markiert wurden, kann vom restlichen Speicher – in dem evtl. noch nicht erreichbare Objekte liegen – angenommen werden, dass dieser frei ist. Der Garbage Collector geht dann den Speicher durch, merkt sich allen freien Speicher und fügt ihn wieder zum „freier Speicher Pool“ hinzu. Um eine Fragmentierung des Speichers zu verhindern ist der letzte Schritt dieses Verfahrens, dass der freie Speicher und auch die noch referenzierten Objekte zu gemeinsamen Blöcken zusammengeführt werden. Während dieses Verfahrens (Aufräumen & Zusammenführen) befindet sich das System in einem instabilen Zustand, da, wenn der Prozess einmal begonnen hat er nicht unterbrochen werden darf. Dies führt zu merklichen Performanceeinbußen. Zum Markieren der Daten (referenziert/nicht referenziert) werde die, in den Datentypen Zeiger/Reference und Object für Software Zwecke reservierte Bits genutzt (Bits 30, 31) Diese gibt’s es sowohl im Typ Zeiger (Reference) als auch im Header eines Objekts für Softwarezwecke. Handles Das Verschieben von Objekten im Speicher wird durch sog. Handles unterstützt. Es wird zwischen direkten Referezen -Zeiger- und indirekten Referenzen –Handles– unterschieden. Dies trifft eine Aussage über die Art der Objektspeicherung. Der 32 bit große Typ Reference (Zeiger) nutzt Bit 0 um zwischen Handle und 'nicht Handle'zu unterscheiden. Ist der Wert von Bit 0=0 handelt es sich um eine direkte Referenz, ist er 1 um eine indirekte. Objektspeicherung An der Adresse auf die ein Zeiger verweist beginnt das Objekt im Speicher. An dieser Stelle liegt der Objekt Header, der Methoden Vektor o.ä. enthält. Ist ein Objekt nun in 'herkömmlicher' Weise im Speicher abgelegt liegen seine weiteren Daten direkt hinter dem Header. Zeiger auf solche Objekte nennen sich direkte Zeiger- das Handle-Bit ist nicht gesetzt. Ist das Handle-Bit einer Referenz allerdings gesetzt, so weist das auf die zweite Form der Objektspeicher hin. Hier liegt direkt hinter dem Objekt Header ein sog. Handle, ein weiterer Zeiger der auf die Adresse verweist an der der Rest des Objekts abgelegt ist. Diese Form hat den Vorteil, daß wenn das Objekt im Speicher verschoben wird, nur die Handles und nicht alle vereisenden Zeiger geändert werden müssen. Sie bieten also einen einfachen Mechanismus für den GarbageCollector Objekte im Speicher zu verschieben, haben allerdings auch Nachteile. Die Nutzung von Handle benötigt zusätzlichen Speicher und Ausführungszeit. Jede Instruction die auf ein Objekt zugreift benötigt zusätzlichen Aufwand von mindestens 2 Taktzyklen um die Umleitung, die Indirektion zu handhaben. Ausserdem werden bei der Speicherung per Handle für jedes gespeicherte Objekt ein zusätzliches Word Speicher für den 'Object Storage Pointer' benötigt. Generational Garbage Collection Bei der Generational Garbage Collection (GGC) werden die Speicherscans des Mark & Sweep auf kleinere Speicherbereiche beschränkt. Dies erhöht die Performance der Garbage Collection, und damit auch deren Einsetzbarkeit in Echtzeit-Umgebungen. Generationen-Kollektoren gehen davon aus, dass einige Objekte länger leben als andere. Weiterhin wird angenommen, dass die meisten Objekte jung sterben. Dementsprechend wird der Heap in mehrere Überlebensräume – Generationen – unterteilt. Neue Objekte werden grundsätzlich in der jüngsten Generation – Eden- alloziert. Ist kein Platz mehr in Eden, greift ein kopierender Kollektor, scannt diesen Überlebensraum und verschiebt lebendige Objekte die eine bestimmte Anzahl von Aufräumphasen überlebt haben in den nächsten Überlebensraum (dabei müssen alle Zeiger auf das Objekt entsprechend angepasst werden). Räume für jüngere Generationen sind kleiner. Wenn ein Bereich ein neues Objekt nicht mehr aufnehmen kann wird er gescannt und per mark & sweep aufgeräumt. Dies führt zu einer höheren Scanfrequenz. Dieser erste Überlebensraum wird mit hoher Frequenz nach nicht mehr referenzierten Objekten gescannt, wodurch bereits die meisten „toten“ Objekte entfernt werden können. Der nächste Überlebensraum wird seltener gescannt. Objekte, die auch hier die Inkubationszeit überleben, werden wieder in den Bereich für die nächste Generation verschoben und auch hier nimmt die Frequenz der Scans wieder ab. Dieses Verfahren wird bis zur n-ten Inkubationsstufe fortgesetzt, wobei jeweils die Scanfrequenz abnimmt. Objekte die auch die n-te Inkubationszeit überleben, werden in den restlichen Speicher, die permanente Generation verschoben. Auch beim GGC-Verfahren muss ab und zu der gesamte Speicher nach nicht mehr benötigten Objekten durchsucht werden, da die Zahl der langlebigen Objekte mit der Zeit zunimmt und die Gefahr besteht, dass der Speicher aufgebraucht wird. Dennoch wird die Zahl dieser zeitintensiven komplett-Scans zur Ausnahme. Generationen optimieren also die Speicherbereinigung insofern, als dass nicht immer der ganze Speicher aufgeräumt wird, sondern nur der Teil, der gerade Vollgelaufen ist. Die Pausen, die auftreten wenn das Programm zur Speicherbereinigung gestoppt werden muss, sind somit geringer als bei nur einem großen zusammenhängenden Speicherbereich, der immer komplett gesäubert werden muss. Dabei macht man sich zunutze, dass ein kleines Speichersegment gewöhnlich sehr schnell mit Objekten belegt ist, die zu einem großen Teil direkt wieder aus dem Speicher entfernt werden können. Es gibt noch weitere Garbage Collection Verfahren, von denen die meisten auch auf einer Segmentierung des Speichers beruhen. picoJava unterstützt daher die Segmentierung von Speicher hardwareseitig durch sog. Schreibbarrieren. Write Barriers Um einen bestimmtes Speichersegment zügig nach Objekten scannen zu können auf die noch Verweise existieren, muss der GC ein Liste mit allen Objekten in diesem Segment , die von außerhalb des Segments erreichbar sind, vorhalten. Ohne eine solche Liste müsste der GC den Rest des Speicher scannen, um herauszufinden welches externe (extern des zu scannenden Segments) Objekt noch Referenzen auf interne Objekte hält (das somit ja nicht entfernt werden darf). Der primäre Mechanismus um eine solche Liste zu erstellen sind sog. 'Write Barriers'. Der Schreib-Barriere Mechnismus ermöglicht es, Zeiger von Objekten die sich ausserhalb des zu scannenden Bereichs befinden, in sog. 'Stores' zu listen um sie effizient daraufhin zu untersuchen, ob sie auf Objekte innerhalb des zu scannenden Segments verweisen. In Generationen Systemen werden diese Zeiger als 'inter generational pointers' bezeichnet. Die 'Write BArrier' ist ein Mechanismus, welcher den GC benachrichtigt sobald Objekt Referenzen im Speicher abgelegt werden. Der GC kann diese Information dann nutzen um freien Speicher zu identifizieren. Der GC wird vom 'Core' jeweils via gc_notify' trap informiert woraufhin dann der Traphandler entsprechend dem genutzten GC Algorithmus in Aktion tritt. Die 'Write-Barrier Trap' wird ausgelöst sobald eine Referenz irgendeines Objekts oder Arrays beschrieben wird. Die genauen Bedingungen wird durch die Flags im 'PSP_GCE' und GC_CONFIG' Register konfiguriert. Hier können zwei Arten des 'Write Barier' Mechanismus konfiguriert werden [6a] − page based write Barrier Hier wird die relative Speicherlokalisierung durch Object-Referenzen genutzt − reference based write Barrier Hier werden die 2 Bit des reservierten GC_TAG Flags einer Referenz genutzt um zu entscheiden, wann eine 'gc_notify' Trap ausgelöst wird. Segmentierung picoJava bietet eine flexible Methode um Segment Grenzen zu definieren. Ist ein Segment einmal festgelegt, werden als nächstes alle Zeiger in den 'Zeiger -Stores' daraufhin geprüft, ob einer der Pointer auf ein Objekt in einem, von sich selbst verschiedenen Segment verweist. Ist das der Fall so wird eine Unterbrechung, ein 'Trap' ausgelöst 7 und der GC kann entsprechend reagieren, was hier bedeutet die Referenz in seine entsprechende Liste aufzunehmen. Der TrapHandler kann abhängig vom genutzten GC Algorithmus verschieden reagieren. Die Segmentierung erfolgt durch Maskierung des Registers GC_Config in welchem die Segment Größen festgelegt werden. Die Bestimmung,zu welchem Segment ein Zeiger oder Objekt gehört, geschieht durch Abgleich des Zeigers/Objekts mit dem Register Benchmark Leider sind kaum Benchmarks zu picoJava zu finden. Die Abbildung zeigt picoJava im Vergleich mit einem 80486- und einem Pentium-Prozessor. Es wurden javac, der Sun Java Compiler, und ein nicht näher bestimmter Raytracer jeweils auf dem Pentium und dem 486er mit einem Interpreter und einem JIT-Compiler getestet. Wie man sieht schneidet picoJava 15 bis 20 mal schneller als der 486 mit Interpreter bzw. 5 mal schneller als der Pentium mit einem JIT Compiler ab. 7 gc_notify(type = 0x27) Dies Testergebnis ist jedoch sehr fraglich, da javac und der nicht weiter spezifizierte Raytracer vielleicht extra gewählt wurden, weil sie besonders gut auf picoJava verarbeitet werden können. Zusammenfassung Java biete durch seine Objektorientierung und das Prinzip der kompilierung in Java-Bytecode viele Vorteile. Beim Einsatz von Java in eingebetteten Echtzeitsystemen zeigt sich jedoch, dass interpretierter Bytecode in der Regel zu langsam ist. Diese Probleme sollen durch einen Java Prozessor gelöst werden Der vorgestellte, von Sun spezifizierte Prozessor-Kern picoJava wurde von mehreren großen Firmen lizenziert, ein picoJava-Boom blieb jedoch aus. Keiner der Lizenznehmer hat jemals picoJava-basierte Chips verkauft 8 . Bereits zum Erscheinen der Spezifikation wurde der Ansatz kritisch beäugt. Es waren bereits Spezial-Prozessoren für andere Sprachen wie LISP und Smalltalk entwickelt worden, und man hatte gesehen, dass Software-Implementierungen auf RISC-Chips bessere Performance boten. Man zweifelte daran, dass Suns picoJava besser performte. Und tatsächlich stellte sich später heraus, dass picoJava weder schnell noch billig noch sparsam genug war, um im Markt für Mobiltelefone und PDAs mithalten zu können. Stattdessen wurde ein etwas anderer Ansatz für Kleingeräte populär: Java-Beschleuniger. Dabei handelt es sich um Bausteine, die ähnlich wie Koprozessoren zusätzlich zum Hauptprozessor verwendet werden können. So lässt sich beispielsweise Nazomis [9] bereits erwähnter JavaKoprozessor in bestehende Designs einbinden und erleichtert so Kleingeräte-Herstellern die Verwendung von Java unter Beibehaltung einer bereits vorhandenen Architektur. Einen anderen Weg ging die Firma ARM [8]. ARM hat seinen Chips den Java-VM-Befehlssatz schlicht als dritten Befehlssatz hinzugefügt. Ein einfaches Umschalten macht so aus dem herkömmlichen ARM-Chip eine Java VM. 8 deswegen gibt es keinen Abschnitt "Implementierungen" Quellen 9 [1] SUN MICROELECTRONICS picoJava Microprocessor Cores http://www.sun.com/microelectronics/picoJava/ (als Kopie auf CD beiliegend) [2] the JavaTM Virtual Machine Specification, Second Edition http://java.sun.com/docs/books/vmspec/ (als Kopie auf CD beiliegend) [3] Harlan McGhan: "picoJava: a direct Execution Engine for Java Bytecode" http://www.ece.purdue.edu/~arch/seminar/schedules/spring00-pdf/mcghan98.pdf (als Kopie auf CD beiliegend) [4] Inside the Java Virtual Machine http://www.artima.com/insidejvm/ed2/ (als Kopie auf CD beiliegend) [5] Sun Gambles on Java Chips http://www.byte.com/art/9611/sec6/art2.htm#116st3d2 (als Kopie auf CD beiliegend) [6] picoJava2 ProzessorCore Description https://spacejug.dev.java.net/source/browse/ spacejug/www/resources/Embedded_Java/picoJava/picoJava-II.pdf (als Kopie auf CD beiliegend) [6a] 'picoJava-II Programmers Reference Manual' (als Kopie auf CD beiliegend) [6b] 'picoJava-II' DataSheet (als Kopie auf CD beiliegend) [7] Suns picoJava: http://www.sun.com/microelectronics/picoJava/ (als Kopie auf CD beiliegend) [8] ARM: http://www.arm.com/ [8a] ARM Jazelle Technology http://www.arm.com/products/esd/jazelle_home.html [9] Nazomi: http://www.nazomi.com/ 9 Da sich Internet Adressen schnell ändern liegen die Quellen soweit möglich, insbesondere aber die ohne Angabe einer URL ([6a], [6b]) auf CD als Kopie bei. [10] Script zur Vorlesung 'Rechnerstrukturen' von Prof. Dr. Th. Risse http://www.weblearn.hs-bremen.de/risse/RST/docs/RST.pdf und für Interssierte: andere Referate zum Thema Java-Prozessoren: Bartosz Tyrakowski: 'K-Virtual Machine' http://www.weblearn.hs-bremen.de/risse/RST/WS02/KVM/KVM.htm Mathias Pohl: Java Prozesoren http://www.weblearn.hs-bremen.de/risse/RST/WS02/javaproc.pdf Markus Müller: Sicherheitsfeatures von Chip-Karten, am Beispiel der JavaCard http://www.weblearn.hs-bremen.de/risse/RST/ws99/JAVACARD/javacard.htm Krzysztof Jedon: JAVA http://www.weblearn.hs-bremen.de/risse/RST/SS98/JAVA/Jedon/Bericht.html Herbert Stehr, Gordon Stöver : Die Java-Maschine als Soft- und HardwareImplementation http://www.weblearn.hs-bremen.de/risse/RST/SS97/JavaProc/JAVAPROC.HTM Roland Klein, Kürsad Afacan: Java Virtual Machine http://www.weblearn.hs-bremen.de/risse/RST/SS96/Java_VM/Java_VM_.htm