Hawlitzek GmbH/ Veröffentlichungen/ HotSpot/
 

Hawlitzek IT-Consulting GmbH ©Foto Kirsten Literski-Hawlitzek

HotSpot

Dieser Artikel ist im JavaSpektrum 4/99 erschienen. Vielen Dank an SIGS für die Genehmigung zur Veröffentlichung auf dieser Webseite!

Langsam wird´s schneller

HotSpot, JIT & Co

Seit April gibt es nun die sehnlich erwartete HotSpot-Technologie von Sun. Kann sie die hoch gesteckten Erwartungen erfüllen? Wie sieht die Performance im Vergleich zu Just-In-Time-Compilern aus? Dieser Artikel stellt die Wirkungsweise der verschiedenen Technologien vor und untersucht deren Einfluss in typischen Anwendungsszenarien.

Der Ruf von Java leidet seit Anfang unter der sprichwörtlichen Langsamkeit. Dies aber häufig zu Unrecht, denn bei vielen Applets liegt der Hauptzeitaufwand bei der Datenübertragung und beim Herunterladen der Anwendung. So wird der Zeitaufwand, der mit dem Übergang von einer Client/Server-Anwendung im Firmen-LAN zur Internetanwendung über eine Modem-Verbindung zwangsläufig einhergeht, der Programmiersprache zugeschrieben.

Warum ist Java langsam?

Aber es gibt auch Gründe, warum Java tatsächlich langsamer ist als zum Beispiel C. Wie Sie sicher wissen, ist Java eine interpretierte Sprache. Das bedeutet, plattformunabhängige Java-Anwendungen werden durch eine plattformspezifische Laufzeitumgebung interpretiert. Hierin ist Java rein interpretierten Sprachen wie BASIC, Perl oder Tcl sehr ähnlich, jedoch wird bei Java eine Vorkompilierung in den so genannten Bytecode vorgenommen.

Dieser Bytecode ist Maschinencode für eine abstrakt definierte Maschine, die so genannte virtuelle Maschine. In dieser Form ist die Java-Anwendung für einen Computer im Gegensatz zum Quellcode bereits wesentlich leichter zu verstehen. Die Laufzeitumgebung muss nur noch die virtuelle auf der realen Maschine emulieren.

Diese zweite Umwandlung des Code ist aber immer noch zeitaufwendiger als die direkte Ausführbarkeit von Maschinencode auf der Zielhardware. Deshalb ist kompilierter Code in Programmiersprachen wie C oder C++ praktisch immer überlegen, er muss jedoch für jede Zielplattform getrennt entwickelt und getestet werden.

Verbesserungsmöglichkeiten

Um die Geschwindigkeit zu verbessern, verfolgt man verschiedene Ansätze:

Optimierung der Laufzeitumgebung:

Die Hersteller versuchen immer effizientere virtuelle Maschinen zu entwickeln, im Windows-Bereich wetteifern hier Sun, Microsoft und IBM um die ersten Plätze in den Benchmarks [Bench].

  1. Kompilierung zur Laufzeit:
    So genannte Just-In-Time-Compiler (JIT) wandeln den Bytecode nach dem Herunterladen des Applet oder dem Start einer Anwendung in Maschinencode um und führen diesen aus. JIT-Compiler sind heutzutage in fast allen Laufzeitumgebungen und Webbrowsern enthalten.
  2. Kompilierung zur Entwicklungszeit:
    Wie in anderen Programmiersprachen auch, gibt es für Java Native-Compiler, die direkt plattformspezifischen Maschinencode herstellen. Diese sind in Entwicklungsumgebungen (IBM VisualAge for Java Enterprise Edition, Symantec Visual Café) oder sogar kostenlos (Jikes auf IBM Alphaworks[Jikes]) erhältlich.
  3. HotSpot: Analyse und Kompilierung zur Laufzeit:
    HotSpot analysiert den Code zur Laufzeit und konzentriert die Kompilation und Optimierung auf besonders häufig benutzte Methoden. Zur Optimierung stehen zusätzliche Informationen aus dem realen Ablaufverhalten zur Verfügung. Auch HotSpot ist nach anfänglichen Wirren nun kostenlos herunterladbar [HotSpot].
  4. Code- und Deployment-Optimierung:
    Neben all diesen maschinellen Verbesserungen ist aber auch der Programmierer gefragt, den Code durch gute Algorithmen und Kenntnis der Eigenheiten von Java zu beschleunigen.


