JavaRush /Java Blog /Random-IT /Come vengono caricate le classi nella JVM
Aleksandr Zimin
Livello 1
Санкт-Петербург

Come vengono caricate le classi nella JVM

Pubblicato nel gruppo Random-IT
Una volta completata la parte più difficile del lavoro di un programmatore e scritta l’applicazione “Hello World 2.0”, non resta che assemblare il kit di distribuzione e trasferirlo al cliente, o almeno al servizio di testing. Nella distribuzione tutto è come dovrebbe essere e quando lanciamo il nostro programma entra in scena la Java Virtual Machine. Non è un segreto che la macchina virtuale legga i comandi presentati nei file di classe sotto forma di bytecode e li traduca come istruzioni per il processore. Propongo di capire un po' lo schema del bytecode che entra nella macchina virtuale.

Caricatore di classi

Viene utilizzato per fornire alla JVM il bytecode compilato, che solitamente viene memorizzato in file con estensione .class, ma può anche essere ottenuto da altre fonti, ad esempio scaricato dalla rete o generato dall'applicazione stessa. Come vengono caricate le classi nella JVM - 1Secondo le specifiche Java SE, per far funzionare il codice nella JVM, è necessario completare tre passaggi:
  • caricamento del bytecode dalle risorse e creazione di un'istanza della classeClass

    Ciò include la ricerca della classe richiesta tra quelle caricate in precedenza, l'ottenimento del bytecode per il caricamento e il controllo della sua correttezza, la creazione di un'istanza della classe Class(per lavorare con essa in fase di runtime) e il caricamento delle classi madri. Se le classi e le interfacce genitore non sono state caricate, la classe in questione viene considerata non caricata.

  • vincolante (o collegamento)

    Secondo le specifiche, questa fase è divisa in altre tre fasi:

    • Verifica , viene controllata la correttezza del bytecode ricevuto.
    • Preparazione , allocazione della RAM per i campi statici e inizializzazione con valori predefiniti (in questo caso, l'eventuale inizializzazione esplicita avviene già in fase di inizializzazione).
    • Risoluzione , risoluzione di collegamenti simbolici di tipi, campi e metodi.
  • inizializzando l'oggetto ricevuto

    qui, a differenza dei paragrafi precedenti, sembra tutto chiaro cosa dovrebbe accadere. Sarebbe ovviamente interessante capire esattamente come ciò avvenga.

Tutti questi passaggi vengono eseguiti in sequenza con i seguenti requisiti:
  • La classe deve essere completamente caricata prima di essere collegata.
  • Una classe deve essere completamente testata e preparata prima di essere inizializzata.
  • Gli errori di risoluzione del collegamento si verificano durante l'esecuzione del programma, anche se sono stati rilevati nella fase di collegamento.
Come sapete, Java implementa il caricamento lento (o lento) delle classi. Ciò significa che il caricamento delle classi dei campi di riferimento della classe caricata non verrà eseguito finché l'applicazione non incontrerà un riferimento esplicito ad essi. In altre parole, la risoluzione dei collegamenti simbolici è facoltativa e non avviene per impostazione predefinita. Tuttavia, l'implementazione JVM può anche utilizzare il caricamento della classe energetica, ad es. tutti i collegamenti simbolici devono essere presi in considerazione immediatamente. È a questo punto che si applica l’ultimo requisito. Vale anche la pena notare che la risoluzione dei collegamenti simbolici non è legata ad alcuna fase di caricamento della classe. In generale ognuna di queste fasi costituisce un buon studio; proviamo a capire la prima, ovvero il caricamento del bytecode.

Tipi di caricatori Java

Esistono tre caricatori standard in Java, ognuno dei quali carica una classe da una posizione specifica:
  1. Bootstrap è un caricatore di base, chiamato anche Primordial ClassLoader.

    carica le classi JDK standard dall'archivio rt.jar

  2. Extension ClassLoader : caricatore di estensioni.

    carica le classi di estensione, che si trovano nella directory jre/lib/ext per impostazione predefinita, ma possono essere impostate dalla proprietà di sistema java.ext.dirs

  3. System ClassLoader : caricatore di sistema.

    carica le classi applicative definite nella variabile d'ambiente CLASSPATH

Java utilizza una gerarchia di caricatori di classi, dove la radice è, ovviamente, quella base. Poi arriva il caricatore di estensioni e poi il caricatore di sistema. Naturalmente ogni loader memorizza un puntatore al genitore in modo da potergli delegare il caricamento nel caso in cui lui stesso non sia in grado di farlo.

Classe astratta ClassLoader

Ogni caricatore, ad eccezione di quello base, è un discendente della classe astratta java.lang.ClassLoader. Ad esempio, l'implementazione del caricatore di estensioni è la classe sun.misc.Launcher$ExtClassLoadere il caricatore di sistema è sun.misc.Launcher$AppClassLoader. Il caricatore di base è nativo e la sua implementazione è inclusa nella JVM. Qualsiasi classe che si estende java.lang.ClassLoaderpuò fornire il proprio modo di caricare le classi con blackjack e queste stesse. Per fare ciò è necessario ridefinire le modalità corrispondenti, che al momento posso considerare solo superficialmente, perché Non ho capito questo problema in dettaglio. Eccoli:
package java.lang;
public abstract class ClassLoader {
    public Class<?> loadClass(String name);
    protected Class<?> loadClass(String name, boolean resolve);
    protected final Class<?> findLoadedClass(String name);
    public final ClassLoader getParent();
    protected Class<?> findClass(String name);
    protected final void resolveClass(Class<?> c);
}
loadClass(String name)uno dei pochi metodi pubblici, che è il punto di ingresso per caricare le classi. La sua implementazione si riduce alla chiamata di un altro metodo protetto loadClass(String name, boolean resolve), che deve essere sovrascritto. Se guardi il Javadoc di questo metodo protetto, puoi capire qualcosa di simile a quanto segue: due parametri vengono forniti come input. Uno è il nome binario della classe (o il nome completo della classe) che deve essere caricato. Il nome della classe viene specificato con un elenco di tutti i pacchetti. Il secondo parametro è un flag che determina se è richiesta la risoluzione del collegamento simbolico. Per impostazione predefinita è false , il che significa che viene utilizzato il caricamento lento della classe. Inoltre, secondo la documentazione, nell'implementazione predefinita del metodo, viene effettuata una chiamata findLoadedClass(String name)che controlla se la classe è già stata caricata in precedenza e, in caso affermativo, restituisce un riferimento a questa classe. Altrimenti, verrà chiamato il metodo di caricamento della classe del caricatore genitore. Se nessuno dei caricatori riesce a trovare una classe caricata, ciascuno di essi, seguendo l'ordine inverso, proverà a trovare e caricare quella classe, sovrascrivendo il file findClass(String name). Questo sarà discusso più dettagliatamente nel capitolo “Schema di caricamento delle classi”. Ed infine, ultimo ma non meno importante, dopo il caricamento della classe, in base al flag di risoluzione , si deciderà se caricare le classi tramite link simbolici. Un chiaro esempio è che la fase di risoluzione può essere richiamata durante la fase di caricamento della classe. Di conseguenza, estendendo la classe ClassLoadere sovrascrivendone i metodi, il caricatore personalizzato può implementare la propria logica per fornire il bytecode alla macchina virtuale. Java supporta anche il concetto di caricatore di classi "corrente". Il caricatore corrente è quello che ha caricato la classe attualmente in esecuzione. Ogni classe sa con quale caricatore è stata caricata e puoi ottenere queste informazioni chiamando il suo file String.class.getClassLoader(). Per tutte le classi di applicazioni, il caricatore "corrente" è solitamente quello di sistema.

Tre principi del caricamento delle classi

  • Delegazione

    La richiesta di caricare la classe viene passata al caricatore genitore e viene effettuato un tentativo di caricare la classe stessa solo se il caricatore genitore non è stato in grado di trovare e caricare la classe. Questo approccio consente di caricare le classi con il caricatore il più vicino possibile a quello base. Ciò consente di ottenere la massima visibilità della classe. Ogni caricatore mantiene un record delle classi da esso caricate, inserendole nella sua cache. L'insieme di queste classi è chiamato scope.

  • Visibilità

    Il caricatore vede solo le “sue” classi e le classi “genitore” e non ha idea delle classi caricate dal suo “figlio”.

  • Unicità

    Una classe può essere caricata solo una volta. Il meccanismo di delega garantisce che il caricatore che avvia il caricamento della classe non sovraccarichi una classe precedentemente caricata nella JVM.

Pertanto, quando scrive il suo bootloader, uno sviluppatore dovrebbe essere guidato da questi tre principi.

Schema di caricamento delle classi

Quando si verifica una chiamata per caricare una classe, questa classe viene cercata nella cache delle classi già caricate del caricatore corrente. Se la classe desiderata non è stata caricata prima, il principio della delega trasferisce il controllo al caricatore principale, che si trova un livello più in alto nella gerarchia. Il caricatore genitore tenta anche di trovare la classe desiderata nella sua cache. Se la classe è già stata caricata e il caricatore conosce la sua posizione, Classverrà restituito un oggetto di quella classe. In caso contrario, la ricerca continuerà finché non raggiungerà il bootloader di base. Se il caricatore di base non dispone di informazioni sulla classe richiesta (ovvero, non è ancora stata caricata), il bytecode di questa classe verrà cercato nella posizione delle classi conosciute dal caricatore specificato e se la classe non può essere caricato, il controllo tornerà al caricatore figlio, che tenterà di caricare da fonti a lui note. Come accennato in precedenza, la posizione delle classi per il caricatore di base è la libreria rt.jar, per il caricatore di estensioni - la directory con estensioni jre/lib/ext, per quella di sistema - CLASSPATH, per quella dell'utente può essere qualcosa di diverso . Pertanto, l'avanzamento del caricamento delle classi va nella direzione opposta: dal caricatore root a quello attuale. Quando viene trovato il bytecode della classe, la classe viene caricata nella JVM e viene ottenuta un'istanza del tipo Class. Come puoi facilmente vedere, lo schema di caricamento descritto è simile all'implementazione del metodo sopra descritta loadClass(String name). Di seguito puoi vedere questo diagramma nel diagramma.
Come vengono caricate le classi nella JVM - 2

Come conclusione

Nei primi passi nell'apprendimento di una lingua, non c'è bisogno particolare di capire come vengono caricate le classi in Java, ma conoscere questi principi di base ti aiuterà a evitare la disperazione quando incontri errori come ClassNotFoundExceptiono NoClassDefFoundError. Bene, o almeno capisci approssimativamente qual è la radice del problema. Pertanto, si verifica un'eccezione ClassNotFoundExceptionquando una classe viene caricata dinamicamente durante l'esecuzione del programma, quando i caricatori non riescono a trovare la classe richiesta né nella cache né lungo il percorso della classe. Ma l'errore NoClassDefFoundErrorè più critico e si verifica quando la classe richiesta era disponibile durante la compilazione, ma non era visibile durante l'esecuzione del programma. Ciò può accadere se il programma ha dimenticato di includere la libreria che utilizza. Ebbene, il fatto stesso di comprendere i principi della struttura dello strumento che usi nel tuo lavoro (non necessariamente un'immersione chiara e dettagliata nelle sue profondità) aggiunge una certa chiarezza alla comprensione dei processi che avvengono all'interno di questo meccanismo, che, in a sua volta, porta ad un uso sicuro di questo strumento.

Fonti

Come funziona ClassLoader in Java Nel complesso una fonte molto utile con una presentazione accessibile delle informazioni. Caricamento delle classi, ClassLoader Un articolo piuttosto lungo, ma con un'enfasi su come realizzare la propria implementazione del caricatore con queste stesse. ClassLoader: caricamento dinamico delle classi Sfortunatamente, questa risorsa non è disponibile al momento, ma lì ho trovato il diagramma più comprensibile con uno schema di caricamento delle classi, quindi non posso fare a meno di aggiungerlo. Specifiche Java SE: capitolo 5. Caricamento, collegamento e inizializzazione
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION