Anwendung von Programm-Analyse zur

Werbung
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
Zugehörige Unterlagen
Herunterladen