JVM Architecture Overview

Naveen AnamaneniUpdated · 8 min read

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

The JVM: class loader subsystem, runtime data areas and execution engine
PartJobRailway station version
Class loader subsystemFinds, loads, checks and prepares classesTrains arriving and being checked in
Runtime data areasMemory for classes, objects and method callsPlatforms, waiting rooms and lockers
Execution engineRuns the bytecode and frees unused memoryThe 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 loaderLoadsExample
BootstrapThe core classes of Javajava.lang.String, java.util.ArrayList
PlatformThe rest of the Java SE platformjava.sql.Date
Application (also called system)Your classes, from the classpathBill, Main

Each loaded class then goes through three steps:

  1. Loading: read the .class file into memory.
  2. Linking: verify the bytecode is safe, prepare memory for static fields with default values, and resolve names to real references.
  3. Initialization: run static field assignments and static blocks, once, the first time the class is used.

You can watch lazy loading and initialization happen:

LazyLoad.java▶ Try it
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");
    }
}
Output
1. Program started
   Config is loaded and initialized
2. City: Pune
3. City again: Pune

How it works

  1. Line 3 runs first. Config hasn't been touched, so it isn't initialized yet.
  2. Line 4 reads Config.city. That's the first use of Config, so the JVM loads and initializes it: line 10 sets city, and the static block on lines 12 to 14 prints its message, before line 4 can finish.
  3. Line 5 uses Config again, 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.

AreaShared or per threadHolds
HeapSharedAll objects and arrays created with new. Cleaned by the garbage collector
Method area (Metaspace in HotSpot)SharedClass information: method code, field names, static variables, constant pools
JVM stackPer threadOne frame per method call, holding that call's local variables and partial results
PC registerPer threadThe address of the bytecode instruction this thread is running now
Native method stackPer threadCalls into native code written in C or C++

In int total = 0; String city = new String("Pune"); inside main:

  • total and the reference city live in main's frame, on the stack.
  • The String object itself lives on the heap.

3. Execution Engine

The execution engine turns bytecode into real work:

ComponentJob
InterpreterReads and runs bytecode one instruction at a time; starts immediately
JIT compilerCompiles frequently used methods to fast machine code while the program runs
Garbage collectorFinds 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:

Depth.java▶ Try it
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");
        }
    }
}
Output
Stack full after 44433 calls

Each 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

  1. Easy: For each item, say which memory area holds it: a local int in main, an object created with new, a static field, the code of a method.
  2. Harder: Add a second class Prices with a static block to LazyLoad, and use it only inside an if that is never true. Does its message print? Explain why.

Quick check: JVM architecture

4 questions
1Which class loader loads java.lang.String?
2Where are objects created with new stored?
3When is a class's static block run?
4Which memory area does each thread get its own copy of?

Key Takeaways

Was this tutorial helpful?

Lesson 24 of 482