How the JVM Works: ClassLoading, Linking, and Runtime Internals
- Published on
- /1 mins read/...
Introduction
Java's primary promise has always been: **"Write Once, Run Anywhere"** (WORA). This cross-platform portability is made possible by the **Java Virtual Machine (JVM)**. Instead of compiling code directly into machine-specific assembly instructions, Java source code is compiled into platform-independent intermediate **bytecode**.
When you execute a Java program, the JVM loads this bytecode, links class references, manages memory regions dynamically, and compiles frequently executed paths into machine code at runtime. Let's look under the hood of the JVM architecture to see how it executes classes.
ClassLoader Parent Delegation Model
The JVM's ClassLoader subsystem brings compiled .class byte files into runtime memory. To prevent security vulnerabilities (like an application overriding core classes like java.lang.System), Java enforces the **Parent Delegation Model**.
When a request to load a class arrives, the current ClassLoader delegates the request up to its parent. Only if the parent cannot find the class does the child attempt to load it. The hierarchy consists of three standard ClassLoaders: Bootstrap ClassLoader (JDK classes), Platform ClassLoader (Extension APIs), and the Application ClassLoader (local classpath code).
ClassLoader Parent Delegation Simulator
Select a class type and trigger loading to observe the security delegation path
Dynamic Linking & Loading Phases
Once a ClassLoader locates class bytes, the JVM processes it through three distinct phases:
- Loading: The binary data of the class is ingested and a Class object is created in the Method Area.
- Linking: Combines class references.
- Verify: Inspects bytecode structural safety to prevent corrupted or malicious code.
- Prepare: Allocates memory for static fields and assigns initial default type values.
- Resolve: Converts symbolic names/strings in the constant pool into direct memory address references.
- Initialization: Executes static initializer blocks and assigns static fields their actual declared values.
JVM Memory Allocation Architecture
During execution, the JVM allocates memory in specific regions collectively called the **Runtime Data Areas**. These areas are divided into memory shared by all threads and memory isolated to individual threads.
Understanding where variables are allocated is crucial for managing garbage collection profiles and avoiding stack overflow or out-of-memory errors. Investigate the interactive inspector below to see how Java code declarations map to memory.
JVM Runtime Data Area Inspector
Click a memory region block below to observe allocation scope and Java code mapping
Heap Area
Stores the actual data objects allocated via new keyword. Managed by the Garbage Collector.
The actual new User(...) object values (string data "Alice" and integer 28) are stored here in Heap memory.
Interactive ClassLoader Simulator
(Use the delegation simulator above to load core, platform, or app classes and trace the parent lookup path.)
Interactive JVM Memory Inspector
(Select JVM memory blocks in the inspector above to explore scopes and see how Java variable references resolve between Stack and Heap.)
JIT Compilation & Garbage Collection
To run bytecode quickly, the JVM uses an **Execution Engine** containing both an Interpreter and a **Just-In-Time (JIT) Compiler**. The Interpreter runs bytecode instructions sequentially (high start speed, low run speed).
As the code runs, the JVM tracks "hot methods" that are executed frequently. The JIT Compiler compiles these hot paths directly into native machine code, bypassing interpreter loops for subsequent calls. Combined with a generational **Garbage Collector** (GC) that sweeps unused Heap allocations automatically, the JVM delivers compiled machine speeds with managed runtime memory safety.
Original system design topics compiled from the ByteByteGo JVM architecture series.
