English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
den gesamten Prozess der Java-Klassenausführung
Ein Java-Datei muss von dem Prozess geladen bis entladen durchlaufen, insgesamt muss er4Diese Stufen:
>laden->verknüpfen (verifizieren+>bereiten+>parsen)->initialisieren (Vorbereitung vor der Verwendung)->verwenden->entsladen
davon geladen (außer benutzerdefiniertem Laden)+Der Prozess der Verknüpfung wird vollständig vom JVM verantwortet, wann soll eine Klasse initialisiert werden (laden+verbinden, die在此之前已经完成了),jvm hat strenge Vorschriften (vier Fälle):
1. Auftritt von new, getstatic, putstatic und invokestatic, dies4Wenn eine Bytecode-Anweisung hinzugefügt wird und die Klasse noch nicht initialisiert wurde, wird sie sofort initialisiert. Es ist quasi3Diese Situationen: Wenn eine Klasse mit new instanziert wird, wenn statische Felder der Klasse gelesen oder gesetzt werden (ausgenommen die mit final dekorierten statischen Felder, da sie bereits in den Konstantenpool eingefügt wurden) und wenn statische Methoden ausgeführt werden.
2.Verwenden Sie java.lang.reflect.*.Wenn das Reflection-Methode auf eine Klasse aufgerufen wird, wenn die Klasse noch nicht initialisiert wurde, wird sie sofort initialisiert.
3.Wenn eine Klasse initialisiert wird und ihr Vater noch nicht initialisiert wurde, wird zuerst ihr Vater initialisiert.
4.Wenn der JVM gestartet wird, muss der Benutzer eine Hauptklasse angeben, die ausgeführt werden soll (enthalten die Klasse, die static void main(String[] args) enthält), dann initialisiert der JVM zuerst diese Klasse.
Oben genannte4Diese Vorverarbeitung wird als aktive Referenzierung auf eine Klasse bezeichnet, alle anderen Szenarien, die als passive Referenzierung bezeichnet werden, lösen die Initialisierung der Klasse nicht aus. Nachfolgend werden einige Beispiele für passive Referenzierung aufgeführt:
/** * Szenario der passiven Referenz1 * Durch Referenzierung auf das statische Feld der Elternklasse durch den Unterklassenbezug wird die Initialisierung der Unterklasse nicht ausgelöst * @author volador * */ class SuperClass{ static{ System.out.println("super class init."); } public static int value=123; } class SubClass extends SuperClass{ static{ System.out.println("sub class init."); } } public class test{ public static void main(String[]args){ System.out.println(SubClass.value); } }
Ausgabeergebnis: super class init.
/** * Szenario der passiven Referenz2 * Durch Referenzierung auf das Array wird die Initialisierung dieser Klasse nicht ausgelöst * @author volador * */ public class test{ public static void main(String[] args){ SuperClass s_list=new SuperClass[10]; } }
Ausgabeergebnis: Es wird nichts ausgegeben
/** * Szenario der passiven Referenz3 * Konstanten werden während der Kompilierung in den Konstantenpool der aufrufenden Klasse gespeichert. Es wird im Wesentlichen keine Referenz auf die Klasse der definierten Konstanten hergestellt, daher wird die Initialisierung der Klasse der definierten Konstanten natürlich nicht ausgelöst * @author root * */ class ConstClass{ static{ System.out.println("ConstClass init."); } public final static String value="hello"; } public class test{ public static void main(String[] args){ System.out.println(ConstClass.value); } }
Ausgabeergebnis: hello (Tipp: Beim Kompilieren wird ConstClass.value bereits in den Konstantenpool der Klasse test als Konstante eingefügt)
Das war für die Initialisierung der Klasse, aber auch Interfaces müssen initialisiert werden, die Initialisierung der Interfaces ist etwas anders als die der Klassen: }}
Der obige Code verwendet static{} zum Ausgeben von Initialisierungsinformationen, was für Interfaces nicht möglich ist, aber der Compiler generiert immer noch einen <clinit>()-Klassenkonstruktor für die Initialisierung der Member-Variables eines Interfaces, was auch in der Initialisierung der Klassen berücksichtigt wird. Der eigentliche Unterschied liegt im dritten Punkt, dass die Initialisierung einer Klasse vor der Ausführung der Initialisierung der Elternklasse abgeschlossen sein muss, während die Initialisierung eines Interfaces anscheinend nicht sehr daran interessiert ist, dass die Elterninterface initialisiert wird, d.h. die Initialisierung des Kindinterfaces erfordert nicht, dass die Initialisierung des Elterninterfaces abgeschlossen ist, sondern wird erst initialisiert, wenn das Elterninterface tatsächlich verwendet wird (z.B. wenn auf Konstanten des Interfaces verwiesen wird).
Unten wird der gesamte Ladevorgang einer Klasse aufgeschlüsselt: Laden->Überprüfung->Vorbereitung->Parsing->Initialisierung
Zunächst ist das Laden:
Dieses Stück muss die Virtuelle Maschine erledigen3Diese Angelegenheit:
1.Durch die vollständige Qualifikationsname einer Klasse den Binär-Byte-Stream, der diese Klasse definiert, abrufen.
2.Die statische Speicherstruktur, die dieser Byte-Stream repräsentiert, in die laufzeitbezogene Datenstruktur des Methodenbereichs umwandeln.
3.Einen java.lang.Class-Objekt, der diesen Typ repräsentiert, im Java-Heap generieren und als Zugangspunkt für diese Daten im Methodenbereich verwenden.
In Bezug auf den ersten Punkt ist es sehr flexibel, viele Technologien beginnen hier, da es keine Beschränkung gibt, woher der Binärstream kommt:
Von Class-Dateien->Allgemeines Laden von Dateien
Von Zip-Paketen->Laden von Klassen aus JAR
Von Netzwerken->Applet
..........
Im Vergleich zu anderen Phasen des Ladens ist die Kontrolle im Ladevorgang am stärksten, da der Class-Loader sowohl systematisch als auch selbst definiert werden kann, und der Programmierer kann seine eigenen Loader schreiben, um den Zugriff auf den Byte-Stream zu kontrollieren.
Nachdem der Binärstream abgerufen wurde, wird er in der Weise, die der JVM benötigt, im Methodenbereich gespeichert und gleichzeitig wird ein java.lang.Class-Objekt im Java-Heap instantiiert, um mit den Daten im Heap in Verbindung zu setzen.
Nach dem Laden müssen Sie mit der Überprüfung dieser Byte-Streams beginnen (viele dieser Schritte laufen tatsächlich parallel, z.B. die Validierung des Dateiformats):
Zweck der Überprüfung: Stellen Sie sicher, dass die Byte-Stream-Informationen der Class-Datei dem Geschmack des JVM entsprechen und den JVM nicht unangenehm fallen. Wenn die Class-Datei aus reinem Java-Code kompiliert wurde, sollten natürlicherweise keine ungesunden Probleme wie Array-Überschreitungen oder Sprünge zu nicht existierenden Code-Blöcken auftreten, da der Compiler dies ablehnen würde, sobald solches auftritt. Aber, wie gesagt, der Class-File-Stream wird nicht unbedingt aus Java-Quellcode kompiliert, sondern auch aus dem Netzwerk oder anderen Orten, oder Sie können selbst16Wenn das JVM diese Daten nicht überprüft, könnte ein schädlicher Byte-Stream das JVM vollständig zum Absturz bringen.
Die Überprüfung durchläuft mehrere Schritte: Dateiformatvalidierung->Metadatenvalidierung->Bytecode-Überprüfung->Symbolische Referenzvalidierung
Dateiformatvalidierung: Überprüfung, ob der Byte-Stream den Normen des Class-Dateiformats entspricht und ob die Version von der aktuellen JVM-Version verarbeitet werden kann. Alles in Ordnung, dann kann der Byte-Stream in den Speicherbereich der Methode gespeichert werden. Nachdem3Diese Prüfungen erfolgen alle im Methodenbereich.
Metadatenvalidierung: Semantische Analyse der durch Bytecode beschriebenen Informationen, um sicherzustellen, dass die beschriebenen Inhalte den Syntaxnormen der Java-Sprache entsprechen.
Bytecode-Überprüfung: Der komplexeste, der den Inhalt des Methodenkörpers überprüft, um sicherzustellen, dass er während der Laufzeit keine ungewöhnlichen Dinge tut.
Symbolische Referenzvalidierung: um die Authentizität und Durchführbarkeit einiger Referenzen zu überprüfen, z.B. wenn andere Klassen im Code referenziert werden, muss überprüft werden, ob diese tatsächlich existieren; oder wenn auf Attribute anderer Klassen zugegriffen wird, wird die Zugänglichkeit dieser Attribute überprüft. (Dieser Schritt legt den Grund für die spätere Analysearbeit.)
Die Validierungsphase ist wichtig, aber nicht obligatorisch, falls einige Codezeilen wiederholt verwendet und überprüft wurden und ihre Zuverlässigkeit bestätigt wurde, kann in der Implementierungsphase versucht werden,-Der Parameter Xverify:none wird verwendet, um die meisten Class-Validierungsmassnahmen abzuschalten, um die Zeit der Klassenladung zu verkürzen.
Nachdem die vorherigen Schritte abgeschlossen sind, tritt der Vorbereitungsphase in der Regel folgt:
In dieser Phase wird Speicher für Klassenvariablen (d.h. statische Variablen) zugewiesen und die Phase der Vergleichung mit den Initialwerten dieser Variablen gestartet, diese Speicher werden im Methodenbereich zugewiesen. Es ist zu beachten, dass in diesem Schritt nur eine Initialwertzuweisung für statische Variablen vorgenommen wird, während Instanzvariablen bei der Instanziierung von Objekten zugewiesen werden. Der Prozess der Initialwertzuweisung für Klassenvariablen ist etwas anders als die Zuweisung der Klassenvariablen, wie zum Beispiel:
public static int value=123;
wird in dieser Phase den Wert 0 und nicht123, da zu diesem Zeitpunkt noch kein Java-Code ausgeführt wird,123noch unsichtbar, während wir das123Die putstatic-Instruktion, die value zuweisen, existiert im <clinit>() nach der Kompilierung des Programms, daher wird value so zugewiesen:123wird nur während der Initialisierung ausgeführt.
Hier gibt es auch eine Ausnahme:
public static final int value=123;
Hier wird der Wert von value während der Vorbereitungsphase initialisiert.123Das bedeutet, dass javac während der Kompilierungsphase eine ConstantValue-Eigenschaft für diesen speziellen Wert generiert und in der Vorbereitungsphase jm entsprechend dem Wert dieser ConstantValue den Wert für value zuweist.
Nachdem das vorherige Schritt abgeschlossen ist, muss die Analyse durchgeführt werden. Die Analyse scheint die Umwandlung der Felder, Methoden und anderer Dinge der Klasse zu betreffen, und betrifft spezifisch die Formatinhalte der Class-Datei, ohne tief in das Thema einzutauchen.
初始化过程是类加载过程的最后一步:
Der Initialisierungsprozess ist der letzte Schritt im Prozess der Klassenladung:
Während des gesamten Prozess der Klassenladung, außer dass der Benutzer in der Ladephase durch benutzerdefinierte Class-Loader beteiligt werden kann, werden die anderen Aktionen vollständig vom JVM geleitet; bis zur Initialisierung, beginnt der eigentliche Code in Java wirklich ausgeführt zu werden.
In diesem Schritt wird einige Voroperationen ausgeführt; beachten Sie, dass bereits im Vorbereitungsstadium eine Systemzuweisung für die Klassenvariablen durchgeführt wurde.
Das <clinit>()-Verfahren wird als Klasse-Konstruktor-Methode bezeichnet und besteht aus der Kombination aller Zuweisungen der Klassenvariablen und der Anweisungen in den statischen Code-Blöcken, die vom Compiler automatisch gesammelt werden und in der gleichen Reihenfolge wie in der Quelldatei geordnet sind.
Das <clinit>()-Verfahren ist anders als der Konstruktor der Klasse; es muss nicht explizit der <clinit>()-Methode des Überinterfaces aufgerufen werden; der Virtual Machine stellt sicher, dass der <clinit>()-Methode des Unterinterfaces vor seiner Ausführung der <clinit>()-Methode des Überinterfaces bereits ausgeführt wurde, das heißt, der erste <clinit>()-Methode, die im Virtual Machine ausgeführt wird, ist die von java.lang.Object.
Nun folgen wir einem Beispiel, um dies zu erläutern:
static class Parent{ public static int A=1; static{ A=2; } } static class Sub extends Parent{ public static int B=A; } public static void main(String[] args){ System.out.println(Sub.B); }
Zunächst wird auf das statische Daten in Sub.B verwiesen, die Klasse Sub muss initialisiert werden. Gleichzeitig muss der Elternklasse Parent erst initialisiert werden. Nach der Initialisierung von Parent, A=2,daher B=2;dieser Prozess entspricht:
static class Parent{ <clinit>(){ public static int A=1; static{ A=2; } } } static class Sub extends Parent{ <clinit>(){ //Die JVM führt zuerst die Methode des Elternklassen aus, bevor sie hieraus ausgeführt wird public static int B=A; } } public static void main(String[] args){ System.out.println(Sub.B); }
Das <clinit>();-Verfahren ist für Klassen und Interfaces nicht obligatorisch; wird in der Klasse oder im Interface keine Zuweisung für Klassenvariablen vorgenommen und es gibt keinen statischen Code-Block, wird der Compiler die <clinit>()-Methode nicht generieren.
Da das statische Code-Block "static{}" im Interface nicht existieren kann, aber dennoch eine Variable-Zuweisung bei der Initialisierung der Variablen existieren kann, wird im Interface auch ein <clinit>()-Konstruktor generiert. Aber anders als bei Klassen muss vor der Ausführung des <clinit>();-Methodens des Unterinterfaces nicht die <clinit>();-Methode des Überinterfaces ausgeführt werden; erst wird das Überinterface initialisiert, wenn die Variablen, die im Überinterface definiert sind, verwendet werden.
Außerdem wird die Implementierung einer Schnittstelle in der Initialisierungsphase ebenfalls nicht den <clinit>()-Methodenaufruf der Schnittstelle ausführen.
Außerdem gewährleistet die JVM, dass der <clinit>();-Methodenaufruf einer Klasse in einem Multithreading-Umfeld korrekt gesperrt und synchronisiert wird. <Da die Initialisierung nur einmal ausgeführt wird>.
Lassen Sie uns dies mit einem Beispiel erläutern:
public class DeadLoopClass { static{ if(true){ System.out.println("Wird von ["+Thread.currentThread()+"] Initialisiert, jetzt folgt ein endloser Zyklus"); while(true){} } } /** * @param args */ public static void main(String[] args) { // TODO Auto-generierter Methodenstift System.out.println("toplaile"); Runnable run=new Runnable(){ @Override public void run() { // TODO Auto-generierter Methodenstift System.out.println("["+Thread.currentThread()+"] Wir werden diese Klasse instanziieren"); DeadLoopClass d=new DeadLoopClass(); System.out.println("["+Thread.currentThread()+"] Die Initialisierung dieser Klasse wurde abgeschlossen"); }}; new Thread(run).start(); new Thread(run).start(); } }
Hier wird bei der Ausführung ein Blockierungsphänomen zu sehen sein.
Vielen Dank für das Lesen, ich hoffe, es hilft Ihnen, danke für Ihre Unterstützung dieser Seite!