Java-Anwendungen laufen auf JVM, aber kennen Sie sich mit der JVM-Technologie aus? In diesem Artikel (dem ersten Teil dieser Serie) erfahren Sie, wie die klassische Java Virtual Machine funktioniert, z. B. die Vor- und Nachteile von Java Write-Once, plattformübergreifende Engines, Garbage Collection-Grundlagen, klassische GC-Algorithmen und Kompilierungsoptimierung. In den folgenden Artikeln geht es um die Optimierung der JVM-Leistung, einschließlich des neuesten JVM-Designs, das die Leistung und Skalierbarkeit der heutigen hochgradig gleichzeitigen Java-Anwendungen unterstützt.
Wenn Sie Entwickler sind, müssen Sie dieses besondere Gefühl kennengelernt haben: Sie haben plötzlich einen Geistesblitz, alle Ihre Ideen sind miteinander verbunden und Sie können sich aus einer neuen Perspektive an Ihre vorherigen Ideen erinnern. Ich persönlich liebe das Gefühl, neues Wissen zu lernen. Ich habe diese Erfahrung oft gemacht, als ich mit der JVM-Technologie gearbeitet habe, insbesondere mit der Garbage Collection und der JVM-Leistungsoptimierung. Ich hoffe, diese Inspirationen in dieser neuen Welt von Java mit Ihnen teilen zu können. Ich hoffe, dass Sie genauso gespannt darauf sind, mehr über die Leistung der JVM zu erfahren, während ich diesen Artikel schreibe.
Diese Artikelserie richtet sich an alle Java-Entwickler, die mehr über das zugrunde liegende Wissen der JVM und die tatsächlichen Funktionen der JVM erfahren möchten. Auf hohem Niveau werde ich die Speicherbereinigung und das endlose Streben nach Sicherheit und Geschwindigkeit des freien Speichers diskutieren, ohne den Anwendungsbetrieb zu beeinträchtigen. Sie lernen die wichtigsten Teile der JVM kennen: Garbage Collection und GC-Algorithmen, Kompilierungsoptimierung und einige häufig verwendete Optimierungen. Ich werde auch erläutern, warum Java-Markup so schwierig ist, und Ratschläge dazu geben, wann Sie Leistungstests in Betracht ziehen sollten. Abschließend werde ich über einige neue Innovationen in JVM und GC sprechen, darunter Azuls Zing JVM, IBM JVM und den Garbage First (G1)-Garbage-Collection-Fokus von Oracle.
Ich hoffe, Sie beenden die Lektüre dieser Serie mit einem tieferen Verständnis der Natur der Skalierbarkeitseinschränkungen von Java und wie diese Einschränkungen uns dazu zwingen, eine Java-Bereitstellung auf optimale Weise zu erstellen. Hoffentlich haben Sie ein Gefühl der Erleuchtung und eine gute Java-Inspiration: Hören Sie auf, diese Einschränkungen zu akzeptieren, und ändern Sie sie! Wenn Sie noch kein Open-Source-Mitarbeiter sind, kann diese Serie Sie dazu ermutigen, sich in diesem Bereich weiterzuentwickeln.
JVM-Leistung und die Herausforderung „Einmal kompilieren, überall ausführen“.
Ich habe neue Neuigkeiten für diejenigen, die hartnäckig davon überzeugt sind, dass die Java-Plattform von Natur aus langsam ist. Als Java zum ersten Mal zu einer Anwendung auf Unternehmensebene wurde, gab es die Java-Leistungsprobleme, für die die JVM kritisiert wurde, bereits vor mehr als zehn Jahren, doch diese Schlussfolgerung ist mittlerweile überholt. Wenn Sie heute einfache statische und deterministische Aufgaben auf verschiedenen Entwicklungsplattformen ausführen, werden Sie höchstwahrscheinlich feststellen, dass die Verwendung von maschinenoptimiertem Code eine bessere Leistung erbringt als die Verwendung einer virtuellen Umgebung unter derselben JVM. Allerdings hat sich die Leistung von Java in den letzten 10 Jahren stark verbessert. Die Marktnachfrage und das Wachstum in der Java-Branche haben zu einer Handvoll Garbage-Collection-Algorithmen, neuen Kompilierungsinnovationen und einer Vielzahl von Heuristiken und Optimierungen geführt, die auf der Weiterentwicklung der JVM-Technologie basieren. Einige davon werde ich in zukünftigen Kapiteln behandeln.
Die technische Schönheit der JVM ist auch ihre größte Herausforderung: Nichts kann als „Einmal kompilieren, überall ausführen“-Anwendung betrachtet werden. Anstatt für einen Anwendungsfall, eine Anwendung oder eine bestimmte Benutzerlast zu optimieren, verfolgt die JVM kontinuierlich, was die Java-Anwendung gerade tut, und optimiert entsprechend. Dieser dynamische Vorgang führt zu einer Reihe dynamischer Probleme. Entwickler, die an der JVM arbeiten, verlassen sich beim Entwurf von Innovationen nicht auf statische Kompilierung und vorhersehbare Zuordnungsraten (zumindest nicht, wenn wir Leistung in Produktionsumgebungen fordern).
Die Ursache der JVM-Leistung
In meiner frühen Arbeit wurde mir klar, dass die Garbage Collection sehr schwer zu „lösen“ ist, und ich war schon immer von JVMs und Middleware-Technologie fasziniert. Meine Leidenschaft für JVMs begann, als ich im JRockit-Team war und eine neue Methode programmierte, um mir selbst beizubringen und Garbage-Collection-Algorithmen selbst zu debuggen (siehe Ressourcen). Dieses Projekt (das zu einer experimentellen Funktion von JRockit wurde und zur Grundlage für den Deterministic Garbage Collection-Algorithmus wurde) begann meine Reise in die JVM-Technologie. Ich habe bei BEA Systems, Intel, Sun und Oracle gearbeitet (da Oracle BEA Systems übernommen hat, habe ich kurz für Oracle gearbeitet). Dann bin ich dem Team von Azul Systems beigetreten, um die Zing-JVM zu verwalten, und jetzt arbeite ich für Cloudera.
Maschinenoptimierter Code kann möglicherweise eine bessere Leistung erzielen (allerdings auf Kosten der Flexibilität), aber das ist kein Grund, ihn für Unternehmensanwendungen mit dynamischem Laden und sich schnell ändernder Funktionalität zu gewichten. Für die Vorteile von Java sind die meisten Unternehmen eher bereit, die kaum perfekte Leistung maschinenoptimierten Codes zu opfern.
1. Einfache Code- und Funktionsentwicklung (d. h. kürzere Zeit, um auf den Markt zu reagieren)
2. Holen Sie sich sachkundige Programmierer
3. Verwenden Sie Java-APIs und Standardbibliotheken für eine schnellere Entwicklung
4. Portabilität – keine Notwendigkeit, Java-Anwendungen für neue Plattformen neu zu schreiben
Vom Java-Code zum Bytecode
Als Java-Programmierer sind Sie wahrscheinlich mit dem Codieren, Kompilieren und Ausführen von Java-Anwendungen vertraut. Beispiel: Nehmen wir an, Sie haben ein Programm (MyApp.java) und möchten, dass es nun ausgeführt wird. Um dieses Programm auszuführen, müssen Sie es zunächst mit javac (dem im JDK integrierten statischen Java-Sprach-zu-Bytecode-Compiler) kompilieren. Basierend auf dem Java-Code generiert javac den entsprechenden ausführbaren Bytecode und speichert ihn in der gleichnamigen Klassendatei: MyApp.class. Nachdem Sie den Java-Code in Bytecode kompiliert haben, können Sie die ausführbare Klassendatei über den Java-Befehl starten (über die Befehlszeile oder das Startskript, ohne die Startoption zu verwenden), um Ihre Anwendung auszuführen. Auf diese Weise wird Ihre Klasse in die Laufzeit geladen (d. h. die Java Virtual Machine wird ausgeführt) und das Programm beginnt mit der Ausführung.
Das ist es, was jede Anwendung an der Oberfläche ausführt, aber jetzt wollen wir untersuchen, was genau passiert, wenn Sie einen Java-Befehl ausführen. Was ist eine Java Virtual Machine? Die meisten Entwickler interagieren mit der JVM durch kontinuierliches Debuggen – also durch Auswahl und Wertzuweisung von Startoptionen, um die Ausführung Ihrer Java-Programme zu beschleunigen und gleichzeitig die berüchtigten Fehler „Nicht genügend Speicher“ zu vermeiden. Aber haben Sie sich jemals gefragt, warum wir überhaupt eine JVM benötigen, um Java-Anwendungen auszuführen?
Was ist eine Java Virtual Machine?
Einfach ausgedrückt ist eine JVM ein Softwaremodul, das Java-Anwendungsbytecode ausführt und den Bytecode in hardware- und betriebssystemspezifische Anweisungen umwandelt. Auf diese Weise ermöglicht die JVM die Ausführung eines Java-Programms in einer anderen Umgebung, nachdem es zum ersten Mal geschrieben wurde, ohne dass Änderungen am ursprünglichen Code erforderlich sind. Die Portabilität von Java ist der Schlüssel zu einer Unternehmensanwendungssprache: Entwickler müssen keinen Anwendungscode für verschiedene Plattformen neu schreiben, da die JVM die Übersetzung und Plattformoptimierung übernimmt.
Eine JVM ist im Grunde eine virtuelle Ausführungsumgebung, die als Bytecode-Anweisungsmaschine fungiert und dazu dient, Ausführungsaufgaben zuzuweisen und Speicheroperationen durch Interaktion mit der zugrunde liegenden Schicht auszuführen.
Eine JVM kümmert sich auch um die dynamische Ressourcenverwaltung für die Ausführung von Java-Anwendungen. Dies bedeutet, dass es das Zuweisen und Freigeben von Speicher beherrscht, ein konsistentes Threading-Modell auf jeder Plattform aufrechterhält und ausführbare Anweisungen organisiert, bei denen die Anwendung auf eine Weise ausgeführt wird, die für die CPU-Architektur geeignet ist. Die JVM befreit Entwickler davon, den Überblick über Verweise auf Objekte und deren Existenzdauer im System zu behalten. Ebenso ist es nicht erforderlich, dass wir verwalten, wann Speicher freigegeben wird – ein Problem in nicht dynamischen Sprachen wie C.
Sie können sich die JVM als ein Betriebssystem vorstellen, das speziell für die Ausführung von Java entwickelt wurde. Seine Aufgabe besteht darin, die Ausführungsumgebung für Java-Anwendungen zu verwalten. Eine JVM ist im Grunde eine virtuelle Ausführungsumgebung, die mit der zugrunde liegenden Umgebung als Bytecode-Anweisungsmaschine interagiert, um Ausführungsaufgaben zuzuweisen und Speicheroperationen durchzuführen.
Übersicht über die JVM-Komponenten
Es gibt viele Artikel über JVM-Interna und Leistungsoptimierung. Als Grundlage dieser Serie werde ich die JVM-Komponenten zusammenfassen und einen Überblick geben. Dieser kurze Überblick ist besonders nützlich für Entwickler, die neu in der JVM sind, und macht Lust auf mehr über die folgenden ausführlicheren Diskussionen.
Von einer Sprache zur anderen – Über Java-Compiler
Ein Compiler verwendet eine Sprache als Eingabe und gibt dann eine andere ausführbare Anweisung aus. Der Java-Compiler hat zwei Hauptaufgaben:
1. Machen Sie die Java-Sprache portabler und müssen Sie beim ersten Schreiben nicht mehr auf eine bestimmte Plattform festlegen.
2. Stellen Sie sicher, dass gültiger ausführbarer Code für eine bestimmte Plattform erstellt wird.
Compiler können statisch oder dynamisch sein. Ein Beispiel für eine statische Kompilierung ist Javac. Es nimmt Java-Code als Eingabe und wandelt ihn in Bytecode (eine Sprache, die in der Java Virtual Machine ausgeführt wird) um. Der statische Compiler interpretiert den Eingabecode einmal und gibt eine ausführbare Form aus, die bei der Ausführung des Programms verwendet wird. Da die Eingabe statisch ist, sehen Sie immer das gleiche Ergebnis. Nur wenn Sie den Originalcode ändern und neu kompilieren, werden Sie eine andere Ausgabe sehen.
Dynamische Compiler wie Just-In-Time-Compiler (JIT) konvertieren eine Sprache dynamisch in eine andere, das heißt, sie tun dies, während der Code ausgeführt wird. Mit dem JIT-Compiler können Sie Laufzeitanalysen sammeln oder erstellen (durch Einfügen von Leistungszählungen), indem Sie die Entscheidungen des Compilers und die vorhandenen Umgebungsdaten nutzen. Ein dynamischer Compiler kann beim Kompilieren in eine Sprache bessere Befehlssequenzen implementieren, eine Reihe von Befehlen durch effizientere ersetzen und sogar redundante Operationen eliminieren. Mit der Zeit werden Sie mehr Codekonfigurationsdaten sammeln und mehr und bessere Kompilierungsentscheidungen treffen; der gesamte Prozess wird normalerweise als Codeoptimierung und Neukompilierung bezeichnet.
Durch die dynamische Kompilierung haben Sie den Vorteil, dass Sie sich an dynamische Änderungen basierend auf dem Verhalten oder neue Optimierungen anpassen können, wenn die Anzahl der Anwendungslasten zunimmt. Aus diesem Grund eignen sich dynamische Compiler perfekt für Java-Operationen. Es ist erwähnenswert, dass der dynamische Compiler externe Datenstrukturen, Thread-Ressourcen, CPU-Zyklusanalyse und -optimierung anfordert. Je tiefer die Optimierung, desto mehr Ressourcen werden Sie benötigen. In den meisten Umgebungen trägt die oberste Ebene jedoch kaum zur Leistung bei – sie ist fünf- bis zehnmal schneller als Ihre reine Interpretation.
Die Zuordnung führt zur Speicherbereinigung
Wird in jedem Thread auf der Grundlage jedes „Java-Prozesses zugewiesenen Speicheradressraums“ zugewiesen oder als Java-Heap oder direkt als Heap bezeichnet. In der Java-Welt ist die Single-Thread-Zuweisung in Client-Anwendungen üblich. Allerdings ist die Single-Threaded-Zuweisung in Unternehmensanwendungen und Workload-Servern nicht vorteilhaft, da sie die Parallelität heutiger Multi-Core-Umgebungen nicht nutzt.
Das parallele Anwendungsdesign zwingt die JVM außerdem dazu, sicherzustellen, dass nicht mehrere Threads gleichzeitig denselben Adressraum zuweisen. Sie können dies steuern, indem Sie den gesamten zugewiesenen Speicherplatz sperren. Diese Technik (oft als Heap-Sperrung bezeichnet) ist jedoch sehr leistungsintensiv, und das Halten oder Warten von Threads kann sich auf die Ressourcennutzung und die Leistung der Anwendungsoptimierung auswirken. Das Gute an Multi-Core-Systemen ist, dass sie eine Vielzahl neuer Methoden erfordern, um Single-Thread-Engpässe bei der Ressourcenzuweisung und Serialisierung zu verhindern.
Ein gängiger Ansatz besteht darin, den Heap in Teile zu unterteilen, wobei jede Partition eine angemessene Größe für die Anwendung hat – offensichtlich müssen sie angepasst werden, Zuweisungsraten und Objektgrößen variieren erheblich zwischen Anwendungen und die Anzahl der Threads für dieselben Auch unterschiedlich. Der Thread Local Allocation Buffer (TLAB) oder manchmal auch Thread Local Area (TLA) ist eine spezielle Partition, in der Threads frei zugewiesen werden können, ohne eine vollständige Heap-Sperre zu deklarieren. Wenn der Bereich voll ist, ist auch der Heap voll, was bedeutet, dass auf dem Heap nicht genügend freier Speicherplatz zum Platzieren von Objekten vorhanden ist und Speicherplatz zugewiesen werden muss. Wenn der Heap voll ist, beginnt die Speicherbereinigung.
Fragmente
Durch die Verwendung von TLABs zum Abfangen von Ausnahmen wird der Heap fragmentiert, um die Speichereffizienz zu verringern. Wenn eine Anwendung beim Zuweisen von Objekten nicht in der Lage ist, einen TLAB-Speicherplatz zu vergrößern oder vollständig zuzuweisen, besteht die Gefahr, dass der Speicherplatz zu klein ist, um neue Objekte zu generieren. Ein solcher freier Speicherplatz wird als „Fragmentierung“ bezeichnet. Wenn die Anwendung einen Verweis auf das Objekt beibehält und dann den verbleibenden Speicherplatz zuweist, bleibt der Speicherplatz möglicherweise für längere Zeit frei.
Bei der Fragmentierung werden Fragmente über den Heap verstreut, wodurch Heap-Speicherplatz durch kleine Abschnitte ungenutzten Speicherplatzes verschwendet wird. Die Zuweisung von „falschem“ TLAB-Speicherplatz für Ihre Anwendung (in Bezug auf Objektgröße, gemischte Objektgröße und Referenzhalteverhältnis) ist die Ursache für eine erhöhte Heap-Fragmentierung. Während die Anwendung ausgeführt wird, nimmt die Anzahl der Fragmente zu und belegt Platz im Heap. Fragmentierung führt zu Leistungseinbußen und das System kann neuen Anwendungen nicht genügend Threads und Objekte zuweisen. Der Garbage Collector wird dann Schwierigkeiten haben, Ausnahmen wegen unzureichendem Arbeitsspeicher zu verhindern.
Bei der Arbeit entsteht TLAB-Abfall. Eine Möglichkeit, eine Fragmentierung vollständig oder vorübergehend zu vermeiden, besteht darin, den TLAB-Speicherplatz für jeden zugrunde liegenden Vorgang zu optimieren. Ein typischer Ansatz für diesen Ansatz besteht darin, dass die Anwendung neu abgestimmt werden muss, solange sie über ein Zuordnungsverhalten verfügt. Dies kann durch komplexe JVM-Algorithmen erreicht werden. Eine andere Methode besteht darin, Heap-Partitionen zu organisieren, um eine effizientere Speicherzuweisung zu erreichen. Beispielsweise kann die JVM Freilisten implementieren, die als Liste freier Speicherblöcke einer bestimmten Größe miteinander verknüpft sind. Ein zusammenhängender freier Speicherblock wird mit einem anderen zusammenhängenden Speicherblock derselben Größe verbunden, wodurch eine kleine Anzahl verknüpfter Listen mit jeweils eigenen Grenzen entsteht. In manchen Fällen führen Freilisten zu einer besseren Speicherzuteilung. Threads können Objekte in Blöcken ähnlicher Größe zuordnen, wodurch möglicherweise weniger Fragmentierung entsteht, als wenn Sie sich nur auf TLABs fester Größe verlassen würden.
GC-Trivia
Einige frühe Garbage Collectors hatten mehrere alte Generationen, aber mehr als zwei alte Generationen würden dazu führen, dass der Mehraufwand den Wert übersteigt. Eine weitere Möglichkeit, Zuweisungen zu optimieren und die Fragmentierung zu reduzieren, besteht darin, die sogenannte junge Generation zu schaffen, bei der es sich um einen dedizierten Heap-Speicherplatz für die Zuweisung neuer Objekte handelt. Der verbleibende Heap wird zur sogenannten alten Generation. Die alte Generation wird verwendet, um langlebige Objekte zuzuordnen. Zu den Objekten, von denen angenommen wird, dass sie lange existieren, gehören Objekte, die nicht durch Müll gesammelt werden, oder große Objekte. Um diese Zuordnungsmethode besser zu verstehen, müssen wir über einige Kenntnisse der Speicherbereinigung sprechen.
Garbage Collection und Anwendungsleistung
Garbage Collection ist der Garbage Collector der JVM, um belegten Heap-Speicher freizugeben, auf den nicht verwiesen wird. Wenn die Garbage Collection zum ersten Mal ausgelöst wird, bleiben alle Objektreferenzen weiterhin erhalten und der von vorherigen Referenzen belegte Speicherplatz wird freigegeben oder neu zugewiesen. Nachdem der gesamte zurückgewinnbare Speicher gesammelt wurde, wartet der Speicherplatz darauf, abgerufen und neuen Objekten erneut zugewiesen zu werden.
Der Garbage Collector kann ein Referenzobjekt niemals erneut deklarieren, da dies die JVM-Standardspezifikation verletzen würde. Die Ausnahme von dieser Regel ist ein weicher oder schwacher Verweis, der abgefangen werden kann, wenn der Garbage Collector bald nicht mehr über genügend Speicher verfügt. Ich empfehle Ihnen jedoch dringend, schwache Referenzen zu vermeiden, da die Mehrdeutigkeit der Java-Spezifikation zu Fehlinterpretationen und Anwendungsfehlern führt. Darüber hinaus ist Java für die dynamische Speicherverwaltung konzipiert, da Sie nicht darüber nachdenken müssen, wann und wo Speicher freigegeben werden soll.
Eine der Herausforderungen des Garbage Collectors besteht darin, Speicher so zuzuweisen, dass laufende Anwendungen nicht beeinträchtigt werden. Wenn Sie nicht so viel Speicher wie möglich sammeln, verbraucht Ihre Anwendung Speicher; wenn Sie zu oft sammeln, verlieren Sie Durchsatz und Antwortzeit, was sich negativ auf die laufende Anwendung auswirkt.
GC-Algorithmus
Es gibt viele verschiedene Garbage-Collection-Algorithmen. Mehrere Punkte werden später in dieser Serie ausführlich besprochen. Auf der höchsten Ebene sind die beiden Hauptmethoden der Garbage Collection Referenzzählung und Tracking-Collectors.
Der Referenzzählkollektor verfolgt, auf wie viele Referenzen ein Objekt verweist. Wenn die Referenz eines Objekts 0 erreicht, wird der Speicher sofort zurückgefordert, was einer der Vorteile dieses Ansatzes ist. Die Schwierigkeit beim Referenzzählansatz liegt in der zirkulären Datenstruktur und der Aktualisierung aller Referenzen in Echtzeit.
Der Tracking-Kollektor markiert Objekte, auf die noch verwiesen wird, und verwendet die markierten Objekte, um alle referenzierten Objekte wiederholt zu verfolgen und zu markieren. Wenn alle Objekte, auf die noch verwiesen wird, als „live“ markiert sind, wird der gesamte nicht markierte Speicherplatz zurückgewonnen. Dieser Ansatz verwaltet Ringdatenstrukturen, aber in vielen Fällen sollte der Collector warten, bis die gesamte Markierung abgeschlossen ist, bevor er nicht referenzierten Speicher zurückfordert.
Es gibt verschiedene Möglichkeiten, die oben beschriebene Methode durchzuführen. Die bekanntesten Algorithmen sind Markierungs- oder Kopieralgorithmen, parallele oder gleichzeitige Algorithmen. Ich werde diese in einem späteren Artikel besprechen.
Im Allgemeinen besteht die Bedeutung der Garbage Collection darin, neuen und alten Objekten im Heap Adressraum zuzuweisen. „Alte Objekte“ sind Objekte, die viele Garbage Collections überstanden haben. Verwenden Sie die neue Generation, um neue Objekte und die alte Generation alten Objekten zuzuordnen. Dadurch kann die Fragmentierung verringert werden, indem kurzlebige Objekte, die den Speicher belegen, schnell wiederverwendet werden. Außerdem werden langlebige Objekte zusammengeführt und an Adressen der alten Generation platziert. All dies reduziert die Fragmentierung zwischen langlebigen Objekten und schützt den Heap-Speicher vor Fragmentierung. Ein positiver Effekt der neuen Generation besteht darin, dass die teurere Sammlung von Objekten der alten Generation verzögert wird und Sie den gleichen Platz für kurzlebige Objekte wiederverwenden können. (Das Sammeln von altem Speicherplatz kostet mehr, da langlebige Objekte mehr Referenzen enthalten und mehr Durchläufe erfordern.)
Der letzte erwähnenswerte Algorithmus ist die Komprimierung, eine Methode zur Verwaltung der Speicherfragmentierung. Bei der Komprimierung werden Objekte grundsätzlich zusammen verschoben, um größeren zusammenhängenden Speicherplatz freizugeben. Wenn Sie mit der Festplattenfragmentierung und den damit umgehenden Tools vertraut sind, werden Sie feststellen, dass die Komprimierung ihr sehr ähnlich ist, mit der Ausnahme, dass diese im Java-Heapspeicher ausgeführt wird. Ich werde später in der Serie ausführlich auf die Verdichtung eingehen.
Zusammenfassung: Rückblick und Highlights
Die JVM ermöglicht Portabilität (einmal programmieren, überall ausführen) und dynamische Speicherverwaltung, alles wichtige Funktionen der Java-Plattform, die zu ihrer Popularität und gesteigerten Produktivität beitragen.
Im ersten Artikel über JVM-Leistungsoptimierungssysteme habe ich erklärt, wie ein Compiler Bytecode in die Befehlssprache der Zielplattform umwandelt und dabei hilft, die Ausführung von Java-Programmen dynamisch zu optimieren. Unterschiedliche Anwendungen erfordern unterschiedliche Compiler.
Ich habe auch kurz auf die Speicherzuweisung und Garbage Collection eingegangen und wie diese sich auf die Leistung von Java-Anwendungen auswirken. Grundsätzlich gilt: Je schneller Sie den Heap füllen und häufiger die Garbage Collection auslösen, desto höher ist die Auslastung Ihrer Java-Anwendung. Eine Herausforderung für den Garbage Collector besteht darin, Speicher so zuzuweisen, dass die laufende Anwendung nicht beeinträchtigt wird, aber bevor der Anwendung der Speicher ausgeht. In zukünftigen Artikeln werden wir die traditionelle und neue Speicherbereinigung und JVM-Leistungsoptimierung detaillierter besprechen.