Detaillierte JVM-Erklärung – Ausführungs-Engine

Wenn Sie mehr darüber erfahren möchten, besuchen Sie bitte meine persönliche Website: Yetong Space

1: Einführung in die Execution Engine

„Virtuelle Maschine“ ist ein mit „physischer Maschine“ verwandtes Konzept. Beide Maschinen verfügen über Codeausführungsfunktionen. Der Unterschied besteht darin, dass die Ausführungs-Engine einer physischen Maschine direkt auf der Prozessor-, Cache-, Befehlssatz- und Betriebssystemebene aufgebaut ist. , Während die Ausführungsmaschine der virtuellen Maschine durch die Software selbst implementiert wird, kann die Struktur des Befehlssatzes und der Ausführungsmaschine ohne Einschränkung durch physische Bedingungen angepasst werden und es können Befehlssatzformate ausgeführt werden, die nicht direkt von der Hardware unterstützt werden .

Die Hauptaufgabe der JVM besteht darin, Bytecodes in sie zu laden. Bytecodes können jedoch nicht direkt auf dem Betriebssystem ausgeführt werden, da Bytecode-Anweisungen nicht den Anweisungen lokaler Maschinen entsprechen und nur einige enthalten, die Bytecode-Anweisungen, Symboltabellen usw. können andere von der JVM erkannte Zusatzinformationen. Wenn Sie dann ein Java-Programm ausführen möchten, besteht die Aufgabe der Ausführungs-Engine darin, Bytecode-Anweisungen in lokale Maschinenanweisungen auf der entsprechenden Plattform zu interpretieren/kompilieren. Vereinfacht ausgedrückt fungiert die Ausführungs-Engine in der JVM als Übersetzer von der Hochsprache in die Maschinensprache.

Unter dem Gesichtspunkt des Erscheinungsbilds sind die Eingaben und Ausgaben der Ausführungs-Engine aller virtuellen Java-Maschinen konsistent: Die Eingabe ist der Bytecode-Binärstrom, der Verarbeitungsprozess ist der äquivalente Prozess der Bytecode-Analyse und -Ausführung und die Ausgabe ist die Ausführung Ergebnis.

Fügen Sie hier eine Bildbeschreibung ein

Zweitens: Verständnis der Zusammenstellung und Interpretation

Bevor Sie über die Kompilierung und Interpretation der JVM sprechen, können Sie sich zunächst die Kompilierung und Interpretation auf Sprachebene ansehen.

Typ Prinzip Vorteil Mangel
kompilierte Sprache Durch einen speziellen Compiler werden alle Quellcodes gleichzeitig in plattformspezifische Maschinencodes umgewandelt und liegen in Form ausführbarer Dateien vor Nach einmaligem Kompilieren kann es ohne Compiler ausgeführt werden, und die Laufeffizienz ist hoch Schlechte Portabilität, nicht flexibel genug
interpretierte Sprache Über einen dedizierten Interpreter kann je nach Bedarf ein Teil oder der gesamte Quellcode in Maschinencode für eine bestimmte Plattform umgewandelt werden Plattformübergreifend ist gut. Durch verschiedene Interpreter wird derselbe Quellcode auf verschiedenen Plattformen in Maschinencode interpretiert. Beim Konvertieren während der Ausführung ist die Effizienz gering

Es wird oft missverstanden, dass kompilierte Sprachen nicht plattformübergreifend sind. Die C-Sprache ist eine kompilierte Sprache, aber C-Programme können auch unter Windows ausgeführt werden, und C-Programme können auch unter Linux ausgeführt werden, da sie zur Ausführung unter Windows in EXE-Dateien kompiliert werden und dann auch in entsprechende ausführbare Dateien kompiliert werden können unter Linux ausgeführt. Warum heißt es also nicht plattformübergreifend?

