skip to content
everything.jvm.jvmArchitecture

JVM architecture

class loaders, runtime data areas, execution engine. the whole map.

chapter
chapter 13.01
reading time
9 min read

the JVM is a C++ program with three jobs: load classes (the class loader subsystem), hold everything in memory (the runtime data areas: heap, metaspace, one stack per thread), and run bytecode (the execution engine: an interpreter, two JIT compilers and a garbage collector).

every chapter in this part zooms into one box of that map. this one is the map.

the picture

you know how everyone says "the JVM" like it's one thing that runs your code? it's more like a tiny operating system that happens to speak bytecode. it has a loader, a memory manager, a scheduler of sorts, two compilers and a cleaner, all inside one process.

think of a restaurant kitchen. deliveries arrive at the back door and get checked before anything goes on a shelf (the class loader). the recipe binder holds every dish the kitchen knows (metaspace). there's one shared pantry where all the ingredients live (the heap). every cook has their own notepad and a finger on the line of the recipe they're on (a stack and a pc register per thread). a new cook reads each recipe line by line (the interpreter). a cook who has made the same dish ten thousand times does it from muscle memory (JIT-compiled machine code). and someone keeps clearing plates nobody is eating from (the GC).

class loader subsystem
runtime data areas
shared by all threads
one set per thread (18 in an idle JVM)
execution engine
native interface

shared by every thread. new allocates here, the GC cleans up here. one big region, split into young and old by the collector.

measured · idle on a 24 GB mac: 6 GB reserved (a quarter of RAM), 388 MB committed.

click any block. the measured numbers come from a real JVM: jdk 25 on an M4 Pro mac with 24 GB, running a tiny program, read off jcmd, -Xlog:class+load and native memory tracking.

