W3docs

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:

LoaderCaricaRiportato come
BootstrapClassi JDK core (java.*, moduli base javax.*)null
PlatformIl resto dei moduli della piattaforma JDKun PlatformClassLoader
System / ApplicationIl tuo codice dal classpath/module pathun 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();          // PlatformClassLoader

Il 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.
  • Linkingverifica 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.

java— editable, runs on the server

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 cui getParent() è il loader successivo in alto. Ogni loader conosce solo il suo genitore, e la catena risale sempre verso il bootstrap.
  • String.class.getClassLoader() stampa null, il modo della JVM per dire "caricato dal bootstrap loader". I tipi JDK core riportano sempre null qui; 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à possiede StringBuilder, quindi hai ottenuto lo stesso identico oggetto Class, non un duplicato — prova che la delega impedisce che i tipi core vengano caricati due volte.
  • Lazy <clinit> running viene stampato una sola volta, tra il marcatore --- referencing Lazy now --- e il primo Lazy.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).
  • a e b si chiamano entrambi Widget, eppure a == b è false e a.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

Pratica
Due class loader personalizzati distinti caricano bytecode identico per una classe chiamata 'com.acme.Widget'. Cosa è vero degli oggetti Class risultanti a (dal loader 1) e b (dal loader 2)?
Due class loader personalizzati distinti caricano bytecode identico per una classe chiamata 'com.acme.Widget'. Cosa è vero degli oggetti Class risultanti a (dal loader 1) e b (dal loader 2)?

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 ReflectionClass.forName e loadClass sono i punti di ingresso su cui si basa la reflection.
Was this page helpful?