Tatsächlich können kompilierte Sprachen auf zwei Arten nicht plattformübergreifend sein:

  • Ausführbare Programme können nicht plattformübergreifend sein, da unterschiedliche Betriebssysteme völlig unterschiedliche Anforderungen an die interne Struktur ausführbarer Dateien stellen und untereinander nicht kompatibel sind.
  • Der Quellcode kann nicht plattformübergreifend sein: Die von verschiedenen Plattformen unterstützten Funktionen, Typen, Variablen usw. können unterschiedlich sein, und der auf einer Plattform geschriebene Quellcode kann im Allgemeinen nicht direkt auf einer anderen Plattform ausgeführt werden. Nehmen Sie als Beispiel die C-Sprache:
    • In der C-Sprache können wir zum Anhalten des Programms die Funktion „sleep“ verwenden. Unter der Windows-Plattform lautet die Funktion „Sleep()“ und die Zeiteinheit ist Millisekunden, während sie unter der Linux-Plattform „sleep()“ ist und die Einheit Sekunden ist. Es ist ersichtlich, dass der erste Buchstabe der beiden Funktionen unterschiedlich ist und der Parameter von Sleep () Millisekunden ist, während der Parameter von Sleep () Sekunden ist und die Einheit unterschiedlich ist.
    • Obwohl C-Sprachen auf verschiedenen Plattformen den Long-Typ unterstützen, ist die Länge der vom Long-Typ belegten Bytes auf verschiedenen Plattformen unterschiedlich. Beispielsweise belegt die Long-Datei unter der 64-Bit-Windows-Plattform 4 Bytes, die Long-Datei unter der 64-Bit-Linux-Plattform jedoch 8 Bytes. Wenn Sie beim Schreiben von Code auf der 64-Bit-Linux-Plattform einer Variablen vom Typ long einen 8-Byte-Wert zuweisen, stellt dies überhaupt kein Problem dar. Wenn er sich jedoch auf der Windows-Plattform befindet, führt dies zu einem numerischen Überlauf und zum Absturz des Programms falsch ausgeführt werden.

Es ist ersichtlich, dass zum Erreichen einer plattformübergreifenden Interpretation einer Sprache die Plattformkompatibilität auf Codeebene berücksichtigt werden muss, dies ist jedoch sehr mühsam.

Zurück zur JVM: Die folgende Abbildung zeigt den Ausführungsablauf des Java-Programms. Wie Sie der Abbildung entnehmen können, gibt es im Prozess zwei Kompilierungen: Das erste Mal besteht darin, die Klassendatei aus der Java-Datei zu kompilieren, und das zweite Mal Zeit ist es, die Klassendatei durch den JIT-Compiler zu kompilieren. Diese beiden Kompilierungen werden auch als Front-End-Kompilierung bzw. Back-End-Kompilierung bezeichnet.

  • Front-End-Kompilierung: Bezogen auf die Quellsprache, nicht auf den Zielcomputer (.java -> .class).
  • Backend-Kompilierung: Es hat nichts mit der Quellsprache zu tun, es hat nichts mit der Zielmaschine zu tun (.class -> Maschinenanweisungen).
    Fügen Sie hier eine Bildbeschreibung ein
    Ob nach der Übergabe des Bytecode-Verifizierers der Java-Interpreter oder der JIT-Compiler verwendet werden soll, werden wir im Folgenden vorstellen.

Drei: JIT-Compiler

JIT (Just In Time), also die Just-in-Time-Kompilierung, kann mithilfe der JIT-Technologie die Ausführungsgeschwindigkeit von Java-Programmen beschleunigen. Also, wie? Wir alle wissen, dass Java eine interpretierte Sprache (oder eine halbkompilierte, halbinterpretierte Sprache) ist. Java kompiliert zunächst das Quellprogramm über den Compiler javac in eine plattformunabhängige Java-Bytecode-Datei (.class) und interpretiert und führt dann die Bytecode-Datei durch die JVM aus, wodurch Plattformunabhängigkeit erreicht wird. Es gibt jedoch Vor- und Nachteile. Der Kern des Interpretations- und Ausführungsprozesses des Bytecodes besteht darin, dass die JVM zunächst den Bytecode in entsprechende Maschinenanweisungen übersetzt und dann die Maschinenanweisungen ausführt. Offensichtlich darf die Ausführungsgeschwindigkeit nach der Interpretation und Ausführung auf diese Weise nicht so gut sein wie bei der direkten Ausführung binärer Bytecodedateien.

Um die Ausführungsgeschwindigkeit zu verbessern, wurde die JIT-Technologie eingeführt. Wenn die JVM feststellt, dass eine Methode oder ein Codeblock besonders häufig ausgeführt wird, betrachtet sie dies als „Hot-Spot-Code“. Dann kompiliert JIT einige „Hot Codes“ in lokale maschinenbezogene Maschinencodes, optimiert sie und speichert dann die kompilierten Maschinencodes für die nächste Verwendung zwischen.

