Anwendung von Programm-Analyse zur Optimierung des Ladeprozesses von Java-Programmen Diplomarbeit vorgelegt bei Herrn Prof. Dr. Uwe Kastens Jens Siebert Fakultät für Elektrotechnik, Informatik und Mathematik der Universität Paderborn Paderborn, Dezember 2006 Danksagung Zunächst gilt mein Dank Prof. Dr. Uwe Kastens, der diese Arbeit ermöglichte und konstruktive Ratschläge beigesteuert hat. Insbesondere möchte ich mich bei Dipl. Inform. Karsten Klohs und Dr. Michael Thies bedanken, die mir während der gesamten Arbeit mit wertvollen Hinweisen und technischer Hilfe zur Seite standen. Ganz besonders bedanke ich mich für das geduldige Lesen und die Kommentare, die die Qualität dieser Arbeit enorm gesteigert haben. Eidesstattliche Erklärung Ich versichere, dass ich die vorliegende Arbeit selbstständig und ohne unerlaubte fremde Hilfe sowie ohne Benutzung anderer als den angegebenen Quellen angefertigt habe. Alle Ausführungen, die wörtlich oder sinngemäÿ übernommen wurden, sind als solche gekennzeichnet. Die Arbeit hat in gleicher oder ähnlicher Form noch keiner anderen Prüfungsbehörde vorgelegen. Ort, Datum Unterschrift Inhaltsverzeichnis 1 Einleitung 1 2 Grundlagen 2.1 2.2 2.3 2.4 3 Die Java-Laufzeitumgebung . . . . . . . . . . . . . . . . . . . . 2.1.1 Komponenten einer Java-Laufzeitumgebung . . . . . . . 2.1.2 Java-Klassendateien . . . . . . . . . . . . . . . . . . . . Der Ladeprozess von Java-Programmen . . . . . . . . . . . . . Statische Programm-Analyse . . . . . . . . . . . . . . . . . . . 2.3.1 Strukturen der Programm-Analyse . . . . . . . . . . . . 2.3.2 Datenuss-Analyse . . . . . . . . . . . . . . . . . . . . . 2.3.3 Aufrufgraph-Analyse . . . . . . . . . . . . . . . . . . . . 2.3.4 Aufrufgraph-Analyse in objektorientierten Programmen Arbeiten zur Optimierung des Ladeprozesses . . . . . . . . . . 2.4.1 ZIP- und JAR-Dateiformat . . . . . . . . . . . . . . . . 2.4.2 Komprimierung nach Pugh . . . . . . . . . . . . . . . . 2.4.3 JAZZ-Dateiformat . . . . . . . . . . . . . . . . . . . . . 2.4.4 Jikes Application Extractor (JAX) . . . . . . . . . . . . 2.4.5 Optimierungen zur Ladezeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Konzept 3.1 3.2 3 4 5 8 10 10 11 13 14 16 16 17 17 17 18 21 Faltung von Konstanten-Pools . . . . . . . . . . . . . . . . . . . . 3.1.1 Einführung eines globalen Konstanten-Pools . . . . . . . . 3.1.2 Konstruktion des globalen Konstanten-Pools . . . . . . . 3.1.3 Speicherung der Analyse-Informationen . . . . . . . . . . 3.1.4 Optimierung des Transportformats . . . . . . . . . . . . . 3.1.5 Bildung der Teilmengen über die Anzahl der Zugrie . . . 3.1.6 Bildung der Teilmengen über die durchschnittliche Anzahl von Zugrien . . . . . . . . . . . . . . . . . . . . . . . . . Ermittlung sicher geladener Klassen . . . . . . . . . . . . . . . . 3.2.1 Denition sicher geladener Klassen . . . . . . . . . . . . . 3.2.2 Analyse zur Ermittlung sicher geladener Klassen . . . . . 3.2.3 Ermittlung sicher aufgerufener Methoden . . . . . . . . . 3.2.4 Ermittlung sicher erreichter Methoden . . . . . . . . . . . 3.2.5 Bildung der Ergebnismenge . . . . . . . . . . . . . . . . . i 21 22 23 24 25 26 27 28 28 29 31 32 32 ii INHALTSVERZEICHNIS 4 Implementierung 4.1 4.2 Implementierung der Analysen . . . . . . . . . . . . . . . . . . . 4.1.1 Implementierung der Faltung von Konstanten-Pools . . . 4.1.2 Implementierung der Ermittlung sicher geladener Klassen Aufbau des Mapping-Attributs . . . . . . . . . . . . . . . . . . . 5 Evaluation 5.1 5.2 5.3 Faltung von Konstanten-Pools . . . . . . . . . . . . . . . . . Optimierung des Transportformates . . . . . . . . . . . . . 5.2.1 Optimierung über die Anzahl von Zugrien . . . . . 5.2.2 Optimierung über die durchschnittliche Anzahl von grien . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2.3 Vergleich der Verfahren . . . . . . . . . . . . . . . . Sicher geladene Klassen . . . . . . . . . . . . . . . . . . . . 6 Zusammenfassung . . . . . . . . . Zu. . . . . . . . . 35 36 36 38 39 43 44 44 46 48 48 51 55 Kapitel 1 Einleitung Der Ladeprozess von Programmen in einer virtuellen Maschine für Java stellt eine kritische Komponente im Ablauf von Java-Programmen dar. Durch bestimmte Eigenschaften der binären Repräsentation von Java-Programmen und der Art wie sie im Programmlauf geladen werden, ergeben sich innerhalb des Ladeprozesses Optimierungspotentiale. Eine dieser Eigenschaften ist die Art, wie die Konstanten-Pools der Klassendateien geladen und im internen Speicher der virtuellen Maschine abgelegt werden. Die Daten aus diesen Konstanten-Pools werden bisher zu ihrer Klasse zugehörig im Speicher abgelegt. Dadurch kann es zur redundanten Speicherung von Informationen und damit zu einer Verschwendung von Speicher kommen. Insbesondere in Geräten mit sehr wenig Ressourcen ist dies jedoch nicht akzeptabel. Eine weitere Eigenschaft des Ladeprozesses ist das dynamische Nachladen von Klassen zur Laufzeit des Programms. Bei dieser Strategie werden die Klassen erst dann geladen, wenn sie im Programmlauf benötigt werden. Dadurch soll der unnötige Verbrauch von Speicher durch nicht benötigte aber geladene Klassen verhindert werden. Beim Nachladen von Klassen wird der Programmlauf jedoch für kurze Zeit unterbrochen, da verschiedene Schritte im Rahmen des Ladeprozesses durchgeführt werden müssen. Wiederum stellen hier Geräte mit wenig Ressourcen ein Problem dar. Bei solchen Geräten würde sich eine Unterbrechung deutlich stärker bemerkbar machen, als bei schnellen Geräten mit ausreichenden Ressourcen. In dieser Arbeit werden zwei Verfahren vorgestellt, welche die beschriebenen Probleme umgehen. Bei dem ersten Verfahren handelt es sich um die Faltung der Konstanten-Pools von Klassendateien in einer Ladeeinheit. Hierbei werden die Konsdasstanten-Pools der einzelnen Klassendateien so zusammengefasst, das Einträge mit identischem Inhalt lediglich einmal im Speicher der virtuellen Maschine abgelegt werden müssen. Hierzu wird eine neue Datenstruktur, der globale Konstanten-Pool, sowie ein Verfahren zur Faltung der Konstanten-Pools vorgestellt. Das zweite vorgestellte Verfahren dient der Ermittlung sicher geladener Klassen eines Programms. Eine Klasse gilt als sicher geladen, wenn sie in jedem möglichen Ablauf eines Programms geladen wird. Das Verfahren untersucht ein Java-Programm mit Hilfe statischer Programm-Analysen und ermittelt eine Menge solcher Klassen. Diese können dann direkt beim Start der virtuellen 1 2 KAPITEL 1. EINLEITUNG Maschine geladen werden. Dies vermeidet die Unterbrechungen durch das dynamische Nachladen der Klassen zur Laufzeit des Programms. Gleichzeitig wird nicht unnötig Speicher verbraucht, da diese Klassen während des Programmlaufs ohnehin geladen würden. In der vorliegenden Arbeit werden in Kapitel 2 zunächst die Grundlagen zum Ladeprozess von Java-Programmen und der statischen Programm-Analyse dargestellt. Der Ladeprozess wird mit dem Fokus auf die Verarbeitung und Speicherung der Daten aus den Konstanten-Pools und des dynamischen Nachladens von Klassen beschrieben. Anschlieÿend erfolgt eine Einführung in die statische Programm-Analyse. Hier werden unter anderem die Datenuss- und die Aufrufgraph-Analyse vorgestellt. Den Abschluss des Grundlagenkapitels bildet eine Abgrenzung der vorgestellten Verfahren zu bereits existierenden Verfahren. In Kapitel 3 werden schlieÿlich die Konzepte zur Faltung von KonstantenPools und der Ermittlung sicher geladener Klassen vorgestellt. Für das Verfahren zur Faltung von Konstanten-Pools wird die Datenstruktur des globalen Konstanten-Pools, ein Konstruktionsverfahren für einen gefalteten globalen Konstanten-Pool und die Speicherung der gewonnen Analyse-Informationen vorgestellt. Durch die besondere Art der Speicherung der Informationen über den globalen Konstanten-Pool ergibt sich eine Vergröÿerung der Ladeeinheit des optimierten Programms. Da dies nicht für alle Anwendungsfälle akzeptabel ist, werden zwei Optimierungsverfahren vorgestellt. Diese sollen einen guten Kompromiss zwischen der Speicherersparnis durch den globalen Konstanten-Pool und der Vergröÿerung der Ladeeinheit des Programms nden. Für die Beschreibung des Verfahrens zur Ermittlung sicher geladener Klassen werden die einzelnen durchgeführten Programm-Analysen beschrieben. Zur Ermittlung dieser Art von Klassen sind vier Analysen, zwei Datenuss- und zwei Aufrufgraph-Analysen nötig. Diese werden in Abschnitt 3.2 beschrieben. Dort wird gezeigt, wie bestimmte Eigenschaften der Struktur von Programmen ausgenutzt werden, um sicher geladene Klassen zu identizieren. Eine Beschreibung der wichtigsten Implementierungs-Aspekte der vorgestellten Verfahren ndet sich in Kapitel 4. Dort wird kurz auf die Implementierung der Verfahren mit Hilfe der Umgebung zur Programm-Analyse PAULI der Arbeitsgruppe Kastens der Universität Paderborn eingegangen. Auÿerdem wird eine Beschreibung der Struktur zur Speicherung der Analyse-Informationen zur Faltung von Konstanten-Pools gegeben. Diese Information ist für spätere Implementierungen eines modizierten Ladeprozesses von Vorteil. Eine Evaluation der Faltung von Konstanten-Pools und der Ermittlung sicher geladener Klassen ndet sich in Kapitel 5. Hier wurden beide Verfahren auf unterschiedliche Programme angewandt und Messwerte bestimmt. An Hand dieser Werte werden der Nutzen und die Qualität der Verfahren dargestellt. Den Abschluss dieser Arbeit bildet eine Zusammenfassung der Ergebnisse und ein Ausblick auf mögliche zukünftige Entwicklungen in Kapitel 6. Kapitel 2 Grundlagen In diesem Kapitel werden die Grundlagen erläutert, die zum Verständnis der in dieser Arbeit beschriebenen Verfahren nötig sind. Zunächst wird ein kurzer Überblick über die Laufzeitumgebung für Java-Programme gegeben. Da sich diese Arbeit mit der Optimierung des Ladeprozesses von Java-Programmen beschäftigt ist es nötig, zunächst ein generelles Verständnis für die Mechanismen der Laufzeitumgebung zu entwickeln. Weiterhin wird der Aufbau der Klassendateien dargestellt. Hier ist insbesondere der Aufbau der Konstanten-Pools interessant, da diese durch das Verfahren zur Faltung von Konstanten-Pools optimiert werden sollen. Anschlieÿend erfolgt die Konzentration auf den Ladeprozess selbst. Dieser Ladeprozess basiert auf dem Prinzip des dynamischen Nachladens von Teilen des Programms zur Laufzeit. Die Nachteile, die sich durch diese Art des Nachladens von Programmteilen ergeben, soll das Verfahren zur Ermittlung sicher geladener Klassen umgehen. Die Ermittlung sicher geladener Klassen nutzt die statische Programm-Analyse, die in Abschnitt 2.3 eingeführt wird. Zunächst werden die ModellierungsStrukturen der Programm-Analyse vorgestellt. Hierzu gehören unter anderem Grundblöcke, Ablaufgraphen, sowie Aufrufgraphen. Anschlieÿend wird die Datenuss-Analyse und die Analyse von Aufrufgraphen vorgestellt, die zur Gewinnung von Informationen für Optimierungen in dieser Arbeit eingesetzt werden. Den Abschluss dieses Kapitels bildet ein Abschnitt über verwandte Arbeiten, die sich ebenfalls mit der Optimierung des Ladeprozesses der Java-Plattform beschäftigen. Neben einer Vorstellung der Konzepte dieser Arbeiten ndet dort auch eine Abgrenzung zu den Konzepten statt, die in der vorliegenden Arbeit entwickelt werden. 2.1 Die Java-Laufzeitumgebung Die Java-Plattform stellt eine Laufzeitumgebung bereit, in der Java-Programme unabhängig von der unterliegenden Hardware und Systemsoftware ausgeführt werden können. Zu diesem Zweck stellt sie eine virtuelle Maschine zur Verfügung, die ein abstraktes Rechnermodell darstellt. Eine ausführliche Beschreibung der virtuellen Maschine ndet sich in der Java Virtual Machine Specication [JVM98]. Wie in Abbildung 2.1 zu sehen ist, bestehen virtuelle Maschinen aus 3 4 KAPITEL 2. GRUNDLAGEN Virtuelle Maschine Ladeeinheit interner Speicher Klasse A Klasse A KP A Klasse B KP B Klassenlader KP A Klasse B KP B Klasse C KP C Klasse C KP C Ausführungseinheit Abbildung 2.1: Schematische Darstellung des Aufbaus der Laufzeitumgebung für Java-Programme mehreren Komponenten, die bei der Ausführung eines Java-Programms zusammenarbeiten. Die für diese Arbeit wichtigen Komponenten werden im Folgenden erläutert. 2.1.1 Komponenten einer Java-Laufzeitumgebung Um ein Java-Programm ausführen zu können, müssen die Programme von der virtuellen Maschine geladen werden. Dazu dient der Klassenlader. Die Programme liegen in Form einzelner Klassendateien oder einer Ladeeinheit vor. Eine Ladeeinheit fasst mehrere Klassendateien in einem Archiv zusammen. Der Klassenlader liest einzelne Klassendateien und rekonstruiert deren Inhalt, so dass die Struktur der Klasse im internen Speicher der virtuellen Maschine abgelegt und von der Ausführungseinheit verwendet werden kann. Zu den Inhalten einer Klassendatei gehören unter anderem auch die Methoden der Klasse. Sie bestehen hauptsächlich aus sogenannten Bytecode-Instruktionen, die von der Ausführungseinheit ausgeführt werden können. Die Methoden werden vom Klassenlader in einem speziellen Bereich des internen Speichers abgelegt, dem sogenannten Methodenbereich. Der Ausführungseinheit stehen während der Programmausführung mehrere verschiedene Speicherbereiche zur Verfügung, in denen Programmobjekte abgelegt werden können. Im oben erwähnten Methodenbereich werden die Instruktionen der Methoden einer Klasse gespeichert. Zur Ablage von Objekten dient eine Halde. Diese wird während eines Programmlaufs permanent auf nicht mehr verwendete Objekte überwacht. Wird eine Objektinstanz im Programm nicht mehr verwendet, so wird der von ihr belegte Speicherbereich automatisch freigegeben. Diese Überwachungs- und Aufräumfunktion übernimmt der sogenannte 2.1. DIE JAVA-LAUFZEITUMGEBUNG 5 Klassendatei Versionsinformationen Konstanten-Pool Schnittstellen Felder Methoden Attribute Abbildung 2.2: Stark vereinfachte Darstellung des Aufbaus einer JavaKlassendatei Garbage-Collector. Zur Speicherung von Zwischenergebnissen, die während der Abarbeitung eines Java-Programms anfallen, dient ein Aufrufkeller. Da die Java Programmiersprache die Programmierung nebenläuger Programme unterstützt, steht pro Ausführungsstrang eines Programms je ein Aufrufkeller zur Verfügung. 2.1.2 Java-Klassendateien Im Folgenden soll nun verstärkt auf den Aufbau von Java-Klassendateien eingegangen werden. Der Aufbau der Klassendateien ist für das Verständnis der später beschriebenen Konzepte zur Gewinnung und Speicherung von AnalyseInformationen besonders wichtig. Eine Klassendatei enthält die binäre Repräsentation einer Klasse oder Schnittstelle. Ein stark vereinfachter Aufbau ist in Abbildung 2.2 dargestellt. Eine präzise Beschreibung des Aufbaus ndet sich in der Java Virtual Machine Specication [JVM98]. Klassendateien bestehen aus verschiedenen Komponenten. Die wichtigsten Komponenten sind der Konstanten-Pool und die Methoden der Klasse. Der Konstanten-Pool der Klasse enthält eine Menge von Einträgen, die konstante Werte oder symbolische Referenzen repräsentieren (siehe Abbildung 2.3). Diese Einträge können zum Beispiel ganzzahlige Konstanten oder Referenzen auf Felder und Methoden anderer Klassen darstellen. Sie werden von den BytecodeInstruktionen der Methoden der Klasse durch eine Indexnummer referenziert. Während der Ausführung der Instruktion in der Ausführungseinheit der virtuellen Maschine wird diese Referenz aufgelöst. Dies ist in Abbildung 2.4 für die Bytecode-Instruktion getfield dargestellt, die zum Zugri auf Daten eines Feldes dient. Anhand des Indexes, welcher der Instruktion als Parameter übergeben wurde, wird ein entsprechender Feld-Eintrag im Konstanten-Pool referenziert. Dieser Eintrag enthält dann Verweise auf andere Einträge, die Informationen wie 6 KAPITEL 2. GRUNDLAGEN Konstanten-Pool 1 2 ’1024’ ’Hallo Welt’ 3 System.out.print 4 ’42’ 5 ’3.14159265’ 6 ’Java ist toll’ . . n Abbildung 2.3: Darstellung des Aufbaus eines Konstanten-Pools Tag 3 4 5 6 1 Typ CONSTANT_Integer CONSTANT_Float CONSTANT_Long CONSTANT_Double CONSTANT_Utf8 Tiefe 0 0 0 0 0 8 7 12 9 10 11 CONSTANT_String CONSTANT_Class CONSTANT_NameAndType CONSTANT_Fieldref CONSTANT_Methodref CONSTANT_InterfaceMethodref 1 1 1 2 2 2 Tabelle 2.1: Übersicht über die Typen von Einträgen in Konstanten-Pools von Java-Klassendateien den Namen und den Typ des Feldes, auf das zugegrien werden soll, enthalten. In Abbildung 2.5 ist der unterschiedliche Aufbau der Einträge in KonstantenPools dargestellt. Ihr Aufbau ist abhängig von der Information, die sie repräsentieren. Enthält ein solcher Eintrag lediglich einen konstanten Wert, zum Beispiel eine ganzzahlige Konstante, so ist dieser Wert direkt im Eintrag gespeichert. Repräsentiert der Eintrag jedoch einen komplexeren Wert, so verteilen sich die Informationen über mehrere Einträge im Konstanten-Pool. Diese Einträge sind untereinander verknüpft und bilden damit eine Baum-Struktur. Diese Verteilung der Information über mehrere Einträge dient dem Zweck der Wiederverwendung von Informationen. Werden beispielsweise mehrere Methoden einer Klasse referenziert, so sind die Informationen über den Namen dieser Klasse lediglich einmal im Konstanten-Pool gespeichert und werden von den verschiedenen Methodenreferenz-Einträgen verwendet. Den einzelnen Einträgen eines Konstanten-Pools sind bestimmte Typen zugewiesen. Eine Übersicht über die verschiedenen Typen ist in Tabelle 2.1 dar- 2.1. DIE JAVA-LAUFZEITUMGEBUNG #3 getfield #3 BytecodeInstruktion Konstanten-PoolIndex #8 #9 7 tag: CONSTANT_Fieldref class_index #8 name_and_type_index #9 . . tag: CONSTANT_Class name_index #12 tag: CONSTANT_NameAndType name_index #13 descriptor_index #14 . . #12 tag: CONSTANT_Utf8 length: xx info: <UTF8String> #13 tag: CONSTANT_Utf8 length: xx info: <UTF8String> #14 tag: CONSTANT_Utf8 length: xx info: <UTF8String> . . . #n Abbildung 2.4: Zugri einer Bytecode-Instruktion auf einen Eintrag des Konstanten-Pools zusammengesetzter Eintrag (Tiefe 1) einfacher Eintrag CONSTANT_String string_index #5 CONSTANT_Integer ’1024’ CONSTANT_Utf8 ’Hello World’ CONSTANT_Fieldref class_index #6 nameandtype_index #7 CONSTANT_Class name_index #8 CONSTANT_NameAndType name_index #10 zusammengesetzter Eintrag (Tiefe 2) type_index #11 CONSTANT_Utf8 ’HelloWorld’ CONSTANT_Utf8 ’testFeld’ CONSTANT_Utf8 ’I’ Abbildung 2.5: Aufbau einfacher und zusammengesetzter Einträge im Konstanten-Pool 8 KAPITEL 2. GRUNDLAGEN gestellt. Die Speicherung der Einträge im internen Speicher der virtuellen Maschine erfolgt in einer Datenstruktur, die der jeweiligen Klasse zugeordnet wird. Durch diese Art der Speicherung ergibt sich ein Optimierungspotential. Enthalten die Konstanten-Pools zweier unterschiedlicher Klassen gleiche Einträge, so werden diese Einträge in zwei Klassen-Datenstrukturen gespeichert. Da aber der Inhalt der Einträge identisch ist, bedeutet dies eine redundante Speicherung von Informationen und somit eine Speicherverschwendung. Dieser Verschwendung soll mit dem später vorgestellten Verfahren zur Faltung von KonstantenPools entgegengewirkt werden, indem die identischen Einträge in einem globalen Konstanten-Pool zusammengefaltet werden. Aus der Faltung von Konstanten-Pools ergeben sich Analyse-Informationen, die in den Klassendateien gespeichert werden sollen. Hierzu dienen die Attribute in Klassendateien. Attribute stellen Erweiterungspunkte für das Format der Klassendateien dar. Die Java Virtual Machine Specication [JVM98] erlaubt es explizit, dass selbst denierte Attribute in Klassendateien eingefügt werden dürfen. Virtuellen Maschinen, die solche selbst denierten Attribute nicht unterstützen, müssen diese ignorieren und mit der Ausführung des Programms fortfahren. Somit ist es möglich, den Klassenlader einer virtuellen Maschine so zu modizieren, dass er ein selbst deniertes Attribut erkennen und auswerten kann. Diese Tatsache wird in dieser Arbeit genutzt, um Analyse-Informationen an eine virtuelle Maschine zu übermitteln. Diese Analyse-Informationen werden in einer Vorverarbeitung einer Ladeeinheit gewonnen. 2.2 Der Ladeprozess von Java-Programmen In diesem Abschnitt soll nun der Ladeprozess für Java-Programme betrachtet werden. Insbesondere soll dabei auf das Prinzip des dynamischen Ladens von Klassen eingegangen werden. Wie bereits in den vorherigen Abschnitten dargestellt wurde, besteht ein Java-Programm aus mehreren Klassen, die in Form einer binären Repräsentation in Klassendateien gespeichert werden. Soll nun ein Java-Programm ausgeführt werden, so müssen die Klassen vom Klassenlader der virtuellen Maschine geladen werden. Dieser Ladeprozess unterteilt sich in drei Phasen, die im Folgenden vorgestellt werden. Laden In dieser Phase wird die binäre Repräsentation der Klasse aus einer Klassendatei gelesen. Es wird zunächst untersucht, ob es sich bei der vorliegenden Datei überhaupt um eine Java-Klassendatei handelt. Weiterhin wird die strukturelle Validität der binären Repräsentation sichergestellt, indem das Format der Repräsentation auf die Einhaltung der Formatregeln für Klassendateien hin überprüft wird. Die weitere Verarbeitung der geladenen binären Repräsentation ndet anschlieÿend in der Phase Binden statt. Binden Diese Phase unterteilt sich in mehrere einzelne Schritte. Zunächst wird eine Verikation der geladenen Klasse durchgeführt. Hierbei werden die Inhalte der Strukturen der Klasse auf ihre Korrektheit hin überprüft. Im Falle des Konstanten-Pools werden die einzelnen Einträge, sowie Verweise zwischen den Einträgen überprüft. Anschlieÿend ndet eine Verikation 2.2. DER LADEPROZESS VON JAVA-PROGRAMMEN 9 der Methoden-Bytecodes statt. Hier wird unter anderem überprüft, ob bei einem Zugri auf Daten entsprechende Typregeln eingehalten werden und ob die zugegrienen Daten bereits initialisiert worden sind. Da die Bytecode-Instruktionen in Java typisiert sind, müssen die zugegrienen Daten dem Typ der Instruktion entsprechen. Schlägt die Verikation fehl, so muss angenommen werden, dass die Binärrepräsentation des Programms manipuliert wurde. Die Verikation soll Manipulationen an den Klassendateien erkennen und so das Einschleusen schadhaften Codes verhindern. Auf die Verikation folgt die Vorbereitung der Klasse. In diesem Schritt werden alle Referenzen auf Oberklassen und Schnittstellen der Klasse aufgelöst und die entsprechenden Klassen geladen, sofern dies noch nicht geschehen ist. Anschlieÿend werden die statischen Klassenvariablen mit typspezischen Standardwerten initialisiert. Es wird jedoch noch kein Programmcode zur Initialisierung der statischen Klassenvariablen ausgeführt. Dies geschieht erst in einem späteren Schritt. Während der Verikation und der Vorbereitung werden die symbolischen Referenzen in den Einträgen des Konstanten-Pools aufgelöst. Dies bedeutet, dass die Speicherstelle des Programmobjekts ermittelt wird, das durch die symbolische Referenz repräsentiert wird. Für folgende Zugrie wird dann direkt die Speicherstelle des Programmobjektes verwendet um den aufwändigen Schritt des Auösens zu umgehen. Initialisieren Die abschlieÿende Phase des Ladeprozesses besteht aus der In- itialisierung der geladenen Klasse und ihrer statischen Felder. Hierzu werden die sogenannten statischen Initialisierer aufgerufen, bei denen es sich um eine Bytecode-Sequenz handelt. Diese Initialisierer können zum Beispiel statische Felder mit berechneten Werten belegen. Nach diesem Schritt wird die Klasse als erfolgreich geladen markiert und für die generelle Verwendung in der virtuellen Maschine freigegeben. Diese drei Phasen beschreiben den Ladeprozess für eine einzelne Klasse. Bei der Ausführung eines Programms wendet die virtuelle Maschine das Prinzip des dynamischen Klassenladens an. Dies bedeutet, dass nicht alle Klassen eines Programms zur Startzeit der virtuellen Maschine geladen werden, sondern erst bei Bedarf. Dadurch wird unnötiger Speicherverbrauch durch das Laden nicht benötigter Klassen verhindert. Das dynamische Laden von Klassen kann während der Ausführung einer Methode, beim Laden einer anderen Klasse und beim Zugri auf statische Felder oder Methoden angestoÿen werden. Wird im Rahmen der Ausführung einer Methode eine neue Objektinstanz einer Klasse erzeugt, so wird zunächst der Zustand der Klasse innerhalb der virtuellen Maschine ermittelt. Ist sie noch nicht geladen, so wird der Klassenlader mit dem Ladeprozess beauftragt. War dieser Ladeprozess erfolgreich, steht die Klasse zur Verwendung durch die Ausführungseinheit der virtuellen Maschine zur Verfügung. Ein weiterer Fall, in dem das dynamische Laden einer Klasse erfolgen kann, ist das Laden einer anderen Klasse. Während des Ladevorgangs dieser Klasse werden auch die Oberklassen und die implementierten Schnittstellen geladen. Benden sich die Informationen über diese Klassen noch nicht im Speicher der virtuellen Maschine, so müssen sie ebenfalls nachgeladen werden. 10 KAPITEL 2. GRUNDLAGEN Das Nachladen von Klassen unterbricht den Ablauf des Programms für kurze Zeit. Dies kann insbesondere auf Geräten mit wenig Ressourcen zu einer spürbaren Verzögerung bei der Ausführung des Programms führen. Mit dem hier vorgestellten Verfahren zur Ermittlung sicher geladener Klassen soll diese Verzögerung vermieden werden. Hierzu werden die Klassen, die in jedem möglichen Ablauf eines Programms geladen werden, gleich beim Start der virtuellen Maschine geladen. Weiterhin können diese Klassen bereits beim Start der Ausführung veriziert werden, was zu einer schnelleren Erkennung von Fehlern im Format der binären Repräsentation führt. 2.3 Statische Programm-Analyse Bisher wurden verschiedene Punkte im Ladeprozess von Java-Programmen angesprochen, an denen sich Optimierungspotentiale ergeben. Die in dieser Arbeit vorgestellten Verfahren zur Nutzung dieser Potentiale stützen sich auf statische Programm-Analyse. Aus diesem Grund erfolgt nun eine Einführung in diese Thematik. Die statische Programm-Analyse dient dazu, Aussagen über das Verhalten eines Programms zur Laufzeit zu ermitteln [KAST90, MUCH97]. Das Haupteinsatzgebiet der statischen Programm-Analyse ist der Übersetzerbau und hier insbesondere der Bereich der Programm-Optimierungen. Sie wird unter anderem dazu eingesetzt, ein Programm von redundanten Berechnungen zu befreien oder auch um nicht genutzten Code aus dem Programm zu entfernen. Dies verbessert die Speicher- und Laufzeitezienz des übersetzten Programms. Um dieses Ziel zu erreichen wurden verschiedene Verfahren entwickelt. Man unterscheidet dabei intraprozedurale und interprozedurale Verfahren. Intraprozedurale Verfahren beziehen sich auf eine Prozedur oder Methode eines Programms, während sich interprozedurale Verfahren global auf ein Programm beziehen. 2.3.1 Strukturen der Programm-Analyse Die statischen Programm-Analyse verwendet verschiedene Modellierungs-Strukturen. Hierzu zählen Grundblöcke, Ablaufgraphen, Verbände, sowie Aufrufgraphen. Diese Strukturen werden im Folgenden genauer erläutert. Grundblock Ein Grundblock ist eine Anweisungsfolge maximaler Länge, deren Anweisungen alle genau dann ausgeführt werden, wenn die Erste ausgeführt wird [KAST90]. Ein Grundblock beginnt also mit einer Anweisung, die ein Sprungziel darstellt und endet mit einem bedingten oder unbedingten Sprung. Zwischen diesen beiden Anweisungen benden sich keine weiteren Sprunganweisungen oder Sprungziele. Die Anweisungen eines Grundblocks werden also nur komplett oder gar nicht ausgeführt. Ablaufgraph Ein Ablaufgraph besteht aus Grundblöcken als Knoten und ge- richteten Kanten zwischen den Grundblöcken, die den Kontrolluÿ modellieren. Ein Ablaufgraph enthält genau dann eine gerichtete Kante zwischen zwei Grundblöcken, wenn einer der Grundblöcke ein Sprungziel der letzten Anweisung eines anderen Grundblocks darstellt. Einen Spezialfall stellen die Rückwärtskanten dar, mit deren Hilfe Schleifen modelliert werden. Sie 2.3. STATISCHE PROGRAMM-ANALYSE 11 können als Sprungziel einer Anweisung wieder die erste Anweisung desselben Grundblocks besitzen. Verband Verbände [MUCH97] stellen die mathematische Grundlage der Da- tenuss-Analyse dar. Verbände sind Mengen von Elementen, die Eigenschaften von Programm-Objekten, wie Variablen oder Ausdrücken repräsentieren. Sei V ein Verband und x, y ∈ V Elemente des Verbandes. Auf Verbänden sind die zwei Operationen ∩ (meet) und ∪ (join) deniert. Sie denieren eine Halbordnung. Es gilt x ⊆ y : x ∩ y = x und x ⊇ y : x ∪ y = x. Diese Operationen liefern eine konservative Abschätzung der Auswirkungen von Programm-Instruktionen auf die Eigenschaften der Programm-Objekte in einer Datenuss-Analyse. Weitere Eigenschaften von Verbänden sind Abgeschlossenheit, Kommutativität und Assoziativität. Sie besitzen zwei eindeutige Elemente >(top) und ⊥ (bottom). Es gilt x∩⊥ = ⊥ und x∪> = > für alle x. Auswirkungen von Programmoperationen auf die Informationen der Datenuss-Analyse lassen sich als monotone Funktionen darstellen. Diese werden auch als Transfer-Funktionen bezeichnet. Die Monotonie der Transferfunktionen und die nite Höhe der Verbände garantieren eine terminierende Berechnung auf den Verbänden. Aufrufgraph Ein Aufrufgraph modelliert die Aufrufbeziehung zwischen den Prozeduren eines Programms. Die Prozeduren werden durch die Knoten des Graphen repräsentiert. Der Aufrufgraph enthält genau dann eine gerichtete Kante zwischen zwei Knoten, wenn einer der Knoten eine Prozedur darstellt, die aus der anderen Prozedur heraus aufgerufen werden könnte. Aufrufgraphen für prozedurale Sprachen lassen sich mit eindeutigen Aufrufzielen erstellen. Es gibt allerdings die Ausnahme der prozeduralen Sprachen mit Funktionsszeigern. Hier kann das Aufrufziel nicht eindeutig bestimmt werden und es müssen alle möglichen Aufrufziele angegeben werden. Ebenso müssen bei objekt-orientierten Sprachen mit dynamischer Methodenbindung alle möglichen Aufrufziele eines Methodenaufrufs angegeben werden. Um das Aufrufziel eines Methodenaufrufs möglichst exakt zu ermitteln, existieren einige Verfahren welche die Menge der möglichen Aufrufziele eingrenzen. Diese Verfahren werden in einem späteren Abschnitt noch eingehender behandelt. 2.3.2 Datenuss-Analyse Die Datenuss-Analyse ermittelt die Eigenschaften von Programm-Objekten zur Laufzeit des Programms. Zu solchen Programm-Objekten gehören zum Beispiel Ausdrücke oder Variablen. Die Datenuss-Analyse arbeitet auf den weiter oben vorgestellten Ablaufgraphen und Verbänden. Sie erstellt nun zu jedem Grundblock eine Gruppe von Mengen, die Datenuss-Informationen enthalten. Über diesen Mengen werden die sogenannten Datenussgleichungen deniert. Mit deren Hilfe werden Aussagen über das Verhalten von Programm-Objekten zur Laufzeit des Programms getroen. Je nach Analyse-Problem existieren unterschiedliche Denitionen für den Inhalt der Mengen, sowie den Aufbau der Datenussgleichungen. Jedem Grundblock B werden die Mengen In(B), Out(B), Gen(B), und Kill(B) zugewiesen. Die Menge In(B) wird dem Eingang eines Grundblocks zu- 12 KAPITEL 2. GRUNDLAGEN Start B1 B2 B3 B4 Ende Abbildung 2.6: Darstellung eines Ablaufgraphen mit Grundblöcken und Kontrolluss-Kanten gewiesen und beschreibt die Menge der Datenuss-Informationen, die zu Beginn der Ausführung des Grundblocks B gültig sind. Analog dazu wird die Menge Out(B) dem Ausgang des Grundblocks B zugewiesen. Sie beschreibt die Menge der Datenuss-Informationen, die am Ende der Ausführung des Grundblocks B gültig sind. Die Mengen Gen(B) und Kill(B) werden für jeden Grundblock konstruiert. Die Menge Gen(B) enthält alle Datenuss-Informationen, die durch die Ausführung der Instruktionen von B Gültigkeit erlangen. Dagegen enthält Kill(B) alle Datenuss-Informationen, die während der Ausführung von B ihre Gültigkeit verlieren. Es gibt unterschiedliche Möglichkeiten in welcher Richtung die Analyse den Ablaufgraphen durchläuft. Man spricht dabei von Vorwärts- oder Rückwärtsanalyse. Je nach Datenussproblem wird eines dieser beiden Verfahren angewandt. Bei der Vorwärtsanalyse wird für jeden Grundblock die Menge Out(B) aus den Mengen In(B), Gen(B) und Kill(B) wie folgt berechnet: Out(B) In(B) = Gen(B) ∪ (In(B) − Kill(B)) K = Out(V ) V ∈V org(B) J Hierbei beschreibt der Operator , je nach Datenussproblem, den Vereinigungsoperator ∪ oder den Durchschnittsoperator ∩. Bei der Anwendung des Operators ∪ spricht man von einem Existenzproblem. Es werden dann also alle Informationen in die Menge In(B) mit einbezogen, die in einer beliebigen Menge Out der Vorgänger von B enthalten sind. Der Operator ∩ beschreibt hingegen ein Allproblem. Hier müssen alle Informationen, die in In(B) enthalten sind, auch in allen Mengen Out der Vorgänger von B enthalten sein. Ein Beispiel für eine Optimierung, die ihre Informationen aus einer Datenuss- 2.3. STATISCHE PROGRAMM-ANALYSE procedure a() { b(); c(); } 13 a() procedure b() { d(); } b() c() procedure c() { b(); } d() procedure d() { } Abbildung 2.7: Darstellung eines Aufrufgraphen für eine prozedurale Sprache Analyse gewinnt, ist die Suche nach verfügbaren globalen Teilausdrücken. Mit dieser Optimierung soll verhindert werden, dass gleiche Ausdrücke, die in mehreren Grundblöcken auftreten, mehrfach berechnet werden. Bei diesem Problem handelt es sich um ein Allproblem, welches durch Vorwärtsanalyse gelöst wird. Die Mengen Gen(B) und Kill(B) werden für jeden Grundblock B und jeden Ausdruck A wie folgt deniert A ∈ Gen(B), A ∈ Kill(B), falls es in B eine Berechnung von A gibt, auf die keine Zuweisung an eine der Variablen von A folgt. falls es in B Zuweisungen an Variablen von A gibt, auf die keine Berechnung von A folgt. Analog zur Vorwärtsanalyse wird bei der Rückwärtsanalyse die Menge In(B) aus den Mengen Out(B), Gen(B) und Kill(B) berechnet: In(B) = Gen(B) ∪ (Out(B) − Kill(B)) K Out(B) = In(N ) N ∈N achf (B) Hier wird die Menge Out(B) aus den Datenuss-Informationen gebildet, die in den Mengen In der Grundblöcke enthalten sind, die Nachfahren von B sind. Die Datenussgleichungen für ein solches Problem können mittels eines iterativen oder eines hierarchischen Verfahrens gelöst werden. Eine Diskussion dieser Verfahren ndet sich in [KAST90]. 2.3.3 Aufrufgraph-Analyse Nachdem die Datenuss-Analyse eingeführt wurde, wird nun die AufrufgraphAnalyse vorgestellt. Dieses Verfahren wird ebenfalls in dieser Arbeit verwendet. Wie bereits in Abschnitt 2.3.1 erläutert wurde, modelliert ein Aufrufgraph die Aufrufbeziehungen zwischen Prozeduren oder Methoden eines Programms. Ein Aufrufgraph für eine prozedurale Programmiersprache ist in Abbildung 2.7 dargestellt. Die Knoten stellen in einem solchen Graphen die Prozeduren dar, während die Kanten Aufrufe zwischen den Prozeduren modellieren. Es existieren einige Programm-Analysen, die auf der Struktur eines Aufrufgraphen aufbauen. Ein Beispiel ist die sogenannte Hüllenbildung. Hierbei wird 14 KAPITEL 2. GRUNDLAGEN class A { method m() method c() } class B extends A { method m() } A.m C.m B.m class C extends A { method p() } E.p class D extends B { method m() } D.m class E extends C { method p() { v.m(); } } Abbildung 2.8: Darstellung eines Aufrufgraphen für eine objekt-orientierte Sprache mit dynamischer Methodenbindung eine Menge aus allen Prozeduren konstruiert, die im Rahmen der Ausführung eines Programms aufgerufen werden können. Ausgehend von einem Startpunkt ist es dann möglich erreichbare Methoden zu erfassen. Dies dient unter anderem dazu, den Analyse-Kontext für anschlieÿende interprozedurale Analyse einzugrenzen. Eine weitere Methode zur Gewinnung von Informationen aus einem Aufrufgraphen ist die Erkennung von Zyklen. Dies dient unter anderem zur Erkennung endrekursiver Funktionen. Man bezeichnet eine Funktion f als Endrekursiv, wenn der rekursive Funktionsaufruf die letzte Aktion zur Berechnung von f ist. Wird eine Funktion als Endrekursiv erkannt, so kann sie von einem Übersetzer in eine iterative Funktion umgeformt werden, was Speicherplatz auf dem Aufrufkeller spart. Der Eingangsgrad der Knoten liefert eine weitere Information für Optimierungen. Ist der Eingangsgrad eines Knoten sehr klein, so gibt es nur wenige Aufrufstellen dieser Prozedur im Programm. Enthält die aufgerufene Prozedur nur wenige Zeilen Code, so kann die sogenannte Inline-Optimierung angewandt werden. Dabei wird der Code der aufgerufenen Prozedur direkt an der Aufrufstelle eingefügt, was den zusätzlichen Aufwand des Prozeduraufrufs vermeiden soll. 2.3.4 Aufrufgraph-Analyse in objektorientierten Programmen Im Folgenden sollen Verfahren zur Konstruktion von Aufrufgraphen für objektorientierte Sprachen betrachtet werden. Das Problem bei der Konstruktion von Aufrufgraphen für objekt-orientierte Programme ist die dynamische Methodenbindung. Hierbei kann das Ziel eines Methodenaufrufs erst zur Laufzeit des Programms bestimmt werden. Dies geschieht durch die Bestimmung des Typs des Objektes auf dem die Methode aufgerufen wurde. Ein Beispiel für den Auf- 2.3. STATISCHE PROGRAMM-ANALYSE 15 rufgraphen eines objekt-orientierten Programms ndet sich in Abbildung 2.8. Daraus ergibt sich für die statische Analyse von objekt-orientierten Aufrufgraphen die Herausforderung, das Ziel eines Methodenaufrufs möglichst exakt zu bestimmen. Eine Optimierung, die auf einer solchen Analyse aufbaut, ist das statische Binden dynamischer Methodenaufrufe. Dies soll die aufwändige Bestimmung von Aufrufzielen zur Laufzeit des Programms vermindern. Man muss beim Aufbau eines objekt-orientierten Aufrufgraphen zunächst davon ausgehen, dass jede Methode mit gleichem Namen ein potentielles Aufrufziel sein kann. Die einzige Ausnahme stellen hier statische Methodenaufrufe, sowie Aufrufe von Konstruktoren oder privaten Methoden dar. Bei dieser Art von Aufrufen ist das Aufrufziel eindeutig bestimmbar. Es existieren Verfahren, mit deren Hilfe die Menge der potentiellen Aufrufziele eingegrenzt werden kann. Einige dieser Verfahren werden im Folgenden vorgestellt. Class Hierarchy Analysis (CHA) Bei dieser Analyse werden für jede Me- thode die Aufrufstellen und die Mengen der potentiellen Aufrufziele ermittelt. Zur Einschränkung der Menge von potentiellen Aufrufzielen werden Informationen über die Klassenhierarchie des Programms hinzugezogen. Zunächst wird der statische Typ des Aufrufziels bestimmt. Mit Hilfe der Informationen aus der Klassenhierarchie des Programms wird in den Unter- und Oberklassen der Typ-Klasse nach Implementierungen der aufgerufenen Methode gesucht. Alle gefundenen Implementierungen stellen potentielle Aufrufziele dar und werden somit der Menge der Aufrufziele der Methode hinzugefügt. Rapid Type Analysis (RTA) Die Rapid Type Analysis erweitert die Class Hierarchy Analysis. Es wird das gleiche Verfahren wie bei der CHA angewandt. Zusätzlich wird eine Hilfsmenge gebildet, in der alle Typen enthalten sind, von denen im untersuchten Programm Objekte erzeugt werden. Aus dieser Menge und der Menge der potentiellen Aufrufziele einer Methode, die mit Hilfe der CHA ermittelt wurde, wird eine Schnittmenge gebildet. In dieser Schnittmenge sind dann nur noch die Klassen mit potentiellen Aufrufzielen eines Methodenaufrufs enthalten, von denen im Programm Objekte erzeugt werden. Dies stellt eine weitere Einschränkung der Menge der potentiellen Aufrufziele eines Methodenaufrufs dar. Field Type Analysis (FTA) Die Field Type Analysis stellt eine Erweiterung der Rapid Type Analysis dar. Bei diesem Verfahren wird jedoch keine globale Hilfsmenge für das gesamte Programm erstellt, sondern je eine Hilfsmenge pro Klasse und pro Methode. In der Hilfsmenge für eine Klasse werden Typen gespeichert, die durch schreibenden Zugri in Feldern der Klasse aufgenommen werden können. Hierdurch soll der Datenuss zwischen Methoden und Feldern modelliert werden. Die Hilfsmenge einer Methode enthält die Typen, die durch Erzeugungsstellen, Eingabeparameter und Rückgabewerte verwendet werden. Ebenso enthält diese Menge die Typen von lesenden Feldzugrien, was wiederum den Datenuss zwischen Methoden und Feldern modelliert. Durch die Verwendung dieser Mengen ndet eine weitere Einschränkung der potentiellen Aufruziele eines Methodenaufrufs statt. In der Menge der potentiellen Aufrufziele benden 16 KAPITEL 2. GRUNDLAGEN sich nun nur noch diejenigen Klassen, von denen in einer Methode lokale Instanzen aktiv sind. DefUse-Analysis Dieses Verfahren basiert auf der Datenuss-Analyse. Im Kontrolluss-Graphen einer Methode werden hierbei alle Zugrie auf Variablen identiziert. Lesende Zugrie bezeichnet man als Verwendungsstellen (Use) und schreibende Zugrie als Denitionsstellen (Def). Es werden dann zu jeder Verwendungsstelle die entsprechenden Denitionsstellen ermittelt (DefUse-Kette). Interpretiert man nun einen Methodenaufruf als Verwendungsstelle so stehen an den zugehörigen Denitionsstellen die Typen, auf denen dieser Aufruf möglicherweise stattndet. In der Menge der Denitionsstellen wird dann nach Implementierungen der aufgerufenen Methode gesucht. Alle als potentielle Aufrufziele identizierte Typen werden einer Ergebnismenge hinzugefügt. 2.4 Arbeiten zur Optimierung des Ladeprozesses In diesem Abschnitt sollen einige verwandte Verfahren vorgestellt werden, die den Ladeprozess von Java-Programmen optimieren. Die in diesen Arbeiten entwickelten Verfahren werden erläutert und zu den Verfahren abgegrenzt, die im Rahmen der vorliegenden Arbeit erstellt wurden. Es werden sowohl Verfahren betrachtet, die eigene Dateiformate spezizieren, als auch Verfahren, die Optimierungen zur Ladezeit durchführen. 2.4.1 ZIP- und JAR-Dateiformat Das ursprüngliche Format der Java Klassendateien ist nicht optimal im Hinblick auf die Dateigröÿe. Aus diesem Grund wurden verbesserte Formate für die Verbreitung von Java-Programmen entwickelt. SUN Microsystems selbst benutzte zunächst das ZIP-Dateiformat [ZIP06] um Programme, die aus vielen Klassendateien bestehen, zu verbreiten. Das ZIP-Format fasst die einzelnen Klassendateien zu einer Archiv-Datei zusammen und kann diese Dateien optional komprimieren. Später benannte SUN dieses Format in JAR (Java ARchive) um. Das Verfahren ist aber weiterhin mit dem ZIP-Format identisch. Die in den JAR-Dateien angewandten Komprimierungsverfahren waren jedoch nicht gut geeignet, um Klassendateien über schmalbandige Netzwerkverbindungen oder auf Geräte mit wenig Ressourcen zu verbreiten. Der gröÿte Nachteil ist, dass jede Klassendatei einzeln komprimiert wird. Dadurch geht die Möglichkeit verloren, Informationen über den globalen Aufbau des Archivs zur besseren Komprimierung zu verwenden. Weiterhin muss auf jeder Plattform, auf der das komprimierte JAR-Format verwendet werden soll, eine Implementierung des ZIP-Algorithmus vorliegen. Dies ist jedoch auf Grund von mangelnden Ressourcen nicht für alle Plattformen möglich. Es wurden einige Verbesserungen für das JAR-Format und auch eigenständige Formate entwickelt, die eine deutlich bessere Kompressionsrate aufweisen. In dieser Arbeit wird das JAR-Dateiformat verwendet, da es sich hierbei um ein standardisiertes Dateiformat handelt. Dieses Format kann von allen virtuellen Maschinen für die Java-Plattform verarbeitet werden. Es werden jedoch keine neuen Komprimierungsverfahren für diese Dateien vorgestellt. 2.4. ARBEITEN ZUR OPTIMIERUNG DES LADEPROZESSES 17 2.4.2 Komprimierung nach Pugh Eine Technik, die es ermöglicht, deutlich kompaktere JAR-Dateien zu erzeugen, wurde von Pugh [PUGH99] vorgestellt. In diesem Verfahren werden der bisherige Komprimierungs-Algorithmus und das Format der JAR-Dateien beibehalten. Allerdings werden die zu verpackenden Klassendateien modiziert, so dass der Komprimierungs-Algorithmus bessere Ergebnisse liefern kann. Hierzu werden die Inhalte der Klassendateien umorganisiert und in einem leichter zu komprimierenden Format kodiert. Durch diese Technik ist es möglich, die Kompression im Gegensatz zu unkomprimierten JAR-Dateien um den Faktor 2 bis 5 zu verbessern, ohne den Aufwand für die Dekomprimierung zu stark zu erhöhen. Der Fokus dieser Arbeit liegt nicht auf der Kompression von JAR-Dateien. Ebenso soll das Format der Klassendateien beibehalten werden. Es besteht jedoch die Möglichkeit, das Komprimierungsverfahren von Pugh auf die JARDateien anzuwenden, die mit den hier beschriebenen Verfahren erzeugt wurden. Dadurch kann der Vergröÿerung der JAR-Dateien durch das Hinzufügen zusätzlicher Attribute in den Java-Klassendateien entgegengewirkt werden. 2.4.3 JAZZ-Dateiformat Zu den eigenständigen Formaten gehört das von Bradley, Horspool und Vitek vorgestellte JAZZ-Format [BRAD98]. Das JAZZ-Format wendet ebenfalls Techniken zur Reorganisation von Informationen in Klassendateien an, um eine bessere Kompression zu erzielen. So werden die Konstanten-Pools der einzelnen Klassendateien im JAZZ-Archiv zu einem globalen Konstanten-Pool zusammengefasst und dadurch redundante Einträge entfernt. Die Indizes der globalen Konstanten-Pools werden mittels des Human-Verfahrens komprimiert. Zeichenketten, Instruktionen und zusätzliche Daten werden mittels des ZIPVerfahrens komprimiert. Mit Hilfe dieser Maÿnahmen lassen sich Klassendateien auf eine Gröÿe von bis zu einem Viertel der Gröÿe eines unkomprimierten JAR-Archives identischen Inhalts komprimieren. Das JAZZ-Format selbst ist für die Betrachtungen in dieser Arbeit weniger interessant, da es nur von modizierten virtuellen Maschinen verarbeitet werden kann. Eine der Voraussetzungen der hier vorgestellten Verfahren ist jedoch, dass das ursprüngliche Klassendatei-Format beibehalten werden soll. Allerdings wurde im Rahmen der Entwicklung des JAZZ-Formates ein Verfahren zur Erzeugung eines globalen Konstanten-Pools vorgestellt, welches jedoch Human-Codes für den Zugri auf die Einträge des globalen Konstanten-Pools verwendet. Ausserdem werden die Einträge vom Typ CONSTANT_CLASS und CONSTANT_String aus dem globalen Konstanten-Pool entfernt. Diese Optimierungen werden von dem in dieser Arbeit vorgestellten Verfahren nicht durchgeführt. 2.4.4 Jikes Application Extractor (JAX) Einen anderen Weg zur Optimierung des Ladeprozesses beschreiten Tip et al. mit dem Jikes Application eXtractor (JAX) [TIP99]. Dieses Verfahren wendet Analysetechniken aus dem Übersetzerbau auf Java-Programme an. Das zu komprimierende Programm wird mittels eines statischen Analyseverfahrens untersucht. Dadurch werden die einzelnen Komponenten des Programms identiziert, 18 KAPITEL 2. GRUNDLAGEN die zur Ausführung benötigt werden. Nicht benötigte Komponenten werden aus dem Archiv entfernt. Weiterhin werden noch einige Programmtransformationen durchgeführt. Diese Transformationen ergebens sich aus den Informationen, die in der Analysephase gesammelt wurden. Durch dieses Verfahren werden sowohl die Ausführungsgeschwindigkeit erhöht, als auch der Speicherverbrauch des Programms verringert. Nach Messungen der Autoren konnte die Gröÿe des Programmarchives im Durchschnitt um ca. 50 % verringert werden. Jedoch müssen bei diesem Verfahren auch lange Verarbeitungszeiten auf Grund der komplexen Analysen in Kauf genommen werden. Der Jikes Application Extractor ist den in dieser Arbeit vorgestellten Verfahren sehr ähnlich, da auch hier Programm-Analyse angewandt wird um die Laufzeit und den Speicherverbrauch eines Java-Programms zur Laufzeit zu verringern. Jedoch wird bei JAX die Programm-Analyse dazu verwendet, Teile des Programms zu entfernen, die während der Ausführung nicht benötigt werden. Das JAX-Verfahren verwendet Programm-Analyse dazu, sicher nicht geladene Teile eines Programms zu identizieren. Eines der in dieser Arbeit vorgestellten Verfahren verwendet die Programm-Analyse zur Identizierung sicher geladener Klassen. Weiterhin werden keine Teile des Programms entfernt. 2.4.5 Optimierungen zur Ladezeit In dem von Rippert, Courbot und Grimaud [RIPP04] beschriebenen Verfahren wird die Tatsache ausgenutzt, dass viele Einträge des Konstanten-Pools lediglich während des Ladevorgangs einer Klassendatei verwendet werden. Insbesondere sind dies Einträge vom Typ CONSTANT_Utf8, die konstante Zeichenketten enthalten. Diese Einträge sind für die Ausführung des Programms überüssig und werden während des Ladevorgangs entfernt. Dadurch wird Speicherplatz in den Laufzeit-Strukturen der virtuellen Maschine eingespart. Während des Ladevorgangs wird versucht, möglichst viele Einträge aus der Menge von CONSTANT_Utf8-Einträgen zu entfernen. Dies ist zum Beispiel möglich für Einträge, die Informationen über Klassen, Zeichenketten oder Namens- und Typbezeichnungen enthalten. Alle diese Einträge enthalten Verweise auf Einträge dieses Typs, die beim Anlegen der entsprechenden Speicherstrukturen für diese Einträge aufgelöst werden können. Wird ein Eintrag vom Typ CONSTANT_Utf8 nicht mehr von anderen Einträge referenziert, so kann er entfernt werden. Dies spart zusätzlich Speicherplatz. Weiterhin werden beim Laden der Methoden der Klasse Bytecode-Instruktionen durch optimierte Varianten ersetzt. Dies betrit insbesondere Instruktionen, die durch Zugrie auf Einträge des Konstanten-Pools bestimmte Typinformationen laden. Stattdessen werden diese Instruktionen durch Varianten ersetzt, die den Zugri auf den Konstanten-Pool umgehen und direkt auf die entsprechenden Werte zugreifen können. Diese Maÿnahme spart zusätzlichen Speicherplatz und verringert auÿerdem die Laufzeit des Programms. Messungen der Autoren ergaben eine Reduzierung der Anzahl der benötigten Einträge im Konstanten-Pool auf bis zu 17 % der ursprünglichen Anzahl von Einträgen. Der Speicherbedarf der Konstanten-Pool-Einträge konnte auf bis zu 8 % der ursprünglichen Gröÿe verringert werden. In diesem Verfahren werden verschiedene Analysen und daraus resultierende Optimierungen zur Ladezeit eines Java-Programms vorgenommen. Dies hat jedoch zur Folge, dass zusätzliche Berechnungen durchgeführt werden müssen, 2.4. ARBEITEN ZUR OPTIMIERUNG DES LADEPROZESSES 19 welche die Ladezeit zusätzlich verlängern. In den Verfahren, die in dieser Arbeit vorgestellt werden, stehen die Analyse-Informationen zur Ladezeit bereits zur Verfügung, da diese bereits in einer Vorverarbeitung ermittelt werden. Durch diese Maÿnahme wird die Ladezeit nur wenig erhöht. 20 KAPITEL 2. GRUNDLAGEN Kapitel 3 Konzept Nachdem die Grundlagen zu Laufzeitumgebungen für Java-Programme und der statischen Programm-Analyse dargestellt wurden, sollen hier die Konzepte zur Optimierung des Ladeprozesses vorgestellt werden. Das Verfahren zur Faltung von Konstanten-Pools dient dazu, den Speicherverbrauch während der Ausführung eines Java-Programms zu senken. Dies wird durch die Einführung einer neuen Datenstruktur, des globalen Konstanten-Pools für eine Ladeeinheit, in eine virtuelle Maschine erreicht. Die Beschreibung der Datenstruktur sowie das Verfahren zur Konstruktion werden in Abschnitt 3.1 dargestellt. Durch das Verfahren zur Ermittlung sicher geladener Klassen können die wichtigsten Klassen eines Programms schon beim Start der virtuellen Maschine geladen werden. Dieses Vorgehen sorgt dafür, dass die Klassen während des Programmlaufs bereits im Speicher der virtuellen Maschine zur Verfügung stehen, wenn sie benötigt werden. Dies verhindert Unterbrechungen des Programmlaufs durch das dynamische Nachladen von Klassen. Weiterhin können diese sicher geladenen Klassen bereits zu Beginn des Programmlaufs veriziert werden. Dadurch können Fehler in den Klassendateien dieser Klassen früher aufgedeckt werden. Das Verfahren zur Ermittlung sicher geladener Klassen wird in Abschnitt 3.2 vorgestellt. 3.1 Faltung von Konstanten-Pools In diesem Abschnitt wird das Verfahren zur Faltung von Konstanten-Pools erläutert. Es wird zunächst die Datenstruktur des globalen Konstanten-Pools eingeführt, in der jeder Eintrag aus den Konstanten-Pools der Klassen einer Ladeeinheit nur noch genau einmal enthalten ist. Anschlieÿend wird auf die Konstruktion eines solchen globalen Konstanten-Pools eingegangen. Eine besondere Vorgabe bei der Entwicklung dieses Verfahrens war die Einschränkung, dass ein groÿer Teil der Optimierung weder zur Ladezeit noch zur Laufzeit des Programms stattnden sollte. Aus diesem Grund muss die Analyse der Ladeeinheit in einer Vorverarbeitung stattnden. Um den Klassenlader der virtuellen Maschine mit den Informationen über den gefalteten KonstantenPool eines Programms zu versorgen, sollte auÿerdem kein neues Dateiformat entworfen werden. So musste auf Erweiterungspunkte im ursprünglichen For21 22 KAPITEL 3. KONZEPT Virtuelle Maschine modifizierte Ladeeinheit interner Speicher Klasse A KP A Klasse A Map-Att Klasse B KP B Map-Att Klassenlader Klasse B Klasse C globaler KonstantenPool Klasse C KP C Map-Att Ausführungseinheit Abbildung 3.1: Darstellung des Ladevorgangs einer modizierten virtuellen Maschine mit einem globalen Konstanten-Pool mat der Java-Klassendateien zurückgegrien werden um die gewonnen AnalyseInformationen zu speichern. Eine Erläuterung der Art der Speicherung der Informationen ndet sich in Abschnitt 3.1.3. Die besondere Art der Speicherung der Analyse-Informationen geht zu Lasten der Gröÿe der Klassendateien. Dies ist nicht für alle Anwendungsfälle sinnvoll. Aus diesem Grund werden in Abschnitt 3.1.4 verschiedene Strategien vorgestellt, mit deren Hilfe die Gröÿe des Transportformates verringert werden kann. 3.1.1 Einführung eines globalen Konstanten-Pools In einem globalen Konstanten-Pool werden alle Einträge aus den KonstantenPools von Klassendateien einer Ladeeinheit genau einmal gespeichert. Durch diese Maÿnahme lässt sich der Speicherverbrauch eines Java-Programms zur Laufzeit verringern (siehe Abbildung 3.1). Betrachtet man die Menge CL der Klassen einer Ladeeinheit L, so existiert zu jeder Klasse c ∈ CL ein Konstanten-Pool CP c . Dieser Konstanten-Pool enthält die Einträge ei mit 1 ≤ i ≤ |CP c |. Der globale Konstanten-Pool GCP L einer Ladeeinheit ist dann wie folgt deniert [ GCP L = CP c c∈CL Der globale Konstanten-Pool stellt also die Vereinigung aller Einträge aus den Konstanten-Pools der Klassen einer Ladeeinheit dar. Jeder Eintrag e aus den ursprünglichen Konstanten-Pools ist nur noch genau einmal im globalen Konstanten-Pool enthalten. Für die Gröÿe des globalen Konstanten-Pools gilt 3.1. FALTUNG VON KONSTANTEN-POOLS 23 Ladeeinheit 1. Verweisbaum konstruieren globaler Konstantenpool CONSTANT_Fieldref class_index #6 4. Wurzelelement bereits im globalen KP? nameandtype_index #7 3. Elemente bereits im globalen KP? CONSTANT_Class name_index #8 CONSTANT_NameAndType name_index #10 type_index #11 CONSTANT_Utf8 ’HelloWorld’ CONSTANT_Utf8 ’testFeld’ . . . CONSTANT_Utf8 ’I’ 2. Blattelemente bereits im globalen Konstanten-Pool? Abbildung 3.2: Darstellung des Vergleichs von zusammengesetzten Einträgen dann |GCP L | ≤ X |CP c | c∈CL Aus dieser Denition folgt, dass die Konstruktion des globalen KonstantenPools im Wesentlichen durch Vergleichsoperationen bewerkstelligt werden kann. Dabei muss lediglich verglichen werden, ob ein Eintrag aus einem KonstantenPool einer Klasse bereits im globalen Konstanten-Pool enthalten ist, oder nicht. 3.1.2 Konstruktion des globalen Konstanten-Pools Die Konstruktion des globalen Konstanten-Pools erfolgt durch den Vergleich der Inhalte von Einträgen. Dabei werden die Einträge, die in den globalen Konstanten-Pool eingefügt werden sollen mit den Einträgen verglichen, die bereits eingefügt wurden. Für die einfachen Einträge der Konstanten-Pools ndet ein direkter Vergleich der Inhalte der Einträge statt. Bendet sich ein Eintrag mit gleichem Inhalt bereits im globalen Konstanten-Pool, so wird der einzufügende Eintrag nicht nochmals hinzugefügt. Es wird lediglich eine Information darüber gespeichert, in welcher Klasse der Eintrag aufgetreten ist. Diese Information wird bei der späteren Speicherung der Analyse-Informationen benötigt. Aufwändiger gestaltet sich dagegen der Vergleich von zusammengesetzten Einträgen (siehe Abbildung 3.2). Da diese Einträge eine Baum-Struktur erzeugen, werden zunächst die Elemente dieses Baumes ermittelt. Nach der Konstruktion dieses Baumes wird dieser von den Blättern her durchlaufen und jedes einzelne Element mit den Elementen im globalen Konstanten-Pool verglichen. Bendet sich eines der Elemente noch nicht im globalen Konstanten-Pool, so wird es eingefügt. In jedem Fall wird bei dieser Vergleichsoperation der Index des gefundenen Eintrags im globalen Konstanten-Pool ermittelt. Mit den so ermittelten Indizes werden temporäre Varianten der zusammengesetzten Einträge erzeugt. Diese temporären Elemente werden dann wiederum mit den Einträgen des globalen Konstanten-Pools verglichen. Dieses Verfahren wird bis zum 24 KAPITEL 3. KONZEPT Wurzelelement eines Verweisbaumes weitergeführt. Durch dieses Vorgehen wird sichergestellt, dass alle Einträge, die von einem zusammengesetzten Eintrag referenziert werden, bereits im globalen Konstanten-Pool enthalten sind. Betrachtet man den Aufbau zusammengesetzter Einträge in KonstantenPools genauer, so stellt man fest, dass die Blätter ihres Verweisbaumes immer aus Einträgen vom Typ CONSTANT_Utf8 bestehen. Weiterhin wurde bei der Betrachtung des Aufbaus der Einträge festgestellt, dass diese unterschiedlich komplex aufgebaut sein können. Daher ist es sinnvoll, das Einfügen von Einträgen in den globalen Konstanten-Pool in mehreren Phasen durchzuführen. In der ersten Phase werden zunächst die einfachen Einträge in den globalen Konstanten-Pool eingefügt. Zu diesen gehören auch die Einträge des Typs CONSTANT_Utf8. Damit ist sichergestellt, dass sich die Einträge, die Blätter eines Verweisbaumes darstellen, bereits im globalen Konstanten-Pool benden. In der nächsten Phase werden die zusammengesetzten Einträge in den globalen Konstanten-Pool eingefügt, bei denen die Tiefe des Verweisbaumes 1 ist. Hier wird der Vergleich der Einträge wie oben beschrieben durchgeführt. In den folgenden Phasen werden jeweils die Einträge mit der nächstgröÿeren Tiefe dem Konstanten-Pool hinzugefügt. Wurde diese Operation für alle Klassen einer Ladeeinheit durchgeführt, so hat man einen globalen Konstanten-Pool für diese Ladeeinheit konstruiert. In diesem ist jeder Eintrag gleichen Inhalts aus den Konstanten-Pools der Klassen in der Ladeeinheit genau einmal enthalten. Redundante Einträge fallen durch diese Konstruktion weg. 3.1.3 Speicherung der Analyse-Informationen Eine Vorgabe bei der Entwicklung des hier vorgestellten Verfahrens zur Faltung von Konstanten-Pools war, dass das ursprüngliche Format der Java-Klassendateien beibehalten werden sollte. Der Grund hierfür ist, dass Ladeeinheiten, die für den Einsatz in einer virtuellen Maschine mit globalen Konstanten-Pools modiziert wurden, auch in virtuellen Maschinen zum Einsatz kommen können, die keine globalen Konstanten-Pools unterstützen. Die Java Virtual Machine Specication [JVM98] deniert für solche Fälle bestimmte Erweiterungspunkte im Format der Klassendateien. Wie bereits in Abschnitt 2.1.2 dargestellt wurde, handelt es sich bei diesen Erweiterungspunkten um die sogenannten Attribute. Die Analyse-Information besteht aus Abbildungen, welche die Indices des Konstanten-Pools einer Klasse auf Indices im globalen Konstanten-Pool abbilden. Da das Format der Klassendateien beibehalten wird, lässt sich mit dieser Information der globale Konstanten-Pool zur Ladezeit aus den ursprünglichen Einträgen und den Abbildungen leicht rekonstruieren. Die Attribute sollen hier verwendet werden um die Analyse-Informationen an eine modizierte virtuelle Maschine weiterzugeben, damit diese den globalen Konstanten-Pool der Ladeeinheit rekonstruieren kann. Hierzu wird eine neues Attribut, das sogenannte Mapping-Attribut, deniert. Dieses Attribut enthält die oben beschriebenen Abbildungen von Indices. Der schematische Aufbau dieses Attributs ist in Abbildung 3.3 dargestellt. Das Attribut besteht aus Abbildungen, die den Index eines Eintrags im Konstanten-Pool der Klasse (source_index) auf einen Index im globalen Konstanten-Pool (target_index) abbilden. Für einfache Einträge reichen diese beiden Werte aus. Bei zusammengesetzten Einträgen müssen zusätzlich die Indices 3.1. FALTUNG VON KONSTANTEN-POOLS 25 MappingAttribut source_index #1 target_index #17 Abbildung für einfachen Eintrag source_index #38 target_index #11 source_name_index target_name_index source_type_index target_type_index . . . . #5 #23 #6 #46 Abbildung für zusammengesetzen Eintrag source_index #33 target_index #117 Abbildung 3.3: Schematische Darstellung des Aufbaus des Mapping-Attributes der Einträge, auf die ein zusammengesetzter Eintrag verweist, abgebildet werden. Wie im Beispiel in Abbildung 3.3 dargestellt ist, würden für einen Eintrag vom Typ CONSTANT_NameAndType noch je eine Abbildung für die referenzierten Name- und den Type-Einträge hinzukommen. 3.1.4 Optimierung des Transportformats Die Annotation der Klassendateien mit den Analyse-Informationen, führt zu einer Vergröÿerung der Ladeeinheit. Dies ist in einigen Fällen nicht akzeptabel. Insbesondere sind dies Anwendungsfälle, in denen die Ladeeinheiten über schmalbandige Netzwerkverbindungen übertragen oder auf Geräte geladen werden, in denen nur wenig Speicher zur Ablage der Ladeeinheit zur Verfügung steht. Hier möchte man die Gröÿe der Ladeeinheiten, und somit die Transportkosten, so gering wie möglich halten. Möchte man jedoch gleichzeitig nicht auf die Speicherersparnis durch die Verwendung eines globalen Konstanten-Pools verzichten, muss nach einem Kompromiss gesucht werden. Zu diesem Zweck betrachtet man die Struktur des globalen Konstanten-Pools und die Art, wie die Klassen der Ladeeinheit auf die Einträge zugreifen. Bei der Art der Zugrie können zwei Extremfälle auftreten, wie in Abbildung 3.4 dargestellt wurde. Es kann zum einen vorkommen, dass die Zugrie auf Einträge des globalen Konstanten-Pools völlig disjunkt sind. Das bedeutet, dass keine zwei Klassen auf denselben Eintrag im globalen Konstanten-Pool zugreifen. Dies wäre sicherlich der schlimmste Fall, denn hier würde die Optimierung durch den globalen Konstanten-Pool keinerlei Speicherersparnis zur Laufzeit des Programms bringen. Im besten Fall ist die Überdeckung der Zugrie aus den Klassen der Ladeeinheit auf die Einträge des globalen Konstanten-Pools möglichst groÿ. Hier würde auf jeden Eintrag aus jeder Klasse der Ladeeinheit mindestens einmal zugegrien werden. In diesem Fall würde auch die maximale Speicherersparnis beim Ablauf des Programms in der virtuellen Maschine auftreten. Zur Optimierung der Transportkosten für eine Ladeeinheit ist es daher sinnvoll, nur diejenigen Klas- 26 KAPITEL 3. KONZEPT Klasse B Klasse B Klasse C Klasse A Schlechtester Fall Klasse C Klasse A Bester Fall Abbildung 3.4: Bester und schlechtester Fall beim Zugri auf Einträge des globalen Konstanten-Pools sen in die Konstruktion eines globalen Konstanten-Pools einzubeziehen, deren Zugrie auf den globalen Konstanten-Pool eine möglichst hohe Überdeckung aufweisen. Durch dieses Vorgehen wird sichergestellt, dass die Transportkosten minimiert werden und gleichzeitig eine Speicherersparnis zur Laufzeit des Programms vorhanden bleibt. Für die weiteren Betrachtungen wird die Anzahl der Zugrie von Klassen auf einen Eintrag des globalen Konstanten-Pools mit z(e), e ∈ GCP L deniert. 3.1.5 Bildung der Teilmengen über die Anzahl der Zugriffe In der hier vorgestellten Strategie sollen diejenigen Klassen in die Konstruktion eines globalen Konstanten-Pools einbezogen werden, deren Einträge im globalen Konstanten-Pool zu einem festgelegten Anteil noch von mindestens einer anderen Klassen verwendet werden. Eine Darstellung dieses Sachverhaltes ndet sich in Abbildung 3.5. Betrachtet man dort die Klasse B, so werden die Einträge im globalen Konstanten-Pool zum Teil ausschlieÿlich von dieser Klasse verwendet. Diese Einträge haben dann eine Zugriszahl z(e) = 1. Es soll nun möglich sein für die Optimierung einen Grenzwert festzulegen der angibt, wie hoch der Anteil der Einträge einer Klasse im globalen Konstanten-Pool ist, die eine Zugriszahl von 1 haben dürfen. Liegt der Anteil dieser Einträge unter diesem Grenzwert, so wird die Klasse noch in die Teilmenge zur Konstruktion eines optimierten globalen Konstanten-Pools aufgenommen. Sei nun c ∈ CL eine Klasse aus der Ladeeinheit L. Weiterhin sei Ec ⊆ GCP L die Teilmenge der Einträge im globalen Konstanten-Pool auf die von der Klasse c heraus zugegrien wird. Für die hier vorgestellte Strategie werden nur solche Einträge aus der Menge Ec benötigt, für die z(e) = 1, e ∈ Ec gilt. Die Anzahl von Einträgen in der Menge Ec mit dieser Eigenschaft wird mit z1 (Ec ) bezeichnet. Enthält die Menge Ec zuviele Einträge mit dieser Eigenschaft, so verschlechtert sich das Verhältnis von Transportkosten zum Nutzen durch die Faltung des Klasse C Klasse B Klasse A 3.1. FALTUNG VON KONSTANTEN-POOLS Einträge 27 #Zugriffe 2 2 2 1 1 1 1 1 1 1 1 1 Abbildung 3.5: Darstellung der Auswahlstrategie über die Anzahl der Zugrie auf Elemente des globalen Konstanten-Pools Konstanten-Pools. Die Teilmenge der Klassen, für die ein Konstantenpool konstruiert werden soll, ist dann wie folgt deniert ½ ¾ z1 (Ec ) CO = c ∈ CL | ≤t |Ec | Der Wert t stellt hierbei den Grenzwert dar, der festlegt, wieviele Einträge in der Menge Ec enthalten sein dürfen, auf die nur eine Klasse zugreift. Für den optimierten globalen Konstanten-Pool werden nun also nur noch diejenigen Klassen verwendet, deren Anteil von Einträgen mit Zugriszahl 1 unterhalb des Grenzwertes liegt. Der Vorteil des Verfahrens ist, dass es über den Grenzwert t steuerbar ist. 3.1.6 Bildung der Teilmengen über die durchschnittliche Anzahl von Zugrien In diesem Abschnitt soll eine weitere Strategie zur Optimierung der Transportkosten einer Ladeeinheit vorgestellt werden. Bei dieser Strategie wird die durchschnittliche Anzahl von Zugrien auf Einträge eines globalen Konstanten-Pools verwendet. Diese Zahl wird als dGCP wie folgt deniert P e∈GCP z(e) dGCP = |GCP | wobei |GCP | die Anzahl der Einträge im globalen Konstanten-Pool darstellt. Bei der hier vorgestellten Strategie handelt es sich um eine lokale Nachoptimierung, welche einen lokal maximalen Wert für die durchschnittliche Anzahl von Zugrien auf den globalen Konstanten-Pool berechnet. Es wird zunächst einmal der globale Konstanten-Pool über alle Klassen berechnet und die durchschnittliche Anzahl von Zugrien dGCP bestimmt. Dieser Wert ist die minimale durchschnittliche Anzahl von Zugrien, die der optimierte globale Konstanten-Pool haben darf. 28 KAPITEL 3. KONZEPT Anschlieÿend wird aus der Menge von Klassen, die auf den globalen Konstanten-Pool zugreifen, eine Klasse entfernt. Erhöht sich der Wert dGCP durch die Entfernung dieser Klasse, so wird sie der Klassenmenge nicht wieder hinzugefügt. Dies wird so oft wiederholt, bis sich keine Verbesserung der durchschnittlichen Anzahl von Zugrien mehr ergibt. Die verbleibende Menge von Klassen wird dann zur Konstruktion eines optimierten globalen Konstanten-Pools verwendet. Dieses Verfahren liefert eine Verbesserung bei der Überdeckung der Zugriffe auf einen globalen Konstanten-Pool. Dies geschieht auf Grund der Tatsache, dass nur dann Klassen aus der Klassenmenge entfernt werden, wenn sich die durchschnittliche Anzahl der Zugrie auf den globalen Konstanten-Pool erhöht. Ist dies für keine Klasse der Fall, so wurde bereits durch die Faltung des Konstanten-Pools ein lokales Maximum bei der durchschnittlichen Anzahl der Zugrie erreicht. 3.2 Ermittlung sicher geladener Klassen In diesem Abschnitt soll ein weiteres Verfahren zur Optimierung des Ladeprozesses dargestellt werden. Bei dieser Optimierung wird das Ziel verfolgt, das Laden von Klassen eines Programms in die Startphase der virtuellen Maschine zu verlegen. Die Voraussetzung für eine Verschiebung des Ladens von Klassen zum Start der virtuellen Maschine hin ist, dass die zu ladenden Klassen zu diesem Zeitpunkt bereits vollständig in der gewünschten Ausprägung vorliegen. Es ist jedoch nur wenig sinnvoll alle Klassen eines Programms, inklusive aller benötigten zusätzlichen Klassen aus externen Bibliotheken, zur Startzeit des Programms zu laden. Dadurch würde sich die Startzeit des Programms stark verlängern. Aus diesem Grund soll die Menge der Klassen, die beim Start der virtuellen Maschine geladen wird, auf eine sinnvolle Menge eingegrenzt werden. In dieser Menge sollen nur die Klassen enthalten sein, von denen man sicher sagen kann, dass sie in einer Ausführung des untersuchten Programms geladen werden. Im Folgenden sollen diese Klassen als sicher geladene Klassen bezeichnet werden. Eine Denition dieses Typs von Klasse wird in Abschnitt 3.2.1 gegeben. Zur Ermittlung dieser Menge von Klassen wird ein Verfahren vorgestellt, das mit Hilfe der statischen Programm-Analyse genau diese Menge erzeugt. Das Analyseverfahren besteht aus drei Teilschritten. Zunächst wird in einer intraprozeduralen Analyse die Menge von sicher geladenen Klassen pro Methode ermittelt. Anschlieÿend werden die Methoden bestimmt, die während eines Programmlaufs sicher aufgerufen werden. Dies dient dazu, die Menge der untersuchten Methoden einzuschränken. Im letzten Schritt werden die zuvor ermittelten Informationen zu einer interprozeduralen Analyse kombiniert. Diese ermittelt nur diejenigen Klassen, die in Methoden sicher geladen werden, die sicher in jedem Programmlauf aufgerufen werden. Eine genaue Erläuterung dieser Analyseschritte ndet sich in Abschnitt 3.2.2. 3.2.1 Denition sicher geladener Klassen Eine Klasse wird als sicher geladen bezeichnet, wenn auf jedem Pfad durch den Ablaufgraphen eines Programms eine Stelle existiert, an der diese Klasse geladen 3.2. ERMITTLUNG SICHER GELADENER KLASSEN 29 public static void main() { A a = new A(); if (a.size == 0) { B b = new B(); C c = new C(); } else { B b = new B(); } D d = new D(); } Abbildung 3.6: Beispielprogramm zur Erläuterung der Denition sicher geladener Klassen werden muss. Als Beispiel sei das Programm in Abbildung 3.6 dargestellt. Dieses Programm besteht aus einer Methode in deren Verlauf mehrere Objekte von Klassen erzeugt werden. Einige dieser Instanzerzeugungen nden innerhalb einer Verzweigung statt. Man kann nun für die Klassen A, B und D sagen, dass sie bei Ausführung dieser Methode sicher geladen werden. Die Erzeugungen von Instanzen der Klassen A und D benden sich im Hauptstrang der Methode. Dieser wird bei einer Ausführung der Methode in jedem Fall durchlaufen, also werden auch die Instanzen der Klassen A und D erzeugt. Innerhalb der Verzweigung werden Instanzen der Klassen B und C erzeugt. Während die Instanz der Klasse C nur in einem der Teilstränge der Verzweigung erzeugt wird, kommt es in jedem der Teilstränge zu einer Erzeugung einer Instanz der Klasse B. Also wird die Klasse B sicher geladen, wohingegen man bei der Klasse C nicht sicher sagen kann, dass sie in allen Programmläufen geladen wird. Weitere Stellen an denen ein Nachladen von Klassen auftreten kann sind der Aufruf statischer Methoden und der Zugri auf statische Felder einer Klasse, sowie die Erzeugung einer Reihung mit Referenzen eines bestimmten Typs. Die Ermittlung dieser Stellen ist ein Bestandteil der gesamten Analyse zur Ermittlung von sicher geladenen Klassen. 3.2.2 Analyse zur Ermittlung sicher geladener Klassen Der erste Teil ist eine intraprozedurale Datenuss-Analyse. Für jede Methode eines Programms werden die Klassen ermittelt, die bei Ausführung der Methode sicher geladen werden. Zunächst wird für jede Methode ein Ablaufgraph erstellt. Für jeden Grundblock des Ablaufgraphen werden die Klassen ermittelt, die durch Instruktionen innerhalb des Grundblocks geladen werden. Zusätzlich müssen für jede geladene Klasse noch Superklassen und Schnittstellen berücksichtigt werden. Diese werden beim Laden einer Klasse ebenfalls geladen. Ein Beispiel eines solchen Ablaufgraphen ndet sich in Abbildung 3.7. Die Menge Gen(B) eines Grundblocks B enthält damit also alle Klassen, die während eines Durchlaufs des Grundblocks geladen werden. Die Menge Kill(B) enthält dagegen keine Elemente, da die gewonnenen Ergebnisse nicht mehr invalidiert werden können. Daraus ergeben sich also die folgenden Denitionen für die Mengen Gen und Kill 30 KAPITEL 3. KONZEPT public static void main() { A a = new A(); if (a.size == 0) { B b = new B(); C c = new C(); } else { B b = new B(); } D d = new D(); } Start . . new A() . {A} {} {A} . new B() . . new B() . new C() {A,B,C} {A} {A,B} {A,B} . . new D() . {A,B,D} Ende Abbildung 3.7: Darstellung eines Ablaufgraphen für die Datenuss-Analyse zur Ermittlung sicher geladener Klassen einer Methode C ∈ Gen(B), Kill(B) = ∅ falls es in B eine Instruktion gibt, die das Laden der Klasse C , einer ihrer Unterklasse oder ihrer Implementierungen bewirkt. Der Ablaufgraph einer Methode wird in dieser Analyse vom Start- zum Endknoten durchlaufen. Es handelt sich hier also um eine Vorwärtsanalyse. Die Analyse ermittelt alle Klassen, die auf allen Ablaufpfaden einer Methode geladen werden. Es handelt es sich bei dem hier betrachteten Problem um ein All-Problem. Deshalb muss der Operator ∩ in den Datenuss-Gleichungen zum Einsatz kommen. Die Datenuss-Gleichungen für einen Grundblock B sind dann wie folgt deniert Out(B) = In(B) = Gen(B) ∪ In(B) \ Out(V ) V ∈V org(B) In Abbildung 3.8 ist ein Beispiel für den Verband dargestellt, der sich aus dem hier beschriebenen Datenuss-Problem ergibt. Bei diesem Verband handelt es sich um einen Potenzmengenverband wie er auch bei Typanalysen zum Einsatz kommt. Jedes der Verbands-Elemente stellt eine Menge von Klassen dar, die von dem untersuchten Programm geladen werden können. Das Element ⊥ ist die leere Menge und das Element > enhält alle Klassen, die geladen werden können. Die Transferfunktion bildet die Eekte von Bytecode-Instruktionen, welche das dynamische Laden von Klassen anstoÿen können, auf diesen Verband ab. 3.2. ERMITTLUNG SICHER GELADENER KLASSEN 31 {A,B,C} {A,B} {A,C} {B,C} {A} {B} {C} {} Abbildung 3.8: Beispiel für den Verband, der als Grundlage für die intraprozedurale Analyse zur Ermittlung sicher geladener Klassen dient public static void main() { Start A a = new A(); a.method(); if (a.size == 0) { {} B b = new B(); . b.method(); . C c = new C(); a.method() c.method() . {a.method} } else { {a.method} {a.method} B b = new B(); . b.method() b.method(); b.method() . } . c.method() D d = new D(); . {a.method, {a.method, d.method(); b.method} b.method, } c.method} {a.method, . b.method} . d.method() . {a.method, b.method, d.method} Ende Abbildung 3.9: Darstellung eines Ablaufgraphen für die Datenuss-Analyse zur Ermittlung sicher aufgerufener Methoden 3.2.3 Ermittlung sicher aufgerufener Methoden Zur Ermittlung der sicher aufgerufenen Methoden wird eine weitere intraprozedurale Datenuss-Analyse angewandt. Das Analyse-Problem gestaltet sich hier identisch zu dem Problem der sicher geladenen Klassen. In dieser Analyse werden für jeden Grundblock die Mengen der aufgerufenen Methoden ermittelt (siehe Abbildung 3.9). Das Ergebnis der Analyse bilden die Methoden, die auf allen Ablaufpfaden einer Methode sicher aufgerufen werden. Eine Methode gilt genau dann als sicher aufgerufen, wenn ihr Aufrufziel eindeutig bestimmbar ist. Für diese Analyse können mehrere Verfahren zur Konstruktion von Aufrufgraphen zum Einsatz kommen. Diese bestimmen die Aufrufziele eines Methodenaufrufs unterschiedlich genau. Je genauer das Verfahren zur Konstruktion die Aufrufziele bestimmt, desto mehr sicher aufgerufene Methoden können erkannt werden. Dies kann später dazu führen, dass mehr sicher geladene Klassen identiziert werden. 32 KAPITEL 3. KONZEPT Da in dieser Analyse nur diejenigen Methoden ermittelt werden sollen, die sicher aufgerufen werden, muss bei der zuvor erläuterten Aufrufgraph-Analyse noch eine Einschränkung gemacht werden. Es gelten nur solche Methoden als sicher aufgerufen, für die ein Analyseverfahren das Aufrufziel exakt bestimmen kann. Damit sind für diese Analyse die Mengen Gen(B) und Kill(B) für einen Grundblock B wie folgt deniert M ∈ Gen(B), Kill(B) = ∅ falls es in B eine Instruktion gibt, die den Aufruf der Methode M zur Folge hat und die Methode M als Aufrufziel eindeutig ermittelbar ist. Da es sich bei diesem Analyse-Problem wieder um die Vorwärtsanalyse eines All-Problems handelt, gelten die gleichen Datenuss-Gleichungen und der gleiche Verband, wie im vorherigen Abschnitt beschrieben. 3.2.4 Ermittlung sicher erreichter Methoden Nachdem nun sowohl die sicher geladen Klassen, als auch die sicher aufgerufenen Methoden für jede Methode des Programms ermittelt wurden, müssen noch die Methoden ermittelt werden, die von einem Eintrittspunkt in das Programm aus sicher erreicht werden. Eine Methode gilt genau dann als sicher erreicht, wenn sie von einem Eintrittpunkt über sichere Methodenaufrufe erreicht wird (siehe vorherigen Abschnitt). Die hierfür verwendete Aufrufgraph-Analyse wird in diesem Abschnitt erläutert. Der Ablauf eines Java-Programms beginnt mit der Methode main. Von dieser Methode aus werden weitere Objekte erzeugt oder weitere Methoden aufgerufen. Um die Methoden zu ermitteln, die vom Eintrittspunkt des Programms aus sicher erreicht werden, wird zunächst ein Aufrufgraph mit der main-Methode als Eintrittspunkt erzeugt. Von dem Knoten der main-Methode aus wird der Aufrufgraph durchlaufen. Dabei werden nur diejenigen Aufrufe berücksichtigt, die in der vorherigen Analyse als sicher klassiziert wurden. Die Methoden, die über solche Aufrufe erreicht werden, bilden dann die Ergebnismenge dieser Analyse. In dieser Analyse werden die statischen Initialisierer (<clinit>-Methoden) einer Klasse nicht als Eintrittspunkte betrachtet. Es wäre jedoch möglich, mittels eines iterativen Verfahrens die <clinit>-Methoden der sicher geladenen Klassen zu berücksichtigen. 3.2.5 Bildung der Ergebnismenge Nachdem nun alle nötigen Informationen für die Ermittlung der Menge der sicher geladenen Klassen vorhanden sind, kann die Ergebnismenge konstruiert werden. Dazu müssen nun die Ergebnismengen, die in den oben beschriebenen Schritten erzeugt wurden, miteinander verknüpft werden. Sei die Menge SR die Menge der Methoden eines Programms, die vom Eintrittspunkt in das Programm aus sicher erreicht werden. Weiterhin betrachten wir die Menge CLm , die alle Klassen enthält, die innerhalb der Methode m sicher geladen werden. Zusammen mit den Elementen der Menge SR ergibt sich 3.2. ERMITTLUNG SICHER GELADENER KLASSEN dann C= [ 33 CLm m∈SR Die Ergebnismenge C dieser Analyse ergibt sich also aus der Vereinigung der Mengen von Klassen, die in Methoden sicher geladen werden, die vom Eintrittspunkt des Programms aus sicher erreicht werden. 34 KAPITEL 3. KONZEPT Kapitel 4 Implementierung Nachdem die Konzepte der Faltung von Konstanten-Pools und zur Ermittlung von sicher geladenen Klassen vorgestellt wurde, soll in diesem Kapitel auf die Implementierung eingegangen werden. Die Analysen wurden mit Hilfe der Umgebung zur Programm-Analyse PAULI der Arbeitsgruppe Kastens an der Universität Paderborn [THIE02] erstellt (siehe Abbildung 4.1). Diese Umgebung ermöglicht es, die Analysen als sogenannte Plugins zu realisieren und diese dann dynamisch zur Laufzeit der Umgebung zu verwenden. Es ist damit möglich, einzelne Programm-Analysen zu einer gröÿeren Analyse zu kombinieren. Wie bereits in den Darstellungen der Konzepte erläutert wurde, werden die gewonnenen Analyse-Informationen in den Klassendateien des untersuchten Programms gespeichert. Zu diesem Zweck kommt der dafür von der Java Virtual Machine Specication [JVM98] vorgesehene Erweiterungsmechanismus zum Tragen. Der Aufbau des Attributs, das die Analyse-Information für die Abbildung des Konstanten-Pools einer Klasse in einen globalen Konstanten-Pool enthält, wird in Abschnitt 4.2 erläutert. Abbildung 4.1: Aufbau der Umgebung zur Programm-Analyse PAULI 35 36 KAPITEL 4. IMPLEMENTIERUNG 4.1 Implementierung der Analysen An dieser Stelle soll nun die Implementierung der Analysen als Plugins für die Umgebung zur Programm-Analyse PAULI vorgestellt werden. Diese Umgebung bietet verschiedene Mechanismen zur Programmierung und Durchführung von Programm-Analysen, sowie der Speicherung der Analyse-Informationen. Die Programmierung von Analysen erfolgt über die Schnittstelle AnalysisPlugin. Diese Schnittstelle wird von einer zentralen Verwaltungsinstanz, dem AnalysisManager, dazu verwendet, Analysen durchzuführen und anschlieÿend die Ergebnisse dieser Analysen zu speichern. Die zentrale Datenstruktur der AnalyseUmgebung ist das ClassUniverse. Diese Datenstruktur bildet die gesamte Umgebung eines Java-Programms und der zugehörigen Bibliotheken, also alle Pakete, Klassen, Schnittstellen und Methoden, ab. Diese einzelnen Ebenen eines Programms dienen als Einteilung in sogenannte Analyse-Kontexte. Eine Programm-Analyse kann für die Durchführung von Analysen auf bestimmten Kontexten konguriert werden. Es ist also möglich, Analysen sowohl speziell auf dem Kontext von Methoden durchzuführen, als auch auf dem Kontext des gesamten Programm-Universums. Die Aufgabe des AnalysisManager besteht nun darin, eine Analyse für einen bestimmten Kontext durchzuführen und das Ergebnis zu speichern. Dabei kann eine Analyse auf einem gröÿeren Kontext gestartet werden, als es die Konguration eines Plugins erlauben würde. Der AnalysisManager extrahiert selbständig die Informationen und startet auf diesen die Analyse. So kann eine Analyse die lediglich auf einzelnen Methoden arbeitet, auf dem Kontext aller Klassen eines ClassUniverse gestartet werden. Dabei werden bereits berechnete Ergebnisse nicht erneut berechnet, sondern aus dem Zwischenspeicher des AnalysisManager ausgelesen. Die Umgebung stellt bereits einige Plugins für häug benötigte ProgrammAnalysen zur Verfügung. Hierzu gehören zum Beispiel Plugins für Analysen der Klassenhierarchie, sowie Aufrufgraph-Analysen. Diese Analysen lassen sich durch die Schnittstelle und die zentrale Verwaltungsinstanz einfach in selbst programmierten Plugins verwenden. 4.1.1 Implementierung der Faltung von Konstanten-Pools Die Analyse zur Faltung von Konstanten-Pools ist als einzelnes Plugin für die Analyse-Umgebung realisiert. Ein Klassendiagramm, das den Aufbau dieser Analyse darstellt, ndet sich in Abbildung 4.2. Das Plugin verwendet eine Datenstruktur, den globalen Konstanten-Pool, zur Speicherung der Einträge aus den Konstanten-Pools der einzelnen Klassen. Hinter dieser Datenstruktur steht die Implementierung einer Reihung in der die einzelnen Einträge des globalen Konstanten-Pools einen eindeutigen Index zugewiesen bekommen. Dieser Index der Einträge wird bei der späteren Speicherung der Mapping-Attribute in den Klassendateien des Programms verwendet. Jeder Eintrag des globalen Konstanten-Pools speichert Informationen darüber in welcher Klasse ein Eintrag mit gleichem Inhalte auftritt und welchen Index er im Konstanten-Pool der Klasse besitzt. Diese Information wird für die Erzeugung der Abbildungen in den Mapping-Attributen verwendet. Der Algorithmus zum Einfügen neuer Einträge in den globalen KonstantenPool erhält als Eingabe jeweils den Konstanten-Pool einer Klasse. Die Einträge dieses Konstanten-Pools werden dann in mehreren Phasen, wie in Abschnitt 4.1. IMPLEMENTIERUNG DER ANALYSEN 37 Abbildung 4.2: Klassendiagramm der Analyse zur Faltung von Konstanten-Pools 3.1.2 beschrieben, eingefügt. Um die Vergleichsoperationen, die für das Einfügen eines neuen Eintrags durchgeführt werden, zu beschleunigen, werden zusätzliche Hashtabellen verwendet. Es existiert je eine Hashtabelle für jeden Typ von Eintrag, der in einem Konstanten-Pool vorkommen kann. Die Einträge in den Hashtabellen verwenden jeweils den Inhalt eines Eintrags aus den KonstantenPools als Schlüssel. Der zugehörige Wert besteht aus dem Index des Eintrags im globalen Konstanten-Pool. Da das Aunden von Schlüsselwerten in Hashtabellen deutlich schneller ist, als die Suche von Einträgen in einer Reihung, führt diese Maÿnahme zu einer deutlichen Steigerung der Geschwindigkeit des Algorithmus. Zusätzlich zu den oben genannten Hashtabellen wird noch eine zusätzliche Hashtabelle eingeführt, welche die Namen von Klassen auf ihre zugehörigen Einträge im globalen Konstanten-Pool abbildet. Diese Maÿnahme dient der Beschleunigung der Erzeugung von Mapping-Attributen einer Klasse. Durch diese Zuordnung lassen sich die, für eine Klasse relevanten, Einträge im globalen Konstaten-Pool schnell bestimmen und anschlieÿend die Abbildungs-Informationen aus diesen Einträgen auslesen. Für die in Abschnitt 3.1.4 erläuterten Verfahren zur Optimierung des Transportformates wurde eine Schnittstelle nach dem Strategie-Muster implementiert, um verschiedene Implementierungen von Optimierungsstrategien leichter austauschen zu können. 38 KAPITEL 4. IMPLEMENTIERUNG Abbildung 4.3: Klassendiagramm der Analyse zur Ermittlung sicher geladener Klassen 4.1.2 Implementierung der Ermittlung sicher geladener Klassen Die Analyse zur Ermittlung sicher geladener Klassen wurde durch mehrere Plugins für die Analyse-Umgebung realisiert, wie in Abbildung 4.3 dargestellt. Es wurde zu jeder der in Abschnitt 3.2 vorgestellten Analysen je ein Plugin, sowie ein Plugin zur Steuerung der gesamten Analyse implementiert. Bei der Implementierung wurde auf verschiedene bereits existierende Analysen zurückgegrien. So verwendet zum Beispiel die Analyse zur Ermittlung sicher geladener Klassen einer Methode die Klassenhierarchie-Analyse, um die Oberklassen und Schnittstellen einer sicher geladenen Klasse zu bestimmen. Weiterhin kommen verschiedene Aufrufgraph-Analysen zum Einsatz, um sicher aufgerufene und sicher erreichte Methoden zu bestimmen. Die beiden Datenuss-Analysen zur Ermittlung sicher geladener Klassen und sicher aufgerufener Methoden wurden mit Hilfe der Bibliothek vDFA implementiert, die allgemeine Datenstrukturen und Lösungsverfahren für Datenussprobleme zur Verfügung stellt. Zur Implementierung einer solchen DatenussAnalyse stellt diese Bibliothek eine Schnittstelle zur Verfügung, die bereits den Lösungsalgorithmus für Datenuss-Probleme enthält. Es ist dann nur noch nötig, eine Datenstruktur für den Verband der Datenuss-Analyse und eine Bytecode-Transferfunktion zu implementieren. Die hier realisierte Datenstruktur für die Repräsentation eines Verbandes beschreibt einen allgemeinen Potenz-mengen-Verband. Diese Implementierung eines mengen-basierten Verbandes wird sowohl für die Ermittlung sicher geladener Klassen, als auch für die Ermittlung sicher aufgerufener Methoden verwendet. Diese beiden Verfahren werden durch Datenuss-Analysen realisiert, die auf Mengen von Klassen beziehungsweise Mengen von Methoden arbeiten. Die Bytecode-Transferfunktion führt Abbildungen von Datenuss-Informationen für bestimmte Bytecode-Instruktionen auf dem Verband durch. Für die 4.2. AUFBAU DES MAPPING-ATTRIBUTS Klassendatei Konstanten-Pool #1 #2 #3 #4 . . . #n Mapping-Attr. #1 -> #5 #2 -> #2 #3 -> #10 #4 -> #6 . . . #n -> #m 39 Virtuelle Maschine Klassenlader globaler Konstanten-Pool #1 #2 . . . #5 #6 . . . #10 #m Abbildung 4.4: Darstellung des Ladeprozesses einer annotierten Klassendatei. Der Klassenlader verwendet das Mapping-Attribut, um die Einträge des Konstanten-Pools in den globalen Konstanten-Pool abzubilden. Implementierung von Transferfunktionen stellt die Bibliothek ebenfalls eine Schnittstelle bereit. Die darin denierte map-Methode wird von dem Lösungsalgorithmus für jede Bytecode-Instruktion in einem Grundblock aufgerufen. Für bestimmte Instruktionen können dann Abbildungen von einer Eingangs- auf eine Ausgangslösung auf dem Verband implementiert werden. So wird bei der Analyse sicher geladener Klassen einer Methode für jede Instruktion, welche das dynamische Laden von Klassen anstoÿen kann, die zu ladende Klasse, sowie Oberklassen und Schnittstellen ermittelt und einem Verbandselement hinzugefügt. Analog wird bei der Ermittlung sicher aufgerufener Methoden mit Aufrunstruktionen verfahren. 4.2 Aufbau des Mapping-Attributs In diesem Abschnitt wird der Aufbau des Mapping-Attributs beschrieben, das zur Speicherung der Analyse-Informationen aus der Faltung von KonstantenPools dient. Dieses Mapping-Attribut enthält Abbildungen der Indices von Einträgen des Konstanten-Pools einer Klasse auf Indices im globalen KonstantenPool einer Ladeeinheit. Aus diesen Informationen kann ein modizierter Klassenlader einer virtuellen Maschine den globalen Konstanten-Pool zur Ladezeit rekonstruieren (siehe Abbildung 4.4). Der Aufbau des Mapping-Attributs ist in Abbildung 4.5 dargestellt. Dieser Aufbau entspricht den Vorgaben, welche die Java Virtual Machine Specication [JVM98] zum Aufbau von Attributen deniert. Der erste Eintrag in diesem Attribut ist der Name des Attributs. Anhand dieses Eintrags kann der Klassenlader bestimmen, um welchen Typ von Attribut es sich handelt, und die folgenden Daten entsprechend verarbeiten. Bei den folgenden Daten handelt es sich um die Abbildungen selbst. Hier wird für jeden Eintrag im Konstanten-Pool der Klasse eine Abbildung abgespeichert. Der Aufbau der Abbildung einfacher Einträge im Konstanten-Pool ist in Ab- 40 KAPITEL 4. IMPLEMENTIERUNG u1 := unsigned byte; u2 := u1[2]; u4 := u1[4]; MappingAttribute { u2 attribute_name_index; u4 attribute_length; u1 attribute_data[attribute_length]; } Abbildung 4.5: Aufbau des Mapping-Attributs SimpleMapping { u1 constant_tag; u2 source_index; u4 target_index; } Abbildung 4.6: Aufbau eines Eintrags im Mapping-Attribut für einfache Einträge im Konstanten-Pool bildung 4.6 dargestellt. Der erste Wert der Abbildung dient zu ihrer Identizierung. Dieser Wert entspricht dem Tag-Wert, welcher den Typ des abgebildeten Eintrags aus dem Konstanten-Pool deniert (siehe Tabelle 2.1). Auf Grund dieses Wertes kann der Klassenlader der virtuellen Maschine die restlichen Daten der Abbildung korrekt laden. Diese bestehen aus dem Index des abzubildenden Eintrags im Konstanten-Pool der Klasse, sowie dem Index dieses Eintrags im globalen Konstanten-Pool. In Abbildung 4.7 ist die Abbildung eines zusammengesetzten Eintrags aus dem Konstanten-Pool einer Klasse am Beispiel des Typs CONSTANT_NameAndType dargestellt. Neben der Abbildung des eigentlichen NameAndTypeEintrags müssen hier auch noch die Indices der Einträge im globalen KonstantenPool angegeben werden, auf welche dieser Eintrag verweist. Als Besonderheit im Aufbau der Mapping-Attribute ist zu erwähnen, dass die Abbildung der Quell-Indices mit einer Länge von 16 Bit auf einen ZielIndex mit einer Länge von 32 Bit stattndet. Der Grund hierfür ist, dass in einem globalen Konstanten-Pool deutlich mehr als 65536 Elemente enthalten sein können. In einer herkömmlichen Klassendatei ist dieser Wert jedoch völlig NameAndTypeMapping { u1 constant_tag; u2 source_index; u4 target_index; u4 name_target_index; u4 type_target_index; } Abbildung 4.7: Aufbau des Eintrags im Mapping-Attribut für Einträge des Konstanten-Pools vom Typ CONSTANT_NameAndType 4.2. AUFBAU DES MAPPING-ATTRIBUTS 41 ausreichend. Aus dieser Entscheidung beim Entwurf der Abbildung ergeben sich allerdings Konsequenzen für die Implementierung einer modizierten virtuellen Maschine. Jeder Bytecode-Instruktion, die auf den Konstanten-Pool zugreift, kann als Index-Parameter lediglich ein 16-Bit Wert übergeben werden. Dies bedeutet, dass ausgehend von herkömmlichen Bytecode-Instruktionen nicht auf einen globalen Konstanten-Pool mit mehr als 216 Elementen zugegrien werden kann. In Umgebungen wie der JavaCard-Plattform lässt sich dieses Problem umgehen, indem man die Indices im globalen Konstanten-Pool auf 16-Bit Werte beschränkt. Da auf dieser Plattform nur sehr wenige Klassen in einer Ladeeinheit enthalten sind, ist diese Gröÿe ausreichend. Auf anderen Plattformen hat man die Möglichkeit, aus der Menge der Klassen einer Ladeeinheit maximal soviele Klassen auszuwählen, dass ein globaler Konstanten-Pool mit höchstens 216 Einträgen entsteht bei dem die Zugrie von Klassen auf den globalen Konstanten-Pool eine hohe Überdeckung aufweisen. Die Optimierungsstrategie lässt sich für eine solche Auswahl anwenden. Weiterhin lässt sich das Mapping-Attribut auf mehrere globale Konstanten-Pools erweitern. Daraus folgt, dass das hier beschriebene Verfahren dafür vorbereitet ist, die Idee beschränkter globaler Konstanten-Pools umzusetzen. 42 KAPITEL 4. IMPLEMENTIERUNG Kapitel 5 Evaluation In diesem Kapitel werden die Ergebnisse der Anwendung der Verfahren zur Faltung von Konstanten-Pools und der Ermittlung sicher geladener Klassen diskutiert. Für die Faltung von Konstanten-Pools wurden mehrere Programme und Bibliotheken mit dem implementierten Algorithmus bearbeitet. Diese sind das JavaCard 2.2.2 Framework, das J2ME CLDC 1.1 Framework, die Standard Widget Toolkit 3.2 Bibliothek, sowie die Laufzeit-Bibliotheken der Java-Umgebungen von IBM und SUN in der Version 1.5. Diese Bibliotheken unterscheiden sich bezüglich der Anzahl von Einträgen in den KonstantenPools. Während die Bibliothek des JavaCard Framework lediglich 3.784 Einträge enthält, so sind es bei der Laufzeit-Bibliothek der Java-Umgebung von SUN 1.519.032 Einträge. Durch diese Auswahl soll ein möglichst breites Spektrum von Bibliotheken beleuchtet werden, um die Eignung des Verfahrens für verschiedene Anwendungsfälle zu ermitteln. Für die oben genannten Bibliotheken wurde die Speicherersparnis durch die Verwendung eines globalen Konstanten-Pools bestimmt. Die Speicherersparnis konnte jedoch nicht direkt ermittelt werden, da keine modizierte virtuelle Maschine zur Verfügung stand, die mit globalen Konstanten-Pools umgehen konnte. Stattdessen wurde die Anzahl der Einträge in den Konstanten-Pools als Kenngröÿe für die Speicherersparnis verwendet. Dieser Wert kann mit der realen Speicherersparnis direkt in Beziehung gesetzt werden. Neben den positiven Eekten durch die Speicherersparnis zur Laufzeit des Programms, wurde sowohl die Gröÿe der Ladeeinheit nach der kompletten Faltung der Konstanten-Pools, als auch die Auswirkungen der Strategien zur Optimierung des Transportformats untersucht. Auch hier wurde die Gröÿe der Ladeeinheiten und die Anzahl der Einträge in den globalen Konstanten-Pools nach der Optimierung bestimmt. Die Evaluierung des Verfahrens zur Ermittlung sicher geladener Klassen ndet sich in Abschnitt 5.3. Auch für diese Evaluierung wurden mehrere Programme ausgewählt auf die das Verfahren angewandt wurde. Die wichtigste Kenngröÿe ist hierbei die Anzahl der sicher geladenen Klassen im Vergleich zu den tatsächlich im Progammlauf geladenen Programm-Klassen. Auÿerdem wurde die Anzahl der Methoden bestimmt, die als sicher erreicht klassiziert wurden und mit der Anzahl der erreichbaren Methoden des untersuchten Programms verglichen. Die Messungen wurden jeweils mit unterschiedlichen Verfahren zur Konstruktion von Aufrufgraphen durchgeführt, um zu ermitteln, inwiefern diese 43 44 KAPITEL 5. EVALUATION Wahl Auswirkungen auf das Endergebnis hat. 5.1 Faltung von Konstanten-Pools Name JavaCard Framework 2.2.2 J2ME CLDC API 1.1 SWT 3.2 (für Linux) Java 1.5 core.jar (IBM) Java 1.5 rt.jar (SUN) #Klassen 58 98 597 3955 13182 #KP-Einträge 3.784 6.682 92.710 501.039 1.519.032 Gröÿe (in byte) 55.639 116.339 1.360.551 6.736.860 19.537.267 Tabelle 5.1: Eigenschaften der Ladeeinheiten, die zur Evaluation der Faltung von Konstanten-Pools verwendet wurden. In diesem Abschnitt sollen die Auswirkungen der Faltung von KonstantenPools auf ausgewählte Ladeeinheiten untersucht werden. Die wichtigsten Eigenschaften der Ladeeinheiten sind in Tabelle 5.1 dargestellt. Auf diese Ladeeinheiten wurde der Algorithmus zur Faltung von Konstanten-Pools angewandt. In Abbildung 5.1 ist die Anzahl der Einträge in den globalen KonstantenPools der Ladeeinheiten relativ zu der Summe der Einträge in den KonstantenPools der Klassen aufgetragen. Man sieht, dass sich durch die Faltung der Konstanten-Pools eine Verringerung der Anzahl der Einträge zwischen 47% und 73% ergibt. Die unterschiedlich starke Verringerung hängt mit der ursprünglichen Anzahl von Einträgen in den Konstanten-Pools der Klassen zusammen. Enthält eine Ladeeinheit viele Klassen, und somit auch Einträge in den Konstanten-Pools, so steigt die Wahrscheinlichkeit, dass viele Einträge mit identischem Inhalt auftreten. Deshalb ergibt sich für groÿe Ladeeinheiten eine stärkere Verringerung in der Zahl von Einträgen. Für diese Gröÿe der Ladeeinheit wurden die gemessenen Werte relativ zur Originalgröÿe der Ladeeinheit in Byte aufgetragen, wie in Abbildung 5.2 zu sehen ist. Es ergeben sich Vergröÿerungen der Ladeeinheiten zwischen 31% und 51%. Die stark unterschiedliche Verteilung der Vergröÿerungen hängt mit der Zusammensetzung der ursprünglichen Konstanten-Pools zusammen. Enthalten sie zum Beispiel viele Referenz-Einträge, so müssen viele teure Abbildungen in die annotierten Ladeeinheiten eingefügt werden, was zu einer starken Vergröÿerung der Ladeeinheit führt. Ein weiterer Faktor für die Vergröÿerung ist der Zusammenhang der Klassen in der Ladeeinheit untereinander. Ist der Zusammenhang der Klassen stark, so enthalten ihre Konstanten-Pools viele Einträge gleichen Inhalts. Bei besonders stark zusammenhängenden Klassen lässt sich also eine bessere Speicherersparnis erzielen. 5.2 Optimierung des Transportformates In diesem Abschnitt werden die beiden vorgestellten Verfahren zu Optimierung des Transportformates für annotierte Ladeeinheiten untersucht. Hierzu wurden die Optimierungsverfahren auf die in Tabelle 5.1 vorgestellten Ladeeinheiten angewandt. Zu den ermittelten Messwerten gehören die Anzahl der Einträge in den Konstanten-Pools und die Gröÿe der Ladeeinheit. 5.2. OPTIMIERUNG DES TRANSPORTFORMATES 45 Abbildung 5.1: Anzahl der Einträge im globalen Konstanten-Pool relativ zur Summe der Einträge in den Konstanten-Pools der Klassen der jeweiligen Ladeeinheit Abbildung 5.2: Vergröÿerung der Ladeeinheiten nach der Faltung der Konstanten-Pools relativ zur Originalgröÿe 46 KAPITEL 5. EVALUATION Die Anzahl der Einträge in den optimierten Ladeeinheiten setzt sich aus zwei Gröÿen zusammen. Dies ist zum einen die Anzahl der Einträge, die im jeweilligen globalen Konstanten-Pool abgelegt sind. Zum anderen müssen die Einträge aus Konstanten-Pools von Klassen berücksichtigt werden, die von der Konstruktion des optimierten globalen Konstanten-Pools ausgeschlossen wurden. Alle Messwerte wurden mit den Werten der Original-Ladeeinheiten verglichen, um die Eekte der Optimierungen gegenüber einer Faltung der KonstantenPools ohne Optimierung besser sichtbar zu machen. 5.2.1 Optimierung über die Anzahl von Zugrien Dieses Optimierungsverfahren lässt sich über den Grenzwert-Parameter t steuern. Dieser bestimmt, welche Klassen in die Konstruktion des globalen Konstanten-Pools einbezogen werden. Die oben genannten Messwerte wurden für verschiedene Grenzwerte t bestimmt. In Abbildung 5.3 ist die Gesamtanzahl der Einträge aus in den jeweiligen Ladeeinheiten nach der Optimierung relativ zur Original-Ladeeinheit dargestellt. Bei einem Grenzwert t = 0 wird die Konstruktion eines globalen KonstantenPools verhindert, da es nur in sehr seltenen Fällen Klassen gibt, die keinen Eintrag im globalen Konstanten-Pool mit einer Zugriszahl z(e) = 1 besitzen. Die Speicherersparnis ist also nicht vorhanden und es wird genauso viel Speicher zur Laufzeit des Programms verbraucht, als würde die Original-Ladeeinheit verwendet. Bei einem Grenzwert t = 1 werden alle Klassen der Ladeeinheit zur Konstruktion des globalen Konstanten-Pools verwendet. Damit ergibt sich die gleiche Speicherersparnis wie im nicht-optimierten globalen Konstanten-Pool. Wie sich die Optimierung auf die Zahl der Einträge und somit auf die Speicherersparnis auswirkt, hängt vom Aufbau der Konstanten-Pools einer Ladeeinheit und der Wahl des Grenzwertes t ab. Für gröÿere Ladeeinheiten kann man ab einem Wert von 0,4 bis 0,5 eine Speicherersparnis von 45% bis 70% gegenüber der Original-Ladeeinheit beobachten. Bei kleinen Ladeeinheiten muss ein gröÿerer Wert für t gewählt werden um eine hohe Speicherersparnis zu erreichen. Die Gröÿe der Ladeeinheiten nimmt mit steigendem Grenzwert t zu, wie in Abbildung 5.4 dargestellt. Um eine spürbare Verringerung der Gröÿe zu erreichen, muss ein Grenzwert von 0,2 bis 0,3 gewählt werden. Damit lassen sich Einsparungen von 20% bis 30% gegenüber der nicht optimierten Ladeeinheit erbringen. Allerdings verschlechtert sich bei Grenzwerten in diesem Bereich die Speicherersparnis im Vergleich zu einem nicht-optimierten globalen KonstantenPool. Zusammenfassend ist der Erfolg dieser Optimierung stark von der Gröÿe und dem Aufbau der Ladeeinheit, sowie der richtigen Wahl des Grenzwertes abhängig. Liegt der Fokus der Optimierung auf der Speicherersparnis, so müssen für groÿe Ladeeinheiten Grenzwerte zwischen 0,4 und 0,5 gewählt werden. Soll hingegen die Gröÿe der Ladeeinheit stärker verringert werden, so müssen Grenzwerte zwischen 0,2 und 0,3 gewählt werden, was jedoch zu Lasten der Speicherersparnis geht. Der Vorteil dieses Verfahrens ist jedoch die Möglichkeit zur Steuerung über den Grenzwert t. Damit lässt sich die Optimierung an die Anforderungen eines Anwenders anpassen. 5.2. OPTIMIERUNG DES TRANSPORTFORMATES 47 Abbildung 5.3: Anzahl der Einträge in den optimierten Ladeeinheiten nach Optimierung über die Anzahl der Zugrie (Relativ zur Anzahl der Einträge der Original-Ladeeinheiten) Abbildung 5.4: Gröÿe der optimierten Ladeeinheiten nach Optimierung über die Anzahl der Zugrie (Relativ zur Gröÿe der Original-Ladeeinheit) 48 KAPITEL 5. EVALUATION Abbildung 5.5: Anzahl der Einträge in den optimierten Ladeeinheiten nach Optimierung über die durchschnittliche Anzahl der Zugrie (Relativ zur Anzahl der Einträge in den Original-Ladeeinheiten) 5.2.2 Optimierung über die durchschnittliche Anzahl von Zugrien Zur Ermittlung der Ergebnisse dieser Optimierung wurden dieselben Kenngröÿen wie im vorigen Abschnitt ermittelt. Dieses Verfahren läuft automatisch ab und kann nicht durch Parameter gesteuert werden. In Abbildung 5.5 ist die Gesamtzahl der Einträge der optimierten Ladeeinheiten relativ zur Original-Ladeeinheit dargestellt. Die Speicherersparnis bei dieser Optimierung liegt zwischen 17% und 47%. Man erkennt in diesem Diagramm die weiter oben beschriebene Zusammensetzung der Gesamtzahl der Einträge. Die Zahl von Einträgen in den globalen Konstanten-Pools ist relativ gering. Nur ca. 10% bis 20% der Einträge aus der Original-Ladeeinheit wurden in den globalen Konstanten-Pool eingefügt. Dadurch wird eine groÿe Zahl von Einträgen aus Konstanten-Pools von Klassen, die nicht auf einen globalen KonstantenPool zugreifen, auf herkömmliche Weise im Speicher einer virtuellen Maschine abgelegt. Aus den zuvor ermittelten Ergebnissen folgt, dass es bei dieser Optimierung zu einer spürbaren Verringerung der Gröÿe der annotierten Ladeeinheiten kommt. Dies wird durch die ermittelten Messwerte bestätigt, wie in Abbildung 5.6 dargestellt. Die Gröÿe der optimierten Ladeeinheiten liegt im Schnitt ca. 20% niedriger, als bei der entsprechenden nicht optimierten Ladeeinheit mit globalem Konstanten-Pool. 5.2.3 Vergleich der Verfahren In diesem Abschnitt werden die beiden vorgestellten Optimierungsverfahren verglichen. Dazu werden die Kenngröÿen über die Gesamtzahl der Einträge in den 5.2. OPTIMIERUNG DES TRANSPORTFORMATES 49 Abbildung 5.6: Gröÿe der optimierten Ladeeinheiten über die durchschnittliche Anzahl der Zugrie (Relativ zur Gröÿe der Original-Ladeeinheit) Konstanten-Pools und die Gröÿe der Ladeeinheiten, die von beiden Verfahren erzeugt wurden, direkt gegenübergestellt. In den Abbildungen 5.7 und 5.8 ndet sich die Gegenüberstellung der Messwerte. Aus einem Vergleich der beiden Diagramme erkennt man, dass für bestimmte feste Grenzwerte t von beiden Verfahren Ladeeinheiten mit nahezu gleicher Anzahl von Einträgen und nahezu gleicher Gröÿe erzeugt werden. Als Beispiel sind in Tabelle 5.2 die Kenngröÿen für die SWT-Bibliothek für den Grenzwert t = 0, 3 dargestellt. Die beiden optimierten Ladeeinheiten besitzen nahezu die gleiche Gröÿe. Die Anzahl der Einträge in den Konstanten-Pools und damit auch die Anzahl der Abbildungen unterscheidet sich nur geringfügig. Einzig in der Anzahl der Klassen, die auf den globalen Konstanten-Pool zugreifen, unterscheiden sich beide Ladeeinheiten. Während in der Ladeeinheit, die mit dem Verfahren zur Optimierung über die Anzahl der Zugrie erzeugt wurde, nur 70% der Klassen auf den globalen Konstanten-Pool zugreifen, sind es in der anderen optimierten Ladeeinheit 90% der Klassen. Vergleicht man die Struktur der globalen Konstanten-Pools der beiden Ladeeinheiten, so ergeben sich daraus Rückschlüsse auf die Art von Einträgen, die beide Verfahren bevorzugt in den globalen Konstanten-Pool einfügen. Eine Aufschlüsselung dieser Struktur für die optimierten Ladeeinheiten der SWT-Bibliothek ndet sich in Tabelle 5.3. Das Verfahren zur Optimierung über die Anzahl der Zugrie fügt bevorzugt zusammengesetzte Einträge in den globalen Konstanten-Pool ein. Im Gegensatz dazu fügt das Verfahren zur Optimierung über die durchschnittliche Anzahl von Zugrien bevorzugt einfache Einträge in den globalen Konstanten-Pool ein. Besonders deutlich zeigt sich, dass das Verfahren zur Optimierung über die durchschnittliche Anzahl von Zugrien deutlich mehr Einträge vom Typ CONSTANT_Utf8 in den globalen Konstanten-Pool einfügt. Durch das Verfahren zur Optimierung über die durchschnittliche Anzahl von Zugrien werden also deutlich mehr Klassen für die Konstruktion des glo- 50 KAPITEL 5. EVALUATION Abbildung 5.7: Vergleich der Optimierungsverfahren über die Anzahl von Einträgen in den optimierten Ladeeinheiten Abbildung 5.8: Vergleich der Optimierungsverfahren über die Gröÿe der optimierten Ladeeinheiten 5.3. SICHER GELADENE KLASSEN Gröÿe der Ladeeinheit #Einträge gKP #Einträge nicht-gKP #Klassen gKP #Abbildungen nicht opt. 1.957.177 92.710 0 597 92710 51 t = 0, 3 1.717.016 15.890 37.661 423 55.049 durchschn. Anzahl 1.715.551 16.532 38.217 535 54.493 Tabelle 5.2: Vergleich der Messwerte der optimierten Ladeeinheiten am Beispiel der SWT-Bibliothek CONSTANT_Integer CONSTANT_Float CONSTANT_Long CONSTANT_Double CONSTANT_String CONSTANT_Class CONSTANT_NameAndType CONSTANT_Fieldref CONSTANT_Methodref CONSTANT_InterfaceMethodref CONSTANT_Utf8 t = 0, 3 122 5 44 4 248 507 2962 1790 3603 13 6592 durchschn. Anzahl 135 5 48 4 330 622 2815 1750 3229 18 7576 Tabelle 5.3: Aufschlüsselung der Typen von Einträgen in den globalen Konstanten-Pools der optimierten Ladeeinheiten der SWT-Bibliothek balen Konstanten-Pools verwendet. Allerdings werden bei einer Ladeeinheit, die mit diesem Verfahren erzeugt wurde, ebenso deutlich mehr zusammengesetzte Einträge auf herkömmliche Weise im Speicher der virtuellen Maschine abgelegt. Wie sich dieser Unterschied zwischen den beiden Verfahren auf den Speicherverbrauch in einer virtuellen Maschine auswirkt, müsste durch Messungen in einer virtuellen Maschine belegt werden, die jedoch nicht Teil dieser Arbeit sind. 5.3 Sicher geladene Klassen In diesem Abschnitt werden die Ergebnisse des Verfahrens zur Ermittlung sicher geladener Klassen betrachtet. Das Verfahren wurde auf mehrere Programme angewandt, die in Tabelle 5.4 aufgelistet sind. Die Programme Testcase1 bis Testcase3 sind einfache Test-Programme im Stil des Programms aus Abbildung 3.6. Bei SwingSet2 und Stylepad handelt es sich um zwei DemonstrationsProgramme aus dem Java Development Kit von SUN. Das Programm javac ist der Java-Übersetzer aus dem JDK und javacc ist ein Generator für Syntaxanalysatoren. Die Auswahl der Programme geschah auf Grund ihres unterschiedlichen Umfangs. Für jedes dieser Programme wurde die Zahl der Klassen in der Ladeeinheit des Programms bestimmt. Diese Zahl soll zeigen, wieviele Klassen das Programm selbst in die Menge aller zur Laufzeit geladenen Klassen einbringt. Des Weiteren wurde die Zahl der Programmklassen bestimmt, die zur Laufzeit der Programme geladen werden. Diese Zahl soll zum Vergleich zwischen 52 KAPITEL 5. EVALUATION Programm #Prg.-Klassen #Geladene Prg.-Klassen Testcase1 Testcase2 Testcase3 SwingSet2 Stylepad javac javacc 5 5 5 131 30 196 140 5 5 5 131 26 175 62 Sicher geladene Kl. CHA RTA 7 7 6 6 7 7 71 71 41 41 25 25 30 30 Tabelle 5.4: Programme und Messergebnisse für die Evaluation der Ermittlung sicher geladener Klassen der Zahl der tatsächlich geladenen Programmklassen und den Klassen dienen, die als sicher geladen klassizierten wurden. Der Algorithmus bietet die Möglichkeit unterschiedliche Verfahren zur Konstruktion eines Aufrufgraphen zu verwenden. Um die Auswirkungen der unterschiedlichen Verfahren zu überprüfen, wurde der Algorithmus jeweils mit dem CHA- und dem RTA-Verfahren auf alle Programme angewandt. Es stehen noch verschiedene andere Verfahren zur Konstruktion zur Verfügung. Diese wurden jedoch auf Grund von Fehlern nicht verwendet. In Tabelle 5.4 sind Anzahl der sicher geladenen Klassen der Programme für das CHA- und das RTA-Verfahren aufgeführt. Aus den ermittelten Zahlen wird deutlich, dass die Auswahl der unterschiedlichen Verfahren zur Konstruktion von Aufrufgraphen keinen Einuss auf das Ergebnis hat. Andere Verfahren könnten hier genauere Ergebnisse für die Mengen der sicher aufgerufenen und der sicher erreichten Methoden erzeugen. Weiterhin könnten sich die Ergebnisse für noch gröÿere Programme stärker unterscheiden. Für die Programme Testcase1 bis Testcase3 liefert der Algorithmus genaue Ergebnisse. Dies ist zum einen darauf zurückzuführen, dass diese Programme nur wenige Klassen enthalten. Zum anderen sind sie speziell so implementiert worden, dass nahezu alle Klassen sicher geladen werden. Mit Hilfe dieser Programme konnte gezeigt werden, dass der Algorithmus korrekt arbeitet. Bei einigen Testprogrammen ermittelt der Algorithmus eine gröÿere Zahl von Klassen die sicher geladen werden, als tatsächlich in der Ladeeinheit des Programms vorhanden sind. Die zusätzlichen Klassen sind hierbei Bibliotheksklassen, die in dem Programm verwendet werden. Für die anderen untersuchten Programme ist die Erkennungsleistung des Algorithmus stark unterschiedlich. So werden für die Programme SwingSet2 und javacc ca. 50% der geladenen Programmklassen als sicher geladen klassiziert. Für das Programm javac sind dies lediglich 14%. Im Gegensatz dazu werden bei dem Programm Stylepad sogar mehr Klassen als sicher geladen identiziert, als in der Ladeeinheit enthalten sind. Dies ist wiederum auf die Verwendung von Bibliotheksklassen zurückzuführen Die stark unterschiedliche Erkennungsleistung ist auf den Aufbau der Programme und die sehr konservativen Abschätzungen bei den Datenuss-Analysen zur Ermittlung sicher geladener Klassen und sicher aufgerufener Methoden zurückzuführen. Dort werden sicher geladene Klassen beziehungsweise sicher aufgerufene Methoden nur dann in die Ergebnismenge aufgenommen, wenn sie in 5.3. SICHER GELADENE KLASSEN Programm Erreichbare Methoden Testcase1 Testcase2 Testcase3 SwingSet2 Stylepad javac javacc 6 10 12 519 121 2163 2097 53 Sicher erreichte Methoden CHA RTA 7 7 6 6 11 11 185 175 70 70 36 42 40 40 Tabelle 5.5: Gegenüberstellung der erreichbaren Methoden und der sicher erreichten Methoden für die Testprogramme jeder Verzweigung einer untersuchten Methode vorhanden sind. Dies ist jedoch in realen Programmen selten der Fall. Bei der Betrachtung der Klassen, die von einer virtuellen Maschine mit den Testprogrammen geladen wurden zeigte sich, dass die als sicher geladen identizierten Klassen bereits bei der Initialisierung der Programme geladen werden. Um dies zu belegen wurden die Programme javac und javacc ohne eine Eingabedatei gestartet. In der ausgegebenen Menge der geladenen Klassen waren die als sicher geladen identizierten Klassen bereits enthalten. Häug handelt es sich hierbei um Klassen, von denen bei Ausführung der main-Methode Objekte erzeugt werden. Eine weitere wichtige Kenngröÿe für die Qualität des Algorithmus ist die Anzahl der Methoden, die als sicher erreicht klassiziert werden. Um die Leistung des Algorithmus in diesem Bereich zu beurteilen, wurde zunächst die Anzahl der erreichbaren Methoden für einen bestimmten Eintrittspunkt ermittelt. Anschlieÿend wurde die Zahl der Methoden ermittelt, welche der Algorithmus unter Anwendung der CHA- und RTA-Verfahren als sicher erreicht klassiziert. Die entsprechenden Messwerte sind in Tabelle 5.5 dargestellt. Auch für die Anzahl der sicher erreichten Methoden sind die Messwerte stark vom Aufbau der Programme abhängig. Für die einfachen Testprogramme Testcase1 bis Testcase3 werden nahezu alle erreichbaren Methoden als sicher erreicht erkannt. Bei den Programmen SwingSet2 und Stylepad werden je 35% und 57% der Methoden als sicher erreicht erkannt. Dies hängt damit zusammen, dass viele der sicheren Methodenaufrufe gleich beim Start der Programme durchgeführt werden. Für die Programme javac und javacc liegt der Anteil der sicher erreichten Methoden bei ca. 2%. Bei diesen Programmen verteilen sich die Methodenaufrufe über viele Ablaufpfade. Sie können also von dem Algorithmus nicht als sicher aufgerufen und damit als sicher erreicht klassiziert werden. Besonders auällig ist, dass für das Programm SwingSet2 durch Verwendung des RTA-Verfahrens weniger Methoden als sicher erreicht klassiziert werden, als mit dem CHA-Verfahren. Da das RTA-Verfahren durch zusätzliche Nutzung von Typ-Informationen die Mengen von Aufrufzielen besser eingrenzt als das CHA-Verfahren, sollten auch mehr Methoden als sicher erreicht erkannt werden. Es konnte jedoch nicht abschlieÿend geklärt werden, weshalb in diesem Fall das RTA-Verfahren schlechtere Ergebnisse lieferte. Es wird jedoch vermutet, dass die Implementierung des RTA-Verfahrens nicht auf dem CHA-Verfahren aufbaut und das es deshalb zu unterschiedlichen Ergebnissen kommt. 54 KAPITEL 5. EVALUATION Zusammenfassend konnte durch die Anwendung des Algorithmus auf einfache Testprogramme gezeigt werden, dass dieser korrekt arbeitet und viele Programmklassen als sicher geladen identiziert. Für komplexere Programme hängt die Leistung des Algorithmus stark vom Aufbau des zu analysierenden Programms ab. Es kann dabei vorkommen, dass mehr Klassen als sicher geladen identiziert werden, als in der Ladeeinheit des Programms enthalten sind. Ebenso können nur sehr wenige Klassen als sicher geladen erkannt werden. Dies ist meist dann der Fall, wenn direkt am Eintrittspunkt in das Programm Verzweigungen auftreten in deren Ablaufpfaden disjunkte Mengen von Klassen geladen werden. Kapitel 6 Zusammenfassung Verfahren In dieser Arbeit wurden zwei Verfahren zur Optimierung des Lade- prozesses von Java-Programmen vorgestellt. Eines dieser Verfahren dient zur Faltung von Konstanten-Pools der Klassen einer Ladeeinheit. Durch diese Faltung wird der Speicherverbrauch eines Programms in einer virtuellen Maschine verringert. Zu diesem Zweck wurde eine neue Datenstruktur, der globale Konstanten-Pool einer Ladeeinheit, entwickelt. Weiterhin wurden Attribute, ein Erweiterungmechanismus für Java-Klassendateien, zur Speicherung der Informationen über den globalen Konstanten-Pool verwendet. Die Informationen werden in den ursprünglichen Klassendateien der Ladeeinheit gespeichert. Dies ermöglicht die Verwendung der Ladeeinheiten sowohl in einer modizierten, als auch in einer herkömmlichen virtuellen Maschine. Eine modizierte virtuelle Maschine nutzt die zusätzlichen Informationen zur Konstruktion des globalen Konstanten-Pools im Speicher. Ein Nachteil dieser Art der Speicherung ist jedoch die daraus resultierende Vergröÿerung der Ladeeinheit. Aus diesem Grund wurden zwei Optimierungsverfahren entwickelt, die einen guten Kompromiss zwischen der Speicherersparnis durch den globalen Konstanten-Pool und der Vergröÿerung der Ladeeinheiten durch die Analyse-Informationen nden. Bei dem zweiten Verfahren handelt es sich um die Ermittlung sicher geladener Klassen eines Programms. Diese Klassen können beim Start des Programms sofort geladen werden. Hierdurch werden Unterbrechungen durch das dynamische Nachladen der Klassen vermieden. Weiterhin werden die Klassen sofort veriziert, was zu einer frühen Erkennung von Manipulationen an der Ladeeinheit führt. Für die Ermittlung sicher geladener Klassen wurden zwei Datenuss- und zwei Aufrufgraph-Analysen entwickelt und implementiert. Zu diesen Analysen gehört die Ermittlung von Klassen und Methoden, die innerhalb einer Methode sicher geladen beziehungsweise aufgerufen werden. Ebenso werden Methoden ermittelt, die von einem Eintrittspunkt in das Programm sicher erreichbar sind. Die Ergebnisse dieser Analysen werden dann kombiniert und ergeben die Menge der sicher geladenen Klassen eines Programms. Ergebnisse Für die Faltung von Konstanten-Pools konnte eine Speicherersparnis von bis zu 73% gegenüber den Konstanten-Pools der OriginalLadeeinheit gemessen werden. Die aus der Annotation der Ladeeinheit mit 55 56 KAPITEL 6. ZUSAMMENFASSUNG Analyse-Informationen resultierende Vergröÿerung beträgt bis zu 51% der Gröÿe der Original-Ladeeinheit. Mit den Verfahren zur Optimierung des Transportformats konnte diese Vergröÿerung auf bis zu 25% eingedämmt werden. Allerdings leidet dann auch der Eekt der Speicherersparnis durch den globalen Konstanten-Pool. Die Ergebnisse des Verfahrens zur Ermittlung sicher geladener Klassen schwanken deutlich, da die Erkennungsleistung des Algorithmus stark vom Aufbau der untersuchten Programme abhängt. Für die ausgewählten Programme konnten zwischen 14% und 50% der Programm-Klassen als sicher geladen identiziert werden. Für einige Programme wurden sogar mehr Klassen als sicher geladen identiziert, als in der Ladeeinheit des Programms enthalten waren. Der Grund hierfür sind Bibliotheksklassen, die als sicher geladen identiziert wurden. Ausblick Für zukünftige Arbeiten ist es sinnvoll, eine modizierte virtuelle Maschine zu implementieren. Mit dieser virtuellen Maschine könnte dann die reale Speicherersparnis durch die Faltung des Konstanten-Pools gemessen werden. In dieser Arbeit wurde die Speicherersparnis lediglich indirekt über die Anzahl der Einträge in den Konstanten-Pools gemessen. Weiterhin könnte die Codierung der Mapping-Attribute, in denen die Analyse-Informationen aus der Faltung der Konstanten-Pools gespeichert sind, optimiert werden. Durch eine solche Optimierung könnte der Vergröÿerung der Ladeeinheiten durch die Faltung weiter entgegen gewirkt werden. Diesem Ziel können ebenfalls bessere Algorithmen zur Optimierung des Transportformates dienen. Hier wäre es möglich, die beiden vorgestellten Verfahren zu kombinieren und somit nur einen globalen KonstantenPool für sicher geladene Klassen zu konstruieren. Dadurch wären nur noch Einträge von Klassen im globalen Konstanten-Pool enthalten, die im Programmlauf sicher geladen würden. Einträge aus Klassen, die nicht geladen werden würden somit entfernt und der Speicherverbrauch durch den globalen Konstanten-Pool würde weiter eingeschränkt. Durch die Implementierung einer modizierten virtuellen Maschine könnte festgestellt werden, inwiefern das Laden von Klassen, die als sicher geladen identiziert wurden, sich auf die Ablaufgeschwindigkeit des Programms auswirkt. Weiterhin kann dieses Verfahren durch Einfügen verbesserter Algorithmen zur Konstruktion von Aufrufgraphen bessere Ergebnisse liefern. Solche Konstruktionsverfahren lassen sich leicht in die bestehende Umgebung integrieren. Eine sinnvolle Umgebung für solche Untersuchungen wären Plattformen mit eingeschränkten Ressourcen, wie zum Beispiel mobile Geräte oder SmartCards. Diese Plattformen besitzen in der Regel nur wenig Speicher und relativ langsame Prozessoren. Daher sollten die oben beschriebenen Eekte in einer solchen Umgebung deutlich sichtbar werden. Literaturverzeichnis [BRAD98] Bradley, Q., Horspool, R. N., and Vitek, J. 1998. JAZZ: an ecient compressed format for Java archive les. In Proceedings of the 1998 Conference of the Centre For Advanced Studies on Collaborative Research (Toronto, Ontario, Canada, November 30 - December 03, 1998). S. A. MacKay and J. H. Johnson, Eds. IBM Centre for Advanced Studies Conference. IBM Press, 7. [JVM98] T. Lindholm, F. Yellin, The Java Virtual Machine Specication, Second Edition, Addison-Wesley, 1998 [KAST90] Kastens U. 1990. Übersetzerbau. Oldenburg Verlag, München [MUCH97] Muchnick, S. 1997. Advanced Compiler Design and Implementation. Academic Press, San Diego [PUGH99] Pugh, W. 1999. Compressing Java class les. In Proceedings of the ACM SIGPLAN 1999 Conference on Programming Language Design and Implementation (Atlanta, Georgia, United States, May 01 - 04, 1999). PLDI '99. ACM Press, New York, NY, 247-258. [RIPP04] Rippert, C., Courbot, A., and Grimaud, G. 2004. A low-footprint class loading mechanism for embedded Java virtual machines. In Proceedings of the 3rd international Symposium on Principles and Practice of Programming in Java (Las Vegas, Nevada, June 16 - 18, 2004). ACM International Conference Proceeding Series, vol. 91. Trinity College Dublin, 75-82. [THIE02] Thies, M. 2002. Combining Static Analysis of Java Libraries with Dynamic Optimization. Shaker Verlag, Aachen. [TIP99] Tip, F., Lara, C., Sweeney, P. F., and Streeter, D. 1999. Practical experience with an application extractor for Java. In Proceedings of the 14th ACM SIGPLAN Conference on Object-Oriented Programming, Systems, Languages, and Applications (Denver, Colorado, United States, November 01 - 05, 1999). A. M. Berman, Ed. OOPSLA '99. ACM Press, New York, NY, 292-305. [ZIP06] ZIP File Format Specication, Version 6.2.2, PKWARE Inc., 2006 57 58 LITERATURVERZEICHNIS Abbildungsverzeichnis 2.1 Schematische Darstellung des Aufbaus der Laufzeitumgebung für Java-Programme . . . . . . . . . . . . 4 2.2 Stark vereinfachte Darstellung des Aufbaus einer Java-Klassendatei . . . . . . . . . . . . . . . . . . . . 5 2.3 Darstellung des Aufbaus eines Konstanten-Pools . . 6 2.4 Zugri einer Bytecode-Instruktion auf einen Eintrag des Konstanten-Pools . . . . . . . . . . . . . . . . . . 7 2.5 Aufbau einfacher und zusammengesetzter Einträge im Konstanten-Pool . . . . . . . . . . . . . . . . . . 7 2.6 Darstellung eines Ablaufgraphen mit Grundblöcken und Kontrolluss-Kanten . . . . . . . . . . . . . . . 12 2.7 Darstellung eines Aufrufgraphen für eine prozedurale Sprache . . . . . . . . . . . . . . . . . . . . . . . . . 13 2.8 Darstellung eines Aufrufgraphen für eine objektorientierte Sprache mit dynamischer Methodenbindung 14 3.1 Darstellung des Ladevorgangs einer modizierten virtuellen Maschine mit einem globalen Konstanten-Pool 22 3.2 Darstellung des Vergleichs von zusammengesetzten Einträgen . . . . . . . . . . . . . . . . . . . . . . . . 23 3.3 Schematische Darstellung des Aufbaus des MappingAttributes . . . . . . . . . . . . . . . . . . . . . . . . 25 3.4 Bester und schlechtester Fall beim Zugri auf Einträge des globalen Konstanten-Pools . . . . . . . . . . . 26 3.5 Darstellung der Auswahlstrategie über die Anzahl der Zugrie auf Elemente des globalen Konstanten-Pools 27 3.6 Beispielprogramm zur Erläuterung der Denition sicher geladener Klassen . . . . . . . . . . . . . . . . . 29 3.7 Darstellung eines Ablaufgraphen für die DatenussAnalyse zur Ermittlung sicher geladener Klassen einer Methode . . . . . . . . . . . . . . . . . . . . . . 30 59 60 ABBILDUNGSVERZEICHNIS 3.8 Beispiel für den Verband, der als Grundlage für die intraprozedurale Analyse zur Ermittlung sicher geladener Klassen dient . . . . . . . . . . . . . . . . . . . 31 3.9 Darstellung eines Ablaufgraphen für die DatenussAnalyse zur Ermittlung sicher aufgerufener Methoden 31 4.1 Aufbau der Umgebung zur Programm-Analyse PAULI 35 4.2 Klassendiagramm der Analyse zur Faltung von Konstanten-Pools . . . . . . . . . . . . . . . . . . . . 37 4.3 Klassendiagramm der Analyse zur Ermittlung sicher geladener Klassen . . . . . . . . . . . . . . . . . . . . 38 4.4 Darstellung des Ladeprozesses einer annotierten Klassendatei. Der Klassenlader verwendet das Mapping-Attribut, um die Einträge des KonstantenPools in den globalen Konstanten-Pool abzubilden. 39 4.5 Aufbau des Mapping-Attributs . . . . . . . . . . . . 40 4.6 Aufbau eines Eintrags im Mapping-Attribut für einfache Einträge im Konstanten-Pool . . . . . . . . . . 40 4.7 Aufbau des Eintrags im Mapping-Attribut für Einträge des Konstanten-Pools vom Typ CONSTANT_NameAndType . . . . . . . . . . . . . . . . 40 Anzahl der Einträge im globalen Konstanten-Pool relativ zur Summe der Einträge in den KonstantenPools der Klassen der jeweiligen Ladeeinheit . . . . . 45 5.2 Vergröÿerung der Ladeeinheiten nach der Faltung der Konstanten-Pools relativ zur Originalgröÿe . . . . . 45 5.3 Anzahl der Einträge in den optimierten Ladeeinheiten nach Optimierung über die Anzahl der Zugriffe (Relativ zur Anzahl der Einträge der OriginalLadeeinheiten) . . . . . . . . . . . . . . . . . . . . . 47 Gröÿe der optimierten Ladeeinheiten nach Optimierung über die Anzahl der Zugrie (Relativ zur Gröÿe der Original-Ladeeinheit) . . . . . . . . . . . . . . . 47 Anzahl der Einträge in den optimierten Ladeeinheiten nach Optimierung über die durchschnittliche Anzahl der Zugrie (Relativ zur Anzahl der Einträge in den Original-Ladeeinheiten) . . . . . . . . . . . . . . 48 Gröÿe der optimierten Ladeeinheiten über die durchschnittliche Anzahl der Zugrie (Relativ zur Gröÿe der Original-Ladeeinheit) . . . . . . . . . . . . . . . 49 5.7 Vergleich der Optimierungsverfahren über die Anzahl von Einträgen in den optimierten Ladeeinheiten . . . 50 5.8 Vergleich der Optimierungsverfahren über die Gröÿe der optimierten Ladeeinheiten . . . . . . . . . . . . . 50 5.1 5.4 5.5 5.6 Tabellenverzeichnis 2.1 Übersicht über die Typen von Einträgen in Konstanten-Pools von Java-Klassendateien . . . . . . . . . . . . . . . . . . . . . . . . . 5.1 Eigenschaften der Ladeeinheiten, die zur Evaluation der Faltung von Konstanten-Pools verwendet wurden. . . . . . . . . . . . . . 44 Vergleich der Messwerte der optimierten Ladeeinheiten am Beispiel der SWT-Bibliothek . . . . . . . . . . . . . . . . . . . . . . 51 Aufschlüsselung der Typen von Einträgen in den globalen KonstantenPools der optimierten Ladeeinheiten der SWT-Bibliothek . . . . 51 Programme und Messergebnisse für die Evaluation der Ermittlung sicher geladener Klassen . . . . . . . . . . . . . . . . . . . . 52 Gegenüberstellung der erreichbaren Methoden und der sicher erreichten Methoden für die Testprogramme . . . . . . . . . . . . . 53 5.2 5.3 5.4 5.5 61 6