Abbildung 1: Performance-Optimierung mittels Compilern

HotSpot

Wie wir bei den Benchmarks noch sehen werden, bringt die HotSpot-Technologie in fast allen Fällen (bis auf AWT) eine deutliche Verbesserung der Performance. Wie zu erwarten war, wurden die schlechtesten Werte jeweils beim ersten Durchlauf erzielt, als der Optimierer noch nicht aktiv war.

HotSpot kompiliert nicht die gesamte Anwendung wie JIT-Compiler das tun, sondern nur besonders performancekritische Methoden, die so genannten hot spots.

Warum bedeutet dies einen Vorteil für den Anwender, wenn manche Teile nach wie vor interpretiert werden? Wie die Benchmark zeigen werden, ist die Testanwendung mit JIT-Compilierung in fast allen Belangen überlegen, insbesondere bei der Ausgabe, die einzigen Ausnahmen sind Multithreading und Objekterzeugung.

Laut Sun besteht ein Vorteil darin, dass der Code sofort im Interpreter gestartet wird und damit die Anwendung sofort benutzt werden kann, während beim JIT-Konzept zunächst die Kompilation abläuft. Allerdings muss die die große HotSpot-DLL (1,4 MB) auch erst geladen und initialisiert werden. Wir haben bei unseren Untersuchungen keinen merklich schnelleren Start feststellen können.

Ein weiterer Vorteil soll die bessere Optimierbarkeit des Codes sein, da zur Laufzeit mehr Informationen über den realen Ablauf der Anwendung vorliegen.

Um dies einschätzen zu können, muss man ein wenig über die Wirkungsweise von HotSpot wissen: HotSpot lässt die Anwendung zunächst im Interpreter-Modus laufen, beobachtet aber das Laufzeitverhalten (Profiling) und versucht daraus Schlüsse zu ziehen, wo Engpässe bestehen, das heißt in welchen Methoden die Anwendung am meisten Zeit verbringt. Diese kompiliert HotSpot, besitzt aber gegenüber dem JIT-Compiler Informationen darüber, welche Zweige der Anwendung wirklich benutzt werden und welche Anweisungen im aktuellen Kontext "toten Code" darstellen, also nie benutzt werden. Hierzu mehr im Kasten "Optimierungsprobleme bei Java-Compilern".

Die Anwendung wird also auf den aktuellen Anwendungsfall und Benutzer hin optimiert. Dieses Verhalten kann sich jedoch verändert und der Code muss neu optimiert werden. Auch dies leistet HotSpot.

Einsatz von HotSpot

Wenn sich die Anwendung viel in bereits nativ vorliegenden Bibliotheken wie zum Beispiel AWT oder IO aufhält oder Low-Level-Rechenoperationen durchführt, machen sich die beschriebene Optimierungen nicht bemerkbar, bei eigenem Code mit vielen Objekten und Threads sind die Vorteile aber deutlich spürbar.

Das bedeutet, dass der Einsatz von HotSpot bei den meisten Client-Anwendungen nicht sinnvoll ist. Wenn man aber Servlets betrachtet, stimmt deren Charakteristik sehr gut mit den Vorteilen von HotSpot überein. Servlets haben in der Regel viele Client-Anfragen zu bearbeiten, was eine gute Multithreading-Performance erfordert und erzeugen viele Objekte, die meisten mit kurzer, einige mit längerer Lebensdauer.

HotSpot und Garbage Collection

HotSpot hat noch ein weiteres "Schmankerl" zu bieten, eine optimierte Garbage Collection. HotSpot unterscheidet Objekte nach ihrer Lebensdauer. Junge Objekte kommen in die "Kinderstube", einen Speicherbereich, der nach einem Überlauf bereinigt wird. Bei diesem Vorgang können die meisten kurzlebigen Objekte bereits wieder entsorgt werden (oft 95%), da sie nicht mehr benötigt werden. Die anderen Objekte werden in einen anderen Speicherbereich für langlebigere Objekte kopiert. In diesem Bereich findet eine konventionelle Speicherbereinigung statt, die neben der Löschung nicht mehr referenzierter Objekte auch für eine Defragmentierung sorgt. Diese soll nach Herstellerangaben weitaus zuverlässiger sein und die gefürchteten Memory Leaks bei langlaufenden Anwendungen vermeiden.

