JVM Architecture Overview
A railway station has three kinds of work going on at once. Trains arrive and are checked onto a platform. Passengers and luggage occupy space on platforms, in waiting rooms and in lockers. And the operations team keeps everything moving and clears the platforms when trains leave. The JVM is organized the same way: a part that brings classes in, memory areas where data lives, and an engine that runs the code and cleans up.
The Big Picture
| Part | Job | Railway station version |
|---|---|---|
| Class loader subsystem | Finds, loads, checks and prepares classes | Trains arriving and being checked in |
| Runtime data areas | Memory for classes, objects and method calls | Platforms, waiting rooms and lockers |
| Execution engine | Runs the bytecode and frees unused memory | The operations team |
1. Class Loader Subsystem
Classes are loaded lazily: only when the program first needs them. Three built-in class loaders share the work:
| Class loader | Loads | Example |
|---|---|---|
| Bootstrap | The core classes of Java | java.lang.String, java.util.ArrayList |
| Platform | The rest of the Java SE platform | java.sql.Date |
| Application (also called system) | Your classes, from the classpath | Bill, Main |
Each loaded class then goes through three steps:
- Loading: read the
.classfile into memory. - Linking: verify the bytecode is safe, prepare memory for
staticfields with default values, and resolve names to real references. - Initialization: run
staticfield assignments andstaticblocks, once, the first time the class is used.
You can watch lazy loading and initialization happen:
public class LazyLoad {
public static void main(String[] args) {
System.out.println("1. Program started");
System.out.println("2. City: " + Config.city);
System.out.println("3. City again: " + Config.city);
}
}
class Config {
static String city = "Pune";
static {
System.out.println(" Config is loaded and initialized");
}
}1. Program started
Config is loaded and initialized
2. City: Pune
3. City again: PuneHow it works
- Line 3 runs first.
Confighasn't been touched, so it isn't initialized yet. - Line 4 reads
Config.city. That's the first use ofConfig, so the JVM loads and initializes it: line 10 setscity, and thestaticblock on lines 12 to 14 prints its message, before line 4 can finish. - Line 5 uses
Configagain, but initialization happens only once, so nothing extra prints.
Try it yourself: delete lines 4 and 5. Does the message from Config still appear?
2. Runtime Data Areas
The JVM divides its memory into areas. Some are shared by every thread; others belong to one thread.
| Area | Shared or per thread | Holds |
|---|---|---|
| Heap | Shared | All objects and arrays created with new. Cleaned by the garbage collector |
| Method area (Metaspace in HotSpot) | Shared | Class information: method code, field names, static variables, constant pools |
| JVM stack | Per thread | One frame per method call, holding that call's local variables and partial results |
| PC register | Per thread | The address of the bytecode instruction this thread is running now |
| Native method stack | Per thread | Calls into native code written in C or C++ |
In int total = 0; String city = new String("Pune"); inside main:
totaland the referencecitylive inmain's frame, on the stack.- The
Stringobject itself lives on the heap.
3. Execution Engine
The execution engine turns bytecode into real work:
| Component | Job |
|---|---|
| Interpreter | Reads and runs bytecode one instruction at a time; starts immediately |
| JIT compiler | Compiles frequently used methods to fast machine code while the program runs |
| Garbage collector | Finds heap objects that nothing refers to any more and frees their memory |
Next to it, JNI (Java Native Interface) and its newer replacement, the Foreign Function and Memory API, let Java call code written in C or C++ when needed. The lesson "JIT Compiler" covers the JIT in more detail, and a later lesson covers garbage collection.
Common Mistakes
Saying "primitives live on the stack, objects on the heap" and stopping there. Local variables live on the stack, but a field inside an object, even an int, lives on the heap with its object. It's about where the variable is declared, not its type.
Thinking static variables are on the stack. They belong to the class, so they're stored with the class data, not in any method's frame.
Thinking all classes load when the program starts. Classes load on first use, which is why a mistake in a rarely used class might only show up much later.
Under the Hood
You can see the stack limit for yourself. This program calls a method inside itself until the stack is full, and counts the calls:
public class Depth {
static int depth = 0;
static void dive() {
depth++;
dive();
}
public static void main(String[] args) {
try {
dive();
} catch (StackOverflowError e) {
System.out.println("Stack full after " + depth + " calls");
}
}
}Stack full after 44433 callsEach call to dive adds a frame to the stack. When there's no room for the next frame, the JVM throws StackOverflowError. Run it with a smaller stack, java -Xss256k Depth.java, and the number drops sharply (about 1,500 on the computer used for this lesson).
Interview Corner
Explain the JVM architecture.
The JVM has three main parts. The class loader subsystem loads classes through the bootstrap, platform and application class loaders, then links (verify, prepare, resolve) and initializes them. The runtime data areas are the heap and method area, shared by all threads, plus a JVM stack, PC register and native method stack for each thread. The execution engine runs bytecode with the interpreter and JIT compiler, and the garbage collector frees unused heap memory.
What is the difference between the heap and the stack in Java?
The heap is shared by all threads and stores objects and arrays; the garbage collector manages it. Each thread has its own stack, which stores one frame per method call with that call's local variables. Running out of stack gives StackOverflowError; running out of heap gives OutOfMemoryError.
What is parent delegation in class loading?
Before loading a class itself, a class loader asks its parent loader first, up to the bootstrap loader. Only if the parents can't find the class does the child load it. This keeps core classes such as java.lang.String from being replaced by application code.
Practice
- Easy: For each item, say which memory area holds it: a local
intinmain, an object created withnew, astaticfield, the code of a method. - Harder: Add a second class
Priceswith astaticblock toLazyLoad, and use it only inside anifthat is never true. Does its message print? Explain why.
Key Takeaways
Was this tutorial helpful?