Java JVM Architecture
How the JVM is structured — class loader, runtime data areas, execution engine, and native interface.
Java JVM Architecture
The Java Virtual Machine is the program that runs your program. You compile .java source to platform-neutral .class bytecode, and the JVM is what loads that bytecode, verifies it, lays out its objects in memory, and executes it on real hardware. "Write once, run anywhere" is really "compile once, and let each platform's JVM do the rest." This chapter maps the JVM's internal structure — the three subsystems every implementation shares — so the memory and garbage-collection chapters that follow have a frame to hang on.
Three subsystems
A JVM, whatever the vendor, is organized into three cooperating subsystems. Everything else is detail inside one of them.
| Subsystem | Responsibility |
|---|---|
| Class loader | Finds, loads, links, and initializes .class files into the runtime |
| Runtime data areas | The memory the JVM manages: heap, stacks, method area, PC registers |
| Execution engine | Interprets and JIT-compiles bytecode, and runs the garbage collector |
The class loader brings types in, the runtime data areas hold the state, and the execution engine runs the code. A method call touches all three: its class is loaded, its frame is pushed on a stack, and its bytecode is executed.
The class loader subsystem
Classes are not all loaded at startup. The JVM loads a class lazily, the first time it is referenced, in three phases: loading (read the bytes), linking (verify the bytecode, prepare static fields, resolve references), and initialization (run static initializers and static { } blocks).
Loaders form a parent-first hierarchy. When asked for a class, a loader delegates up to its parent before trying itself — so a core type like String always comes from the trusted bootstrap loader and can never be shadowed by application code.
// 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: nullThe three standard loaders, from child to parent, are the application (classpath) loader, the platform loader (JDK modules like java.sql), and the bootstrap loader (core java.base, implemented in native code, represented as null).
Runtime data areas
Once a class is loaded, its code and data live in regions the JVM partitions for distinct purposes:
- Heap — shared across all threads; every object and array lives here. This is what the garbage collector manages.
- JVM stacks — one per thread. Each method call pushes a frame holding local variables and operands; the frame pops on return. Deep recursion overflows it (
StackOverflowError). - Method area (Metaspace in modern JVMs) — class metadata, the runtime constant pool, and static fields. Non-heap memory.
- PC register — per thread; the address of the bytecode instruction currently executing.
// 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 is the key split: object instances are on the heap; class structures and the machinery itself are not.
The execution engine
The execution engine turns bytecode into action. Modern HotSpot JVMs are adaptive: they start by interpreting bytecode (fast startup), profile which methods run hot, then hand those to the Just-In-Time (JIT) compiler, which emits optimized native machine code. Cold code stays interpreted; hot code gets compiled — you pay optimization cost only where it repays itself.
The engine also houses the garbage collector, which reclaims heap objects no longer reachable from any live reference, and the Java Native Interface (JNI), the bridge to libraries written 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
}A worked example: inspecting the live JVM
This program does not configure anything — it asks the running JVM to describe itself through the java.lang.management beans and the class-loader API. It names the VM and execution engine, walks the loader hierarchy, reports heap vs. non-heap memory, and grows the stack with recursion.
What to take from the run:
- The
RuntimeMXBeannames the execution engine — a HotSpot-family VM (thevm nameline shows something like 'OpenJDK 64-Bit Server VM') — confirming the JIT-capable "Server" engine is what is running your bytecode, not a bare interpreter. - The loader chain prints something like
<unnamed> -> app -> platform -> bootstrap(null): each loop step climbed to the parent, and the chain terminated at anullparent — the bootstrap loader, which is native code with no Java object. The hierarchy is real and observable, not a metaphor. String.class.getClassLoader()isnull— corejava.basetypes come from that same bootstrap loader at the top of the chain, which is exactly why application code can never substitute its ownString. Parent-first delegation is doing its job.- The memory lines show heap used in KB but a heap max in MB, and a separate non-heap figure: object instances live on the GC-managed heap, while class metadata (Metaspace) is counted as non-heap — the two regions are tracked independently.
depth(1)returns5because each recursive call pushed its own frame onto this thread's JVM stack and popped it on return; the stack is per-thread and frame-structured, which is why runaway recursion ends inStackOverflowErrorrather than corrupting the heap.
Practice
A Java program references the class 'java.lang.String'. In the standard parent-first class loader hierarchy, which loader ultimately provides it, and how does that loader appear from Java code?