Alternativ bietet HotSpot auch eine inkrementelle Garbage Collection an, die darauf hin optimiert ist, lange Pausen bei der Ausführung der Anwendung zu vermeiden, die durch die Garbage Collection entstehen (Parameter

–Xincgc, siehe Kasten "HotSpot: Installation und Optionen"). Die inkrementelle Garbage Collection läuft nebenher und dauert in der Regel nicht länger als 10 ms, so dass eine dauerhafte gute Antwortzeit sicher ist. Der Algorithmus garantiert jedoch keine harte Echtzeitfähigkeit, das heißt eine garantierte Höchstdauer für die Unterbrechung.

Auch dies ist für den Bereich der Java-Serveranwendungen wichtig, denn man möchte hier natürlich eine gleichbleibend gute Performance ohne Einbrüche.

Code-Optimierungen

Bisher haben wir uns auf die nachträgliche Verbesserung der Performance einer Anwendung konzentriert. Viel effektiver sind jedoch in der Regel Optimierungen am Code selbst. In der Literatur findet man viele Informationen über gute Algorithmen und parallele Programmierung, dies gilt nicht nur für Java, sondern für jede Programmiersprache. Ein paar Java-typische Dinge sollte man jedoch beachten:

  1. Richtiges Packaging und dynamisches Nachladen von Klassen:
    Die Zeit für das Herunterladen des Codes (und der benötigten Ressourcen) von einem Server lässt sich durch die Benutzung von Java-Archiven (JARs) reduzieren. Diese Archive sind komprimiert und reduzieren die Anzahl der aufzubauenden Netzwerkverbindungen. Wenn man allerdings viele nicht benötigte Klassen in ein solches Archiv packt, dreht sich der positive Effekt allerdings ins Gegenteil. Deshalb sollte man nur diejenigen Klassen in ein Archiv packen, die mit Sicherheit benutzt werden. Klassen, die nur in Spezialfällen benötigt werden hingegen, sollte man in der Anwendung dynamisch vom Server nachladen (mit Class.forName()). Dies kann die Startzeit eines Applets drastisch reduzieren.
  2. Synchronisation:
    Synchronisation ist notwendig, um bei mehreren, parallel ablaufenden Threads eine Konsistenz der Daten zu garantieren. Sie erfolgt in Java mittels des Modifiers synchronized. Eine synchronized definierte Methode hält aber sämtliche anderen ablaufenden Threads an, was ein erhebliches Performance-Problem verursachen kann. Deshalb sollte man nur die wirklich kritischen Methode synchronized definieren und insbesondere bei längeren Methoden einen spezifischeren Schutz mittels synschronized-Blöcken implementieren (Schutz auf Objektebene statt global).
  3. Serialisierung:
    Die Ein-/Ausgabe und die Netzwerkprogrammierung erfolgt in Java über Streams. Mitttels ObjectStreams kann man Objekte in einer Byte-Darstellung übertragen, die Umwandlung in diese Form nennt man Serialisierung. Das Performance-Problem ergibt sich bei der Behandlung von Referenzen. Wenn man ein Objekt serialisiert, das ein anderes Objekt referenziert, dann muss dieses andere Objekt auch serialisiert werden. Wenn man das rekursiv weiterverfolgt, kann mit einem einzigen Serialisierungsaufruf ein ganzes Geflecht von sehr vielen Objekten erfasst werden. Dies ist auch ein Grund, warum RMI häufig so langsam wird. RMI benutzt nämlich genau diesen Mechanismus, um Objekte zu übertragen, die als Parameter der entfernten Methode übergeben werden. Die bei einer Serialisierung nicht benötigten Referenzvariablen sollte man mit dem Modifier transient kennzeichnen.
  4. Der Modifier final:
    final dient bei Klassen dazu, die Vererbung auszuschalten. Man kann ihn auch bei Methoden einsetzen. Hier bedeutet er, dass diese Methode nicht durch eine Subklasse überschrieben werden darf. Dies gibt dem Compiler eine weitere Optimierungsmöglichkeit. Es müssen nun keine virtuellen Methoden eingesetzten werden (siehe Kasten "Optimierungsprobleme bei Java-Compilern"). Das bedeutet statt anhand einer virtuellen Methodentabellen zu entscheiden, welche Methode aus der Ableitungshierarchie benutzt werden muss, kann die richtige direkt (als feste Adresse) aufgerufen werden. Beim Einsatz von final muss man natürlich gut über eine geplante Wiederverwendung nachdenken, denn man verbietet damit ja ein Überschreiben!
  5. Die Klasse StringBuffer:
    Wenn Sie viel mit Strings manipulieren, werden Sie wissen dass die Verkettung von Strings mit "+" relativ langsam ist. Das liegt daran, dass Java jedes Mal ein neues String-Objekt für das Ergebnis (und für Zwischenergebnisse) anlegt. Besser geeignet für die String-Manipulation ist die Klasse StringBuffer, die die meisten Operation "am Platz" durchführen kann.
  6. Connection-Pooling bei Datenbankanwendungen:
    Datenbankzugriffe per JDBC müssen nicht zwangsweise langsam sein. Viel Zeit geht üblicherweise verloren, da Internet-Programme nicht wie konventionelle Datenbankanwendungen eine Verbindung aufbauen, damit arbeiten und sie danach wieder freigeben. Im Internet (z.B. bei einer Suchmaschine) hat man hingegen in der Regel viele, sehr kurze Transaktionen, die jeweils eine neue Verbindung aufbauen. Dies kostet viel Zeit. Besser ist der Einsatz eines Connection-Pools, in dem bereits ein Satz offener Verbindungen bereitsteht. Die Client-Anwendung bekommt dann nur noch ein Handle auf eine solche Verbindung und gibt diese danach wieder in den Pool zurück.

