English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Il processo di caricamento completo di una classe Java
Un file Java, dal momento del caricamento alla disattivazione, deve passare attraverso quattro fasi:
Caricamento -> Collegamento (verifica + preparazione +解析) -> Inizializzazione (preparazione prima dell'uso) -> Uso -> Disattivazione
Il processo di caricamento (eccetto il caricamento personalizzato) + il collegamento sono completamente responsabili del JVM, quando deve essere eseguita l'inizializzazione della classe (il caricamento + il collegamento sono stati completati prima), la JVM ha regole rigorose (quattro casi):
1. Quando si incontrano le istruzioni di byte code new, getstatic, putstatic, invokestatic, se la classe non è stata ancora inizializzata, deve essere inizializzata immediatamente. In realtà ci sono tre casi: quando si istanzia un'oggetto di una classe con new, quando si legge o si imposta un campo statico della classe (esclusi i campi statici decorati con final, poiché sono già stati inseriti nel pool delle costanti), e quando si esegue un metodo statico.
2. Quando si utilizzano i metodi di reflection di java.lang.reflect.* per chiamare una classe, se la classe non è stata inizializzata, inizializzala immediatamente.
3. Durante l'inizializzazione di una classe, se il padre non è stato inizializzato, inizializzerà prima il padre.
4. Quando il JVM si avvia, l'utente deve specificare una classe principale da eseguire (contenente static void main(String[] args)), quindi il JVM inizializzerà prima questa classe.
Le 4 preelaborazioni sopra menzionate sono chiamate riferimenti attivi a una classe, tutte le altre situazioni, dette riferimenti passivi, non attiveranno l'inizializzazione della classe. Di seguito sono riportati anche alcuni esempi di riferimenti passivi:
/** * Scena di riferimento passivo 1 * Riferimento a un campo statico del padre tramite una sottoclasse non attiva l'inizializzazione della sottoclasse * @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); } }
Risultato di output: super class init.
/** * Scena di riferimento passivo 2 * Riferimento a una classe tramite array non attiva l'inizializzazione di questa classe * @author volador * */ public class test{ public static void main(String[] args){ SuperClass s_list=new SuperClass[10]; } }
Risultato di output: senza output
/** * Scena di riferimento passivo 3 * Le costanti vengono memorizzate nel pool delle costanti della classe chiamante durante la fase di compilazione, in sostanza non vengono riferite alla classe che definisce la costante, quindi naturalmente non verrà attivata l'inizializzazione della classe che definisce la costante * @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); } }
Risultato di output: hello (suggerimento: quando si compila, ConstClass.value è stato trasformato in costante 'hello' e inserito nel pool delle costanti della classe test)
Come sopra per l'inizializzazione delle classi, anche le interfacce devono essere inizializzate, l'inizializzazione delle interfacce è un po' diversa dall'inizializzazione delle classi:
Il codice sopra è tutto usato per output informazioni di inizializzazione con static {}, non può essere fatto con l'interfaccia, ma il compilatore ancora genera un costruttore di classe <clinit>() per l'interfaccia quando l'interfaccia viene inizializzata, per inizializzare le variabili membro dell'interfaccia, questo è anche fatto nella inizializzazione della classe. La differenza vera e propria è nel punto terzo, la inizializzazione della classe richiede che tutti i genitori siano stati inizializzati prima dell'esecuzione dell'inizializzazione della classe, ma l'inizializzazione dell'interfaccia sembra non essere molto interessata all'inizializzazione dell'interfaccia genitore, cioè, quando l'interfaccia figlia viene inizializzata, non è richiesto che l'interfaccia genitore sia stata inizializzata, ma solo quando viene effettivamente utilizzata l'interfaccia genitore (ad esempio, quando si fa riferimento ai costanti dell'interfaccia).
Ecco una分解 di tutto il processo di caricamento di una classe: caricamento -> verifica -> preparazione ->解析 -> inizializzazione
Prima di tutto, è il caricamento:
Questa sezione deve completare 3 cose per il JVM:
1. Ottenere il flusso di byte binario definito dal nome qualificato di una classe attraverso.
2. Convertire la struttura di archiviazione statica rappresentata da questo flusso di byte in una struttura di dati di esecuzione del metodo.
3. Generare un oggetto java.lang.Class che rappresenta questa classe nel heap di Java, come punto di accesso ai dati nel metodo.
Riguardo al primo punto, è molto flessibile, molte tecnologie entrano qui, perché non limita da dove proviene il flusso binario:
Dal file class -> caricamento di file generico
Dal pacchetto zip -> caricamento delle classi dal jar
Dal network -> Applet
..........
In confronto con altre fasi del processo di caricamento, la fase di caricamento ha la maggiore controllabilità, perché il carico di classe può essere del sistema o del proprio, il programmatore può scrivere il proprio carico di classe per controllare l'acquisizione del flusso di byte.
Dopo aver completato l'acquisizione del flusso binario, verrà memorizzato nel metodo in modo conforme alle esigenze del JVM, e contemporaneamente verrà istanziato un oggetto java.lang.Class nel heap per associarlo ai dati nel heap.
Dopo il completamento del caricamento, inizierà a verificare quei flussi di byte (in realtà molte delle fasi sono incrociate con quelle sopra, come la verifica del formato del file):
L'obiettivo della verifica: garantire che l'informazione del flusso di byte del file class sia conforme al gusto del JVM, non faccia sentire il JVM a suo agio. Se il file class è stato compilato da codice Java puro, naturalmente non si verificheranno problemi irregolari come il superamento dei limiti degli array, il salto a blocchi di codice inesistenti, ecc., perché se si verificano tali fenomeni, il compilatore rifiuterà di compilare. Tuttavia, come è stato detto prima, il flusso di file Class non viene necessariamente compilato dal codice sorgente Java, ma può venire dalla rete o da altri luoghi, persino puoi scriverlo tu stesso in esadecimale, se il JVM non verifica questi dati, alcuni flussi di byte dannosi possono far crollare completamente il JVM.
La verifica attraversa diversi passaggi: verifica del formato del file -> verifica dei metadati -> verifica dei byte code -> verifica delle referenze simboliche
Verifica del formato del file: verifica se il flusso di byte rispetta lo standard del formato del file Class e se la versione può essere gestita dalla versione corrente del JVM. Dopo aver verificato che tutto è a posto, il flusso di byte può essere salvato nell'area di metodo della memoria. Le tre verifiche successive vengono eseguite nell'area di metodo.
Verifica dei metadati: analisi semantica delle informazioni descritte dai byte code, garantendo che il contenuto sia conforme alle norme grammaticali del linguaggio java.
Verifica dei byte code: la più complessa, verifica il contenuto del corpo del metodo, garantendo che non faccia nulla fuori dagli schemi durante l'esecuzione.
Verifica delle referenze simboliche: verifica l'autenticità e la fattibilità delle referenze, come quando il codice fa riferimento a altre classi, in questo caso, si deve verificare se quegli oggetti esistono veramente; o quando il codice accede alle proprietà di altre classi, si verifica la visibilità di quelle proprietà. (Questa fase preparerà la base per il lavoro di elaborazione successivo).
La fase di verifica è molto importante, ma non necessaria. Se alcuni codici vengono utilizzati ripetutamente e verificati per la loro affidabilità, la fase di implementazione può tentare di utilizzare il parametro -Xverify:none per disattivare la maggior parte delle misure di verifica delle classi, per abbreviare il tempo di caricamento delle classi.
Dopo aver completato i passaggi precedenti, si passerà alla fase di preparazione:
In questa fase, viene assegnata memoria ai variabili di classe (quelle statiche) e impostata come valore iniziale, e questa memoria viene assegnata nell'area di metodo. È necessario chiarire che questo passaggio assegnerà solo un valore iniziale a quelle variabili statiche, mentre quelle variabili d'istanza vengono assegnate durante l'istanziazione degli oggetti. La configurazione del valore iniziale delle variabili di classe è leggermente diversa dall'attribuzione delle variabili di classe, come nel caso seguente:
public static int value=123;
In questa fase, il valore di value sarà 0 invece di 123, perché a questo punto non è iniziata l'esecuzione di alcun codice java, 123 è invisibile e l'istruzione putstatic che assegna 123 a value esiste solo nel <clinit>() dopo che il programma è stato compilato, quindi l'attribuzione di 123 a value avviene solo durante l'inizializzazione.
C'è anche un'eccezione qui:
public static final int value=123;
Qui, durante la fase di preparazione, il valore di value viene inizializzato a 123. Questo significa che durante la fase di compilazione, javac genererà un attributo ConstantValue per questo valore speciale e, durante la fase di preparazione, jm assegnerrà il valore di ConstantValue a value.
Dopo aver completato il passo precedente, deve essere eseguita l'elaborazione. L'elaborazione sembra essere una conversione dei campi, dei metodi e di altre cose delle classi, che coinvolge specificamente il formato del contenuto del file Class, ma non è stato esplorato in profondità.
Il processo di inizializzazione è l'ultimo passo del processo di caricamento della classe: }}
Nel processo di caricamento della classe, oltre al fatto che l'utente può partecipare attraverso il carico di classi personalizzate durante la fase di caricamento, tutte le altre azioni sono completamente主导 dal JVM, fino a quando si inizia a eseguire il codice java, inizia a eseguire il codice.
Questo passaggio eseguirà alcune operazioni preliminari, attenzione a distinguere che durante la fase di preparazione, è stata eseguita una sistema di assegnazione una volta per le variabili di classe.
In realtà, questo passaggio è il processo di esecuzione del metodo <clinit>(); del programma. Ora esaminiamo il metodo <clinit>();:
Il metodo <clinit>() è chiamato metodo costruttore della classe, che combina automaticamente tutte le assegnazioni delle variabili di classe e le istruzioni nei blocchi statici, mantenendo l'ordine di loro posizione come nell'archivio sorgente.
Il metodo <clinit>(); è diverso dal costruttore della classe, non richiede di chiamare esplicitamente il metodo <clinit>(); del padre; il JVM garantisce che il metodo <clinit>(); della sottoclasse venga eseguito prima di eseguire questo metodo, il che significa che il primo metodo <clinit>() eseguito nel JVM è quello di java.lang.Object.
Ecco un esempio per illustrare:
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); }
Prima di tutto, Sub.B fa riferimento ai dati statici, Sub deve essere inizializzato. Allo stesso tempo, il padre Parent deve eseguire l'azione di inizializzazione prima. Dopo l'inizializzazione del Parent, A=2, quindi B=2; il processo precedente è equivalente a:
static class Parent{ <clinit>(){ public static int A=1; static{ A=2; } } } static class Sub extends Parent{ <clinit>(){ // Il JVM esegue prima il metodo di questa classe padre prima di eseguire qui public static int B=A; } } public static void main(String[] args){ System.out.println(Sub.B); }
Il metodo <clinit>(); non è obbligatorio per le classi e gli interfacce; se né la classe né l'interfaccia assegnano valori alle variabili di classe né contengono blocchi statici, il metodo <clinit>() non verrà generato dal compilatore.
Poiché non è possibile esistere in un'interfaccia un blocco statico{} di questo tipo, ma potrebbe esistere un'operazione di assegnazione di variabili durante l'inizializzazione delle variabili, quindi all'interno dell'interfaccia verrà anche generato il costruttore <clinit>(). Ma diversamente dal caso delle classi, non è necessario eseguire il metodo <clinit>(); dell'interfaccia padre prima di eseguire il metodo <clinit>(); dell'interfaccia figlia; quando vengono utilizzati i variabili definiti nell'interfaccia padre, l'interfaccia padre viene inizializzata.
Inoltre, la classe che implementa un'interfaccia non esegue il metodo <clinit>() dell'interfaccia durante l'inizializzazione.
Inoltre, il JVM garantisce che il metodo <clinit>(); di una classe venga correttamente bloccato e sincronizzato in ambiente multithread. <Poiché l'inizializzazione viene eseguita una sola volta>.
Di seguito, vediamo un esempio per chiarire:
public class DeadLoopClass { static{ if(true){ System.out.println("Deve essere inizializzato da ["+Thread.currentThread()+"]; ora arriva un ciclo infinito"); while(treu){} } } /** * @param args */ public static void main(String[] args) { // TODO Auto-generato metodo stub System.out.println("toplaile"); Runnable run=new Runnable(){ @Override public void run() { // TODO Auto-generato metodo stub System.out.println("["+Thread.currentThread()+"] Stiamo per istanziare quella classe"); DeadLoopClass d=new DeadLoopClass(); System.out.println("["+Thread.currentThread()+"] Ha completato l'inizializzazione della classe"); }}; new Thread(run).start(); new Thread(run).start(); } }
In questo caso, durante l'esecuzione vedrai un fenomeno di blocco.
Grazie per la lettura, spero che possa aiutarti, grazie per il supporto al nostro sito!