Caricamento delle classi in Java
Come la JVM trova e carica le classi tramite i class loader — bootstrap, platform, system e loader personalizzati.
Prima che la JVM possa eseguire anche una sola riga del tuo codice, deve trovare il file .class, leggere il suo bytecode, verificarlo e trasformarlo in un oggetto Class vivo in memoria. Questo compito spetta a un class loader. Il caricamento delle classi è ciò che rende java.lang.String disponibile senza che tu faccia nulla, ciò che permette a un JAR nel classpath di apparire a runtime, e ciò che alimenta i sistemi di plugin, i server applicativi e gli strumenti di hot-reload. Questo capitolo mostra come i loader sono organizzati, come funziona la delega e perché l'identità di una classe è più del semplice nome.
La gerarchia dei class loader
I loader sono organizzati come una catena di genitori, ciascuno responsabile di una diversa fonte di classi. In un JDK moderno (9+) ci sono tre loader integrati:
| Loader | Carica | Riportato come |
|---|---|---|
| Bootstrap | Classi JDK core (java.*, moduli base javax.*) | null |
| Platform | Il resto dei moduli della piattaforma JDK | un PlatformClassLoader |
| System / Application | Il tuo codice dal classpath/module path | un AppClassLoader |
Ogni classe ricorda il loader che l'ha definita. Puoi chiedere a qualsiasi classe quale loader l'ha prodotta:
ClassLoader appLoader = MyApp.class.getClassLoader(); // AppClassLoader
ClassLoader strLoader = String.class.getClassLoader(); // null = bootstrap
ClassLoader parent = appLoader.getParent(); // PlatformClassLoaderIl bootstrap loader è scritto in codice nativo, non in Java, motivo per cui String.class.getClassLoader() restituisce null invece di un oggetto — non esiste alcuna istanza Java di ClassLoader da restituire.
Il modello di delega
I class loader seguono il modello di delega parent-first. Quando gli viene chiesto di caricare una classe, un loader non cerca immediatamente di trovarla. Prima chiede al suo genitore, che chiede al suo genitore, risalendo fino al bootstrap. Solo se nessun antenato riesce a fornire la classe, il loader originale tenta di definirla autonomamente.
// Conceptual shape of ClassLoader.loadClass:
protected Class<?> loadClass(String name, boolean resolve) {
Class<?> c = findLoadedClass(name); // already loaded? reuse it
if (c == null) {
try {
c = parent.loadClass(name); // delegate UP first
} catch (ClassNotFoundException e) {
c = findClass(name); // only now load it myself
}
}
return c;
}Questa delega garantisce che i tipi core vengano caricati una sola volta, dal loader più in alto che è in grado di fornirli. Per questo motivo non puoi sovrascrivere java.lang.String inserendo il tuo String.class nel classpath — il bootstrap loader rivendica il nome per primo.
Caricamento, collegamento, inizializzazione
Portare una classe in vita avviene in tre fasi, che non sono la stessa cosa:
- Loading — leggi il bytecode e crea l'oggetto
Class. - Linking — verifica che il bytecode sia ben formato, prepara i campi statici con i valori predefiniti e risolve i riferimenti simbolici.
- Initialization — esegue gli inizializzatori statici e le assegnazioni dei campi statici (il metodo
<clinit>della classe).
Il fatto pratico fondamentale: l'inizializzazione è lazy e avviene esattamente una volta. Una classe viene inizializzata solo al primo utilizzo attivo — il primo new, la prima chiamata a un metodo statico o la prima lettura di un campo statico non costante.
class Config {
static final Map<String, String> SETTINGS = load(); // runs once, on first touch
static Map<String, String> load() {
System.out.println("Config initialized");
return Map.of("env", "prod");
}
}
// "Config initialized" prints only when Config is first actively used.Class loader personalizzati
Puoi estendere ClassLoader per caricare classi da qualsiasi fonte — un database, uno stream di rete, bytecode generato dinamicamente o un JAR cifrato. I due metodi che contano sono findClass (individua e definisce i byte) e defineClass (consegna i byte grezzi alla JVM, che restituisce un oggetto Class).
class BytesLoader extends ClassLoader {
private final byte[] bytecode;
BytesLoader(byte[] bytecode) { this.bytecode = bytecode; }
@Override
protected Class<?> findClass(String name) {
return defineClass(name, bytecode, 0, bytecode.length);
}
}URLClassLoader è la versione integrata di questa idea — puntala su JAR o directory e caricherà le classi su richiesta:
URL jar = Path.of("plugin.jar").toUri().toURL();
try (URLClassLoader loader = new URLClassLoader(new URL[]{ jar })) {
Class<?> plugin = loader.loadClass("com.example.Plugin");
Object instance = plugin.getDeclaredConstructor().newInstance();
}Identità della classe: nome più loader
Ecco la sottigliezza che spesso inganna gli sviluppatori: l'identità a runtime di una classe è il suo nome completamente qualificato e il loader che l'ha definita. Carica i byte di Widget attraverso due loader diversi e otterrai due oggetti Class distinti — non uguali, non assegnabili l'uno all'altro — anche se entrambi provengono da bytecode identico. È esattamente così che i server applicativi isolano due app distribuite che contengono entrambe una classe chiamata com.acme.Util.
Un esempio pratico: loader, delega, lazy loading e identità
Questo programma non richiede classi esterne — usa i loader già presenti in qualsiasi JVM. Percorre la catena dei loader, dimostra che le classi core provengono dal bootstrap loader, mostra la delega che restituisce lo stesso oggetto Class, osserva un inizializzatore statico che si attiva in modo lazy e una sola volta, quindi definisce lo stesso bytecode costruito a mano attraverso due loader per dimostrare la regola dell'identità nome-più-loader.
Cosa ricavare dall'esecuzione:
- La catena di loader stampata è la gerarchia live dei class loader con il tuo codice in fondo:
ClassLoadingDemoè stato definito da un loader a livello applicativo il cuigetParent()è il loader successivo in alto. Ogni loader conosce solo il suo genitore, e la catena risale sempre verso il bootstrap. String.class.getClassLoader()stampanull, il modo della JVM per dire "caricato dal bootstrap loader". I tipi JDK core riportano semprenullqui; un oggetto implicherebbe che provengono da un loader più basso, il che non accade mai.app.loadClass("java.lang.StringBuilder") == StringBuilder.classètrue. La delega ha inviato la richiesta al loader che già possiedeStringBuilder, quindi hai ottenuto lo stesso identico oggettoClass, non un duplicato — prova che la delega impedisce che i tipi core vengano caricati due volte.Lazy <clinit> runningviene stampato una sola volta, tra il marcatore--- referencing Lazy now ---e il primoLazy.VALUE = 42, e non viene stampato nuovamente alla seconda lettura. L'inizializzazione è lazy (ha aspettato il primo utilizzo) e idempotente (il blocco statico viene eseguito esattamente una volta per loader).aebsi chiamano entrambiWidget, eppurea == bèfalseea.isAssignableFrom(b)èfalse. Due loader hanno definito lo stesso bytecode in due tipi distinti — prova concreta che l'identità della classe a runtime è il nome completamente qualificato più il loader che la definisce, il meccanismo alla base dell'isolamento del classpath nei server applicativi.
Esercitazione
Argomenti correlati
Il caricamento delle classi si trova al confine tra la JVM e il tuo codice, quindi tocca diversi argomenti adiacenti:
- Architettura della JVM — dove il sottosistema dei class loader si inserisce tra il motore di esecuzione e le aree dati runtime.
- Java Memory Model — come le classi caricate e i loro dati statici vivono in memoria.
- Garbage Collection — i class loader (e le loro classi) possono essere scaricati quando non sono più referenziati.
- Introduzione ai Moduli — sul module path, il caricamento è guidato dalla leggibilità dei moduli anziché da un classpath piatto.
- Introduzione alla Reflection —
Class.forNameeloadClasssono i punti di ingresso su cui si basa la reflection.