Einige Entwickler werden überrascht sein, warum Sie einen Interpreter verwenden müssen, um die Ausführungsleistung des Programms zu „verlangsamen“, da die HotSpot-VM über einen integrierten JIT-Compiler verfügt. JRockit VM enthält beispielsweise keinen Interpreter und alle Bytecodes werden von einem Just-in-Time-Compiler kompiliert und ausgeführt.

  • Wenn das Programm gestartet wird, kann der Interpreter sofort wirksam werden, was die Kompilierungszeit spart und es sofort ausführt. Damit der Compiler funktioniert, benötigt er eine gewisse Ausführungszeit, um den Code in nativen Code zu kompilieren. Nach dem Kompilieren in nativen Code ist die Ausführungseffizienz jedoch hoch. Obwohl die Ausführungsleistung des Programms in der JRockit-VM also sehr effizient ist, dauert die Kompilierung des Programms beim Start zwangsläufig länger. Bei serverseitigen Anwendungen steht die Startzeit nicht im Mittelpunkt, aber für Anwendungsszenarien, bei denen die Startzeit im Vordergrund steht, kann es erforderlich sein, eine Architektur einzuführen, in der ein Interpreter und ein Just-in-Time-Compiler im Austausch für einen Ausgleich koexistieren Punkt. In diesem Modus kann beim Start der Java Virtual Machine der Interpreter zuerst wirksam werden, ohne dass vor der Ausführung darauf gewartet werden muss, dass der JIT-Compiler alle Kompilierungen abgeschlossen hat, was viel unnötige Kompilierungszeit sparen kann. Mit der Zeit kommt der Compiler ins Spiel, der immer mehr Code in nativen Code kompiliert, um die Ausführungseffizienz zu steigern.
  • Wenn die Speicherressourcen in der Programmlaufumgebung begrenzt sind (z. B. in einigen eingebetteten Systemen), kann der Interpreter zum Speichern von Speicher verwendet werden, andernfalls kann der Compiler zur Verbesserung der Effizienz verwendet werden. Wenn nach der Kompilierung eine „seltene Falle“ auftritt, können Sie außerdem durch Deoptimierung auf die interpretierte Ausführung zurückgreifen.
  • Es wird gesagt, dass JIT schneller ist als die Interpretation. Tatsächlich heißt es, dass „das Ausführen von kompiliertem Code“ schneller ist als „Interpretieren und Ausführen durch den Dolmetscher“. Dies bedeutet nicht, dass die Aktion „Kompilieren“ schneller ist als die Aktion „ Dolmetschen“. Egal wie schnell die JIT-Kompilierung ist, sie ist zumindest etwas langsamer als die einmalige Interpretation, und um das endgültige Ausführungsergebnis zu erhalten, müssen Sie einen Prozess des „Ausführens des kompilierten Codes“ durchlaufen. Daher ist bei Code, der „nur einmal ausgeführt“ wird, die Ausführung der Interpretation immer schneller als die Ausführung der JIT-Kompilierung. Wie kann es als „Code, der nur einmal ausgeführt wird“ betrachtet werden? Wenn diese beiden Bedingungen gleichzeitig erfüllt sind, wird sie grob gesagt streng „nur einmal ausgeführt“: „nur einmal aufgerufen, z. B. der Konstruktor einer Klasse“, „keine Schleife“. Man kann sagen, dass der Gewinn den Gewinn durch JIT-Kompilierung und Ausführung des Codes, der nur einmal ausgeführt wird, überwiegt. Bei Code, der nur wenige Male ausgeführt wird, gleicht die durch die JIT-Kompilierung erzielte Verbesserung der Ausführungsgeschwindigkeit möglicherweise nicht unbedingt den durch die anfängliche Kompilierung verursachten Mehraufwand aus. Nur für häufig ausgeführten Code kann die JIT-Kompilierung positive Vorteile garantieren.

Achten Sie auf die subtile dialektische Beziehung zwischen interpretierter Ausführung und kompilierter Ausführung in der Online-Umgebung. Die Belastung, der die Maschine im heißen Zustand standhält, ist größer als im kalten Zustand. Wenn der Datenverkehr im heißen Zustand dazu verwendet wird, den Datenfluss zu unterbrechen, kann der Server im kalten Zustand ausfallen, weil er den Datenverkehr nicht übertragen kann. Während des Freigabeprozesses der Produktionsumgebung wird die Freigabe stapelweise durchgeführt und entsprechend der Anzahl der Maschinen in mehrere Stapel unterteilt. Die Anzahl der Maschinen in jedem Stapel macht höchstens 1/8 des gesamten Clusters aus. Es gab einmal einen solchen Fehlerfall: Ein Programmierer veröffentlichte stapelweise auf der Veröffentlichungsplattform und gab bei der Eingabe der Gesamtzahl der Veröffentlichungen fälschlicherweise die Anzahl der Veröffentlichungen in zwei Stapel ein. Wenn es sich in einem heißen Zustand befindet, kann die Hälfte der Maschinen den Datenverkehr unter normalen Umständen kaum transportieren. Da die neu gestarteten JVMs jedoch interpretiert und ausgeführt werden, wurden Hot-Code-Statistiken und dynamische JIT-Kompilierung noch nicht durchgeführt. Nachdem die Maschinen dies getan haben gestartet, die aktuelle Version 1/2 ist erfolgreich. Alle Server sind gleichzeitig abgestürzt, was auf das Vorhandensein der JIT hinweist. — Ali-Team

Um die JIT-Kompilierung auszulösen, müssen Sie zunächst Hotcode identifizieren. Derzeit ist die Hauptmethode zur Identifizierung von Hotspot-Codes die Hotspot-Erkennung (Hot Spot Detection), die die folgenden zwei Typen aufweist:

  • Stichprobenbasierte Hot-Spot-Erkennung: Erkennen Sie regelmäßig die Stapeloberseiten jedes Threads. Wenn eine Methode häufig oben im Stapel erscheint, wird sie als Hot-Spot-Methode betrachtet. Der Vorteil liegt in der Einfachheit, der Nachteil besteht darin, dass es unmöglich ist, die Beliebtheit einer Methode genau zu bestätigen. Die Hotspot-Erkennung kann leicht durch Thread-Blockierungen oder andere Gründe gestört werden.
  • Zählerbasierte Hot-Spot-Erkennung: Die virtuelle Maschine, die diese Methode verwendet, erstellt einen Zähler für jede Methode, auch für einen Codeblock, und zählt die Ausführungszeiten der Methode. Wenn eine Methode den Schwellenwert überschreitet, wird sie als Hot-Spot-Methode betrachtet. JIT auslösen Zusammenstellung.

Der zweite Typ wird in der virtuellen HotSpot-Maschine verwendet – die zählerbasierte Hotspot-Erkennungsmethode, die für jede Methode zwei Zähler vorbereitet: einen Methodenaufrufzähler (der die Anzahl der Aufrufe einer Methode aufzeichnet) und einen Backside-Zähler (Schleife). läuft).

Viertens: AOT-Compiler

JDK9 führte den AOT-Compiler (statischer Ahead-of-Time-Compiler) ein. Dies ist das Gegenteil des Konzepts der Just-in-Time-Kompilierung. Unter Just-in-Time-Kompilierung versteht man den Prozess der Konvertierung von Bytecode in Maschinencode, der während der Ausführung des Programms direkt auf der Hardware ausgeführt werden kann, und der Bereitstellung in der Hosting-Umgebung. Unter AOT-Kompilierung versteht man die Konvertierung von Bytecode in Maschinencode vor der Programmausführung, sodass der native Code direkt verwendet werden kann, wenn das Programm ausgeführt wird.

Die Vorteile von AOT liegen auf der Hand: Die Java Virtual Machine lädt die vorkompilierte Binärbibliothek und kann sie direkt ausführen. Es ist nicht erforderlich, auf das Aufwärmen des Just-in-Time-Compilers zu warten, wodurch die schlechte Erfahrung einer „langsamen Ausführung beim ersten Mal“ bei Java-Anwendungen verringert wird.

Aber auch die Mängel liegen auf der Hand: Die dynamische Natur der Java-Sprache selbst bringt zusätzliche Komplexität mit sich, was sich auf die Qualität des statisch kompilierten Codes des Java-Programms auswirkt. Zum Beispiel das dynamische Laden von Klassen in der Java-Sprache, da AOT vor der Ausführung des Programms kompiliert wird, sodass diese Informationen nicht abgerufen werden können, was zu einigen Problemen führt.

Im Allgemeinen sind AOT-Compiler hinsichtlich der Kompilierungsqualität definitiv nicht so gut wie JIT-Compiler. Der Zweck seiner Existenz besteht darin, den Leistungsverbrauch oder Speicherverbrauch des JIT-Compilers zur Laufzeit oder den frühen Leistungsaufwand des Interpreters zu vermeiden.

In Bezug auf die Ausführungsgeschwindigkeit ist der vom AOT-Compiler kompilierte Code langsamer als der von JIT kompilierte, aber schneller als die interpretierte Ausführung. Auch hinsichtlich der Kompilierungszeit ist AOT eine konstante Geschwindigkeit. Daher ist die Existenz des AOT-Compilers eine Strategie für die JVM, Qualität zugunsten der Leistung zu opfern. So wie im Betriebsmodus der JVM der gemischte Modus ausgewählt ist, führt der C1-Kompilierungsmodus nur eine einfache Optimierung durch, während der C2-Kompilierungsmodus eine aggressivere Optimierung durchführt. Nutzen Sie die Vorteile der beiden Modi voll aus, um eine optimale Betriebseffizienz zu erreichen.

Schließlich bedeutet die Einführung von AOT in Spring 6, das offiziell im November 2022 veröffentlicht wurde, dass das Spring-Ökosystem offiziell eine vorzeitige Kompilierungstechnologie eingeführt hat. Themen wie Recycling.

Supongo que te gusta

Origin blog.csdn.net/tongkongyu/article/details/129327219
Recomendado
Clasificación