W3docs

Architettura della JVM in Java

Struttura della JVM: class loader, aree dati di runtime, motore di esecuzione e interfaccia nativa.

La Java Virtual Machine (JVM) è il programma che esegue il tuo programma. Compili il sorgente .java in bytecode .class neutro rispetto alla piattaforma, e la JVM è ciò che carica quel bytecode, lo verifica, dispone i suoi oggetti in memoria e lo esegue sull'hardware reale. "Scrivi una volta, esegui ovunque" significa in realtà "compila una volta, e lascia che la JVM di ciascuna piattaforma faccia il resto." Questo capitolo mappa la struttura interna della JVM — i tre sottosistemi che ogni implementazione condivide — in modo che i capitoli sul modello di memoria e sulla garbage collection che seguono abbiano un quadro di riferimento.

Tre sottosistemi

Una JVM, indipendentemente dal fornitore, è organizzata in tre sottosistemi cooperanti. Tutto il resto è dettaglio all'interno di uno di essi.

SottosistemaResponsabilità
Class loaderTrova, carica, collega e inizializza i file .class nel runtime
Aree dati di runtimeLa memoria gestita dalla JVM: heap, stack, area metodi, registri PC
Motore di esecuzioneInterpreta e compila JIT il bytecode, ed esegue il garbage collector

Il class loader porta i tipi dentro, le aree dati di runtime mantengono lo stato, e il motore di esecuzione esegue il codice. Una chiamata a un metodo tocca tutti e tre: la sua classe viene caricata, il suo frame viene inserito in uno stack, e il suo bytecode viene eseguito.

Il sottosistema class loader

Le classi non vengono tutte caricate all'avvio. La JVM carica una classe in modo lazy, la prima volta che viene referenziata, in tre fasi: loading (lettura dei byte), linking (verifica del bytecode, preparazione dei campi statici, risoluzione dei riferimenti), e initialization (esecuzione degli inizializzatori statici e dei blocchi static { }).

I loader formano una gerarchia parent-first. Quando viene richiesta una classe, un loader delega al genitore prima di provare da sé — così un tipo core come String proviene sempre dal bootstrap loader fidato e non può mai essere sostituito dal codice applicativo.

// Walk the loader chain of any class
ClassLoader loader = MyType.class.getClassLoader();
while (loader != null) {
  System.out.println(loader.getName());
  loader = loader.getParent();
}
// A null result means the bootstrap loader (native, no Java object).
System.out.println(String.class.getClassLoader()); // prints: null

I tre loader standard, dal figlio al genitore, sono il loader application (classpath), il loader platform (moduli JDK come java.sql), e il loader bootstrap (core java.base, implementato in codice nativo, rappresentato come null). Il capitolo sul class loading copre questo ciclo di vita e il modello di delega in dettaglio; i moduli spiegano come java.base e gli altri sono confezionati.

Aree dati di runtime

Una volta che una classe è caricata, il suo codice e i suoi dati risiedono in regioni che la JVM suddivide per scopi distinti:

  • Heap — condiviso tra tutti i thread; ogni oggetto e array vive qui. Questo è ciò che gestisce il garbage collector.
  • Stack JVM — uno per thread. Ogni chiamata a un metodo inserisce un frame contenente variabili locali e operandi; il frame viene rimosso al ritorno. La ricorsione profonda lo fa traboccare (StackOverflowError).
  • Area metodi (Metaspace nelle JVM moderne) — metadati delle classi, il runtime constant pool e i campi statici. Memoria non-heap.
  • Registro PC — per thread; l'indirizzo dell'istruzione bytecode attualmente in esecuzione.
// Heap allocation: 'new' carves space out of the heap
byte[] buffer = new byte[1024]; // lives on the heap, GC-managed

// Stack growth: each call adds a frame to this thread's stack
static long factorial(long n) {
  return n <= 1 ? 1 : n * factorial(n - 1); // each call = one more frame
}

Heap versus non-heap è la divisione chiave: le istanze degli oggetti sono sull'heap; le strutture delle classi e la macchina stessa non lo sono. Il capitolo stack vs. heap mette a confronto queste due regioni in dettaglio.

Il motore di esecuzione

Il motore di esecuzione trasforma il bytecode in azione. Le moderne JVM HotSpot sono adattive: iniziano interpretando il bytecode (avvio rapido), profilano quali metodi sono frequentemente eseguiti, poi li passano al compilatore Just-In-Time (JIT), che emette codice macchina nativo ottimizzato. Il codice freddo rimane interpretato; il codice caldo viene compilato — il costo dell'ottimizzazione viene pagato solo dove si ripaga.

Il motore ospita anche il garbage collector, che recupera gli oggetti heap non più raggiungibili da nessun riferimento attivo, e la Java Native Interface (JNI), il ponte verso le librerie scritte in C/C++.

// A hot loop: the JIT will compile sum() to native code after enough calls
long total = 0;
for (int i = 0; i < 100_000_000; i++) {
  total += i; // interpreted at first, then JIT-compiled, then much faster
}

Un esempio pratico: esaminare la JVM in esecuzione

Questo programma non configura nulla — chiede alla JVM in esecuzione di descrivere sé stessa tramite i bean java.lang.management e l'API del class loader. Identifica la VM e il motore di esecuzione, percorre la gerarchia del loader, riporta la memoria heap e non-heap, e aumenta lo stack con la ricorsione.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Il RuntimeMXBean identifica il motore di esecuzione — una VM della famiglia HotSpot (la riga vm name mostra qualcosa come 'OpenJDK 64-Bit Server VM') — confermando che il motore "Server" capace di JIT è ciò che esegue il tuo bytecode, non un semplice interprete.
  • La catena del loader stampa qualcosa come <unnamed> -> app -> platform -> bootstrap(null): ogni iterazione del ciclo è salita al genitore, e la catena si è terminata con un genitore null — il bootstrap loader, che è codice nativo senza oggetto Java. La gerarchia è reale e osservabile, non una metafora.
  • String.class.getClassLoader() è null — i tipi core di java.base provengono da quel bootstrap loader in cima alla catena, che è esattamente il motivo per cui il codice applicativo non può mai sostituire il proprio String. La delega parent-first sta facendo il suo lavoro.
  • Le righe di memoria mostrano l'heap usato in KB ma un heap massimo in MB, e una figura separata per il non-heap: le istanze degli oggetti vivono sull'heap gestito dal GC, mentre i metadati delle classi (Metaspace) sono contati come non-heap — le due regioni sono tracciate indipendentemente.
  • depth(1) restituisce 5 perché ogni chiamata ricorsiva ha inserito il proprio frame nello stack JVM di questo thread e lo ha rimosso al ritorno; lo stack è per-thread e strutturato a frame, motivo per cui la ricorsione incontrollata termina con StackOverflowError anziché corrompere l'heap.

Esercizio

Pratica
Un programma Java referenzia la classe 'java.lang.String'. Nella gerarchia standard di class loader parent-first, quale loader la fornisce alla fine, e come appare quel loader dal codice Java?
Un programma Java referenzia la classe 'java.lang.String'. Nella gerarchia standard di class loader parent-first, quale loader la fornisce alla fine, e come appare quel loader dal codice Java?
Was this page helpful?