the boxes the spec actually requires are the class loading pipeline, the heap, a method area, per-thread stacks and pc registers, and something that executes bytecode. everything about how (which GC, whether there's a JIT at all, how an object is laid out in memory) is left to the implementation. on this site that implementation is HotSpot, the JVM inside every OpenJDK build (Temurin, Oracle, Corretto, Zulu, all of them).

follow java Hello through it

here's the smallest program that touches most of the map:

Hello.java
public class Hello {
    public static void main(String[] args) {
        System.out.println("hello");
    }
}

and here's all of main after javac, as javap -c prints it. four instructions:

javap -c Hello
public static void main(java.lang.String[]);
  Code:
     0: getstatic     #7    // Field java/lang/System.out:Ljava/io/PrintStream;
     3: ldc           #13   // String hello
     5: invokevirtual #15   // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     8: return

press step and watch which parts of the JVM each moment touches.

you type java Hello. the launcher, a small C program, loads libjvm and asks it to create a VM.

step 1/12
a lit block is the one doing the work at that moment. notice how much happens before the first instruction of main runs (steps 1 to 5), and that the GC never wakes up.

the numbers are real: -Xlog:class+load printed 415 lines for that run, and Hello was line 402. everything before it was the JDK booting itself, almost all of it mapped straight out of the CDS archive (a pre-parsed snapshot of JDK classes that ships with the JDK, more in AOT and Leyden).

java -Xlog:class+load Hello
[0.008s][info][class,load] java.lang.Object source: shared objects file
[0.008s][info][class,load] java.io.Serializable source: shared objects file
[0.008s][info][class,load] java.lang.Comparable source: shared objects file
...
[0.021s][info][class,load] Hello source: file:/.../jvm-lab/arch/
...
[0.022s][info][class,load] java.lang.Shutdown$Lock source: shared objects file

what's actually inside the process

your program is one thread. the JVM brings its own crew. this is jcmd <pid> Thread.print against a program whose main just sleeps:

  • 18 threads
  • ├── have a java.lang.Thread object
  • │ ├── mainyour code
  • │ ├── Reference Handlerhands cleared references to their queues
  • │ ├── Finalizerruns finalize(), deprecated but still here
  • │ ├── Signal Dispatcherctrl-c, kill -3
  • │ ├── Common-Cleanerjava.lang.ref.Cleaner actions
  • │ ├── Notification ThreadJMX notifications
  • │ ├── Service Threadhousekeeping, hidden from java
  • │ ├── Monitor Deflation Threadshrinks idle lock monitors, hidden
  • │ ├── C2 CompilerThread0the optimizing JIT, hidden
  • │ ├── C1 CompilerThread0the fast JIT, hidden
  • │ └── Attach Listeneronly exists because jcmd attached
  • └── pure C++, no Thread object
  • ├── VM Threadruns stop-the-world operations
  • ├── VM Periodic Task Threadthe JVM's own timer
  • ├── GC Thread#0G1 pause work
  • ├── G1 Main Markerconcurrent marking
  • ├── G1 Conc#0
  • ├── G1 Refine#0remembered set refinement
  • └── G1 Service
18 threads, one of them yours. click the groups to fold them. the purple ones are the JIT and the GC, the machinery the rest of this part is about.

java can only see a handful of those. the compiler threads are real java threads, but HotSpot hides them from your code:

Main.java
void main() {
    Thread.getAllStackTraces().keySet().stream()
        .map(t -> t.getName() + (t.isDaemon() ? " (daemon)" : ""))
        .sorted()
        .forEach(IO::println);
}
output
Common-Cleaner (daemon)
Finalizer (daemon)
Notification Thread (daemon)
Reference Handler (daemon)
Signal Dispatcher (daemon)
main

and here's where that idle process's memory went, from -XX:NativeMemoryTracking=summary and jcmd <pid> VM.native_memory summary. this is committed memory (actually backed by RAM), not the much bigger reserved address space:

java heap388 MB
GC data structures62.9 MB
CDS archive (shared classes)13.6 MB
code cache7.5 MB
JVM internal1.2 MB
symbols1.1 MB
metaspace0.45 MB
thread stacks (touched)0.16 MB
log scale
committed memory of an idle JVM on jdk 25, 24 GB mac, default flags. the heap is the biggest slice, but it isn't the only one.
quick check
which of these does every thread get its own copy of?

the spec vs HotSpot

the JVM is two things: a specification (the JVMS, a long PDF) and implementations of it. the spec is deliberately vague about anything that would tie an implementation's hands.

the spec saysHotSpot does
a class file format, and what each bytecode meansfollows it exactly. that's the contract
a heap, reclaimed by "an automatic storage management system"G1 by default, or Parallel, Serial, ZGC, Shenandoah
a "method area" for per-class datametaspace, in native memory (until java 7 it was permgen, a slice of the heap)
one stack per thread, with framesframes live on the native OS thread stack, interleaved with C frames
execute bytecodeinterpret first, then compile hot methods with C1 and C2
nothing at all about object layouta 12-byte header today, 8 with compact headers (object layout)

other JVMs exist and make different choices: Eclipse OpenJ9 (its own GC and JIT, a smaller footprint), and GraalVM (HotSpot with the Graal compiler, plus native image, see AOT and Leyden). your bytecode runs on all of them, which is the whole point of the spec.

one level deeper:why there are two compilers+

compiling well is slow. compiling fast gives you mediocre code. and the most useful information for optimizing (which branch is taken, which class actually shows up at a call site) only exists once the code has run for a while.

so HotSpot does both. everything starts in the interpreter, which counts calls and loop iterations. warm methods go to C1, which compiles in milliseconds and keeps counting (now with a detailed profile). hot methods go to C2, which spends real time and uses that profile to make bets: "this call always hits ArrayList, so inline ArrayList.get and check the class with one compare". if a bet ever loses, the code is thrown away and the method drops back to the interpreter. that whole dance is the interpreter and the JIT.

keylines
  • the JVM loads classes, holds memory, and runs bytecode. three subsystems, one process.
  • heap, metaspace and code cache are shared. stack, pc register and native stack are per thread.
  • java Hello loads 415 classes before main runs. an idle JVM has 18 threads, and one of them is yours.
  • the spec fixes the class file and bytecode. HotSpot chooses the GC, the JIT and the object layout.
  • -Xmx limits the heap, not the process.