The JVM is the one thing every Java developer uses constantly and understands the least. You know it "runs your bytecode," but if someone asked you to actually walk through what happens between java MyApp and your program executing, most people go quiet after "well, it loads the class." Here's the whole thing, one piece at a time — and if you want to go even deeper than this article after reading, the official Java Virtual Machine Specification is the actual source all of this is based on.
1. Class Loader Subsystem — getting your code into memory
This is the part responsible for taking your .class files and loading them into memory. It happens in three steps, always in this order.
Loading — reads the .class file and creates a corresponding Class object in memory. This is handled by one of three loaders, each responsible for a different source:
-
Bootstrap Class Loader — loads core Java classes (
java.lang.*, etc.) from the JDK itself -
Platform Class Loader (formerly called the Extension Class Loader) — loads classes from platform/extension libraries like
java.sql.* - Application (System) Class Loader — loads your own classes, from your classpath
Linking — happens in three sub-steps:
- Verification — checks the bytecode is valid and safe, and follows JVM rules. This is a real security boundary, not just a formality — it's what stops malformed or tampered bytecode from running.
- Preparation — allocates memory for static variables and sets them to default values (0, false, null)
- Resolution — converts symbolic references (like a class or method name) into actual, direct references
Initialization — runs static initializer blocks and assigns the real values to static variables. This only happens once per class, and only right before the class is actually used for the first time.
2. Runtime Data Areas — where everything actually lives in memory
These are created while your program runs, and they split into two categories: shared across all threads, or private to a single thread.
Shared by all threads:
- Method Area — stores class structure, method data, the runtime constant pool, and bytecode itself. In Java 7 and earlier this lived in a space called PermGen; since Java 8, it's called Metaspace instead.
-
Heap — where every object and array created with
newactually lives. This is what the Garbage Collector manages automatically. You can control its size with the-Xms(initial size) and-Xmx(max size) flags.
Private to each thread:
- Java Stack — every thread gets its own. Each method call pushes a new "stack frame" containing local variables, an operand stack, and frame data (like the return address). Created when a thread starts, destroyed when it ends.
- PC Register — tracks the address of the instruction the thread is currently executing. Every thread needs its own, since each one is at a different point in the program.
- Native Method Stack — used specifically when the JVM needs to manage calls into native (C/C++) code.
3. Execution Engine — actually running the bytecode
This is what turns loaded bytecode into your program actually doing something.
- Interpreter — reads and executes bytecode line by line. Simple, but slow, since it re-interprets the same instructions every time they run.
- JIT Compiler (Just-In-Time) — watches for bytecode that runs frequently, and compiles that into native machine code instead, so it runs at near-native speed from then on.
- Garbage Collector — automatically finds objects in the Heap and Method Area that are no longer reachable, and reclaims that memory, so you don't have to manually free it yourself. If you ever need to actually tune this in production, Oracle's official Garbage Collection Tuning Guide covers the different collectors (G1, Parallel, ZGC) and when to reach for each one.
The interpreter and JIT compiler work together, not separately: everything starts out interpreted, and the "hot" code — the parts actually running a lot — gets bumped up to compiled native code over time.
4. JNI (Java Native Interface) — the bridge to C/C++
JNI is what lets JVM code call out to native code, and vice versa. This matters for system-level work that pure Java can't do on its own — low-level I/O, hardware access, or reusing an existing native library instead of rewriting it in Java.
5. Native Method Libraries
These are the actual native (C/C++) libraries that JNI calls into — things like file handling, network communication, hardware access, or compression libraries that need to operate below the level Java itself normally works at.
The complete flow, start to finish
MyApp.java
→ javac (compiler)
→ MyApp.class (bytecode)
→ Class Loader Subsystem (Loading → Linking → Initialization)
→ Runtime Data Areas (memory allocated)
→ Execution Engine (Interpreter / JIT + Garbage Collector)
→ JNI (if native code is involved)
→ Native Libraries (if needed)
→ Output
Class loader hierarchy: parent delegation
The three class loaders aren't independent — they're arranged in a hierarchy, and they follow something called the parent delegation model:
Bootstrap Class Loader
↑ (parent)
Platform Class Loader
↑ (parent)
Application Class Loader
When a class needs loading, the Application loader doesn't just load it directly — it first asks its parent (Platform), which asks its parent (Bootstrap). Only if none of the parents can find the class does the request fall back down the chain. This is why you can't accidentally override a core class like java.lang.String just by defining your own class with the same name — the Bootstrap loader already owns that name, and it's asked first.
Quick answers, if this comes up in an interview
- "What are the three main JVM subsystems?" → "Class Loader Subsystem, Runtime Data Areas, and the Execution Engine."
-
"What's the difference between the Method Area and the Heap?" → "The Method Area stores class-level data — structure, bytecode, the constant pool. The Heap stores actual object instances created with
new. Both are shared across all threads." - "Why does each thread get its own stack, but not its own heap?" → "Objects need to be shared between threads, so the Heap is shared. But each thread executes independently, so it needs its own call stack to track its own method calls."
-
"What replaced PermGen, and why?" → "Metaspace, starting in Java 8. Unlike PermGen, Metaspace isn't part of the JVM's fixed heap — it uses native memory and can grow dynamically, which avoids the classic
OutOfMemoryError: PermGen spaceproblem." - "What is the parent delegation model?" → "Before a class loader loads a class itself, it asks its parent loader first. This prevents core Java classes from being silently overridden by user-defined classes with the same name."
The short version
The JVM isn't one thing — it's three subsystems working together: the Class Loader gets your code into memory (loading, linking, initialization), the Runtime Data Areas hold everything while it runs (shared Method Area and Heap, plus a private Stack, PC Register, and Native Stack per thread), and the Execution Engine actually runs it (interpreter and JIT compiler, with garbage collection running alongside). JNI and native libraries exist for the moments Java code needs to reach outside the JVM entirely. Understanding this flow is what separates "the JVM runs my code" from actually knowing what your program is doing at runtime.