Benchmarks

Tabelle 1 gibt einen Überblick über die Auswirkungen der verschiedenen Mechanismen bei unterschiedlichen Aufgaben. Untersucht wurde die reine Rechenleistung, verschiedene numerische Operationen und die Erzeugung großer Mengen neuer Objekte. Beim Serialisierungstest werden Objekte in eine Bytedarstellung umgewandelt und in eine Datei geschrieben. Zur Überprüfung des Verhaltens mehrerer Threads wurde ein Test erstellt, der massiv synchronisierte Methoden und Blöcke benutzt. Den Abschluss bildet die Schnelligkeit der graphischen (AWT-)Oberfläche.

Dabei wurden die Tests unter Windows NT mit dem JDK 1.1.6 mit und ohne Benutzung des Symantec Just-In-Time-Compilers durchgeführt, mit der Laufzeitumgebung von Java 2, mit HotSpot 1.0 und schließlich nativ mit dem High-Performance Java Compiler (HPJC) von IBM kompiliert. Angegeben sind jeweils die schnellsten und langsamsten Testergebnisse. Es ging in dieser Untersuchung nicht darum, welche die schnellste virtuelle Maschine oder die beste Java-Plattform ist [Bench], sondern um die Auswirkungen der verschiedenen Optimierungen. Angegeben wurde auch der Hauptspeicher, den der Java-Prozess nach dem Start sowie nach 5maligen Durchlaufen aller Tests belegt.

 

JDK 1.1.6

JDK 1.1.6 -nojit

JRE 1.2

JRE 1.2 Hotspot

VAJ HPJC

Floating Point

0.50 - 0.56 s

23.92 – 24.66 s

0.45 – 0.52 s

2.17 – 14.41 s

3.70 – 4.01 s

Integer

0.62 - 0.68 s

19.19 – 20.50 s

0.60 – 0.66 s

0.74 – 19.42 s

0.92 – 0.99 s

Numerical

0.56 – 0.72 s

0.78 – 0.84 s

0.82 – 1.04 s

0.64 – 1.01 s

0.46 – 0.55 s

Object Creation

1.57 – 1.72 s

1.76 – 1.81 s

1.95 – 2.30 s

1.30 – 1.81 s

1.22 – 1.36 s

Serialization

4.59 – 5.17 s

10.96 – 12.47 s

2.12 – 2.84 s

2.30 – 5.56 s

3.29 – 3.74 s

Multithreading

0.89 – 3.25 s

0.99 – 3.84 s

1.27 – 2.16 s

0.41 – 1.04 s

0.52 – 0.59 s

AWT/GUI

0.67 – 0.79 s

0.72 – 0.81 s

0.75 – 1.00 s

1.20 – 1.81 s

0.58 – 0.67 s

RAM nach Start

4.4 MB

3.7 MB

8.9 MB

10.8 MB

6.1 MB

RAM nach 5x All

6.8 MB

5.9 MB

12.2 MB

13.9 MB

12.4 MB

Tabelle 1: Benchmark-Ergebnisse und Speicherverbrauch
Bewertung der Ergebnisse

Bei allen Optimierungsarten ist eine deutliche Beschleunigung gegenüber dem rein interpretierten Ablauf festzustellen. Dies wird vor allem beim Rechnen mit Integer- und Fließkommazahlen sichtbar. Insbesondere bei letzterem hinkt auch HotSpot selbst nach der Optimierung deutlich hinterher, aber auch nativer Code (HPJC) schneidet ziemlich schlecht ab.

Der numerische Test greift intensiv auf Bibliotheksfunktionen zurück, die in der Regel bereits in Maschinencode realisiert sind. Wie erwartet gibt es hier keine besonderen Abweichungen, die gute Leistung der mittels HPJC kompilierten Anwendung ist jedoch auffallend.

Bei der Objekterzeugung fällt auf, dass Java 2 gegenüber 1.1.6 deutlich langsamer geworden ist, HotSpot und HPJC machen dagegen eine sehr gute Figur.

Nach diesen Basisfunktionen, die in jedem Anwendungstyp auftreten, sind die nächsten Tests für bestimmte Arten von Programmen typisch. Serialisierung tritt bei verteilten Anwendungen häufig auf, zum Beispiel bei RMI, Netzwerkprogrammierung oder der Ein-/Ausgabe. Durch Verbesserungen der Algorithmen und Rücknahme einiger scharfer Synchronisationsvorschriften ist hier Java 2 gegenüber den Vorversionen mehr als doppelt so schnell geworden.

Multithreading ist besonders bei Servlets und anderen Serveranwendungen wichtig, die viele Clients unabhängig voneinander bedienen. Viel Zeit geht hier durch die Synchronisation verloren, die sicherstellt, dass Methoden, die gleichzeitig ablaufen, nicht miteinander in Konflikt geraten. Zum Beispiel muss sichergestellt sein, dass während des Auslesen eines Datensatzes diese Operation nicht durch eine Schreiboperation unterbrochen werden darf, die dieselben Daten manipuliert. In diesem Bereich trumpft HotSpot auf, dieser Test läuft meist dreimal so schnell ab wie mit dem klassischen JDK.

Der letzte Test betrifft ausschließlich den Client. Die Darstellung graphischer Oberflächen mit dem AWT ist mit dem JDK 1.1.6 mit JIT und dem HPJC sehr schnell. Java 2 ist hier durch Integration der 2D-API zwar wesentlich mächtiger, aber auch merklich langsamer geworden. HotSpot bringt sogar eine weitere Verschlechterung.

Abbildung 2 zeigt die relativen Ergebnisse. Die dunklen Säulen zeigen die schnellsten Ergebnisse an, die helleren den Worst-Case. Wenn der hellere Bereich sehr groß ist, wie zum Beispiel bei HotSpot oder Multithreading, deutet dies auf eine große Schwankungsbreite.


Abbildung 2: Performance-Vergleich nach Aufgaben und Technologien

Fazit

Seit den Kinderschuhen von Java hat sich viel getan. Momentan muss man den Just-In-Time-Compilern die beste Performance bei Applets und Stand-Alone-Anwendungen bescheinigen. Für den Endanwender und Internet-Surfer wird sich also nichts ändern.

Anders sieht es beim Server aus, hier kommt HotSpot voll zur Geltung: Das gute Multithreadingverhalten und der schnelle Objekt-Lebenszyklus fallen hier genauso ins Gewicht wie die zuverlässigere und konstantere Garbage Collection. Eine Alternative stellt hier aber auch die Native-Kompilierung dar, da auf einem bekannten Webserver (feste Zielplattform) die Plattformunabhängigkeit keine so große Rolle spielt.

Links

 

[Bench] Benchmarks: http://www.spec.org/osg/jvm98/

 

[FAQ] HotSpot Benchmarking FAQ: http: //www.javasoft.com/products/hotspot/launch/Q+A.html

 

HotSpot] HotSpot: http://www.javasoft.com/products/hotspot

 

[Jikes] Jikes: http://www.alphaworks.ibm.com

 

[Haw] Download des vom Autor geschriebenen Benchmark-Programms

 

 

 

 

© 2006 Hawlitzek IT-Consulting GmbH,
Marketing&Design Kirsten Literski-Hawlitzek

Seitenanfang

Hawlitzek GmbH
Die Gründer
Dienstleistungen
Java Downloads
Vorträge
Veröffentlichungen
Kontakt
International
Sitemap