JavaJVM & Memoryadvanced
Updated:

JVM Architecture: Class Loader, Runtime Areas and Execution Engine

6 min read

Follow a .class file through the JVM: how the class loader subsystem, runtime data areas and the JIT-powered execution engine turn bytecode into running machine code.

TL;DR – Quick Answer

The JVM has three main subsystems. The class loader subsystem loads, links and initializes .class files on demand. The runtime data areas hold the program's state: heap, per-thread stacks, metaspace, program counters. The execution engine runs the bytecode, starting in the interpreter and promoting hot methods to native code via the JIT compiler, with the garbage collector managing heap memory alongside.

On This Page

"Write once, run anywhere" is not magic — it is a specific machine design. Your .java file compiles to bytecode, and the JVM turns that bytecode into running native code on whatever hardware it lands on. Understanding how it does that is what separates "I know Java syntax" from "I know Java" in senior interviews.

The architecture has three moving parts: the class loader subsystem brings code in, the runtime data areas hold state, and the execution engine runs it. This article walks a class through all three. (If you sometimes mix up the JVM with the JRE and JDK, square that away first with JDK vs JRE vs JVM.)

The map: three subsystems

 .class files
      |
      v
+--------------------------+
|  CLASS LOADER SUBSYSTEM  |  loading -> linking -> initialization
+--------------------------+
      |
      v
+--------------------------+
|   RUNTIME DATA AREAS     |  heap | metaspace | per-thread:
|                          |  stack, PC register, native stack
+--------------------------+
      ^
      |
+--------------------------+
|    EXECUTION ENGINE      |  interpreter | JIT compilers | GC
+--------------------------+
      |
      v
   JNI  -->  native libraries (.dll / .so)

Everything below is a guided tour of this diagram, top to bottom.

Class loader subsystem: loading, linking, initialization

Classes are loaded lazily — the JVM touches a .class file only when the running code first needs it. Getting a class ready happens in three phases.

1. Loading. A class loader reads the binary .class data and creates a Class object in metaspace. Three built-in loaders form a hierarchy:

Loader Loads Example
Bootstrap Core JDK modules (java.base) java.lang.String
Platform Other JDK modules java.sql.Driver
Application (system) Your classpath / module path your own classes

2. Linking. Three sub-steps: verification (is this bytecode structurally valid and safe?), preparation (allocate static fields, set defaults — 0, false, null), and resolution (replace symbolic constant-pool references with direct ones, often lazily).

3. Initialization. Static initializers and static field assignments finally run, exactly once, on first active use of the class.

You can see the hierarchy from any program:

public class LoaderDemo {
    public static void main(String[] args) {
        // Bootstrap loader is native code — it prints as null
        System.out.println(String.class.getClassLoader());

        // Platform loader handles non-core JDK modules like java.sql
        System.out.println(java.sql.Driver.class.getClassLoader());

        // Your classes come from the application loader
        System.out.println(LoaderDemo.class.getClassLoader());

        // And the parent chain: app -> platform -> null (bootstrap)
        ClassLoader app = LoaderDemo.class.getClassLoader();
        System.out.println(app.getParent());
        System.out.println(app.getParent().getParent());
    }
}

What to notice: String.class.getClassLoader() returns null because the bootstrap loader lives inside the JVM itself and has no Java object. The parent chain printed at the end is the backbone of the delegation model.

Parent-first delegation

When a loader is asked for a class, it does not load it immediately — it delegates to its parent first. The request climbs to bootstrap; only when no ancestor can find the class does the original loader try. Two consequences matter:

  • Security. You cannot replace java.lang.String by putting your own String.class on the classpath — bootstrap always wins.
  • Uniqueness. A class is identified by its name plus its loader. Two loaders can each load a class of the same name, and the JVM treats them as different types — the root cause of the occasional baffling ClassCastException: X cannot be cast to X in app servers and plugin systems.

Interview note: "Explain the classloader delegation model" is a fixture in interviews at the 2–3 year level. Name the three loaders, state parent-first delegation in one sentence, and give the security consequence. Mentioning that frameworks like Tomcat deliberately break parent-first for webapp isolation marks you as someone who has seen real systems.

Runtime data areas: the state

Once loaded, a program needs memory for its state. The JVM divides it by lifetime and ownership:

  • Heap — all objects and arrays; shared by every thread; managed by GC.
  • Metaspace — class metadata, method bytecode, runtime constant pools; native memory.
  • JVM stack (one per thread) — a frame per method call: local variables, operand stack.
  • PC register (one per thread) — the address of the current bytecode instruction.
  • Native method stacks (one per thread) — frames for JNI calls.

Each frame's operand stack is where bytecode actually computes: iadd pops two ints and pushes their sum. The JVM is a stack machine — no general-purpose registers in the abstract model, which keeps bytecode compact and portable.

We keep this section short deliberately: the regions, their failure modes (OutOfMemoryError, StackOverflowError) and their tuning flags get a full treatment in Java memory management.

Execution engine: from bytecode to native code

Here is the part most explanations get wrong: the JVM is neither "just an interpreter" nor "a compiler." It is both, in sequence.

The interpreter starts executing bytecode immediately — no compile wait, great startup. But interpreting every instruction repeatedly is slow.

The JIT (just-in-time) compilers fix that. The JVM counts method invocations and loop iterations; when a method gets hot, it is compiled to optimized native machine code. HotSpot uses tiered compilation: C1 compiles quickly with light optimization; C2 recompiles the truly hot paths aggressively — inlining, loop unrolling, escape analysis, devirtualization — using profile data the earlier tiers gathered.

You can watch warm-up happen:

public class JitWarmup {

    static long work() {
        long sum = 0;
        for (int i = 0; i < 20_000_000; i++) {
            sum += i % 7;
        }
        return sum;
    }

    public static void main(String[] args) {
        for (int round = 1; round <= 5; round++) {
            long start = System.nanoTime();
            long sum = work();
            long ms = (System.nanoTime() - start) / 1_000_000;
            System.out.println("Round " + round + ": " + ms + " ms (sum " + sum + ")");
        }
    }
}

What to notice: the first round is typically the slowest (interpreter plus C1), and later rounds get faster as C2 replaces work() with optimized native code. Run it with -XX:+PrintCompilation to see the compilation events interleaved with your output. This is also why naive one-shot benchmarks of Java code are meaningless — they measure the interpreter, not your code.

The garbage collector is the third resident of the execution engine, reclaiming heap memory concurrently with your code — its algorithms deserve their own article: how GC really works.

Common mistake: A common mistake beginners make is saying "Java is slow because it's interpreted." Long-running Java is JIT-compiled to native code specialized with runtime profile data — sometimes enabling optimizations an ahead-of-time compiler cannot prove safe. The honest cost of the JVM is startup time and memory footprint, not steady-state speed.

JNI and native libraries

The Java Native Interface is the escape hatch to C and C++: methods declared native are implemented in shared libraries (.so, .dll) that the JVM loads at runtime. The JDK itself uses JNI for file I/O, sockets and graphics. Application code touches it rarely — each JNI crossing has overhead and gives up memory safety — and modern Java is replacing most hand-written JNI with the Foreign Function & Memory API.

Life of a class: the whole pipeline in one story

Tie it together with what happens when you run java Main:

  1. The launcher starts a JVM process; bootstrap and platform loaders wire up the core modules.
  2. The application loader loads Main, verifying, preparing and resolving it.
  3. Static initializers of Main run; main()'s frame is pushed on the main thread's stack.
  4. The interpreter starts executing bytecode; objects go on the heap, class metadata sits in metaspace.
  5. Hot methods get JIT-compiled tier by tier; GC collects the young generation as garbage accumulates.
  6. main() returns, non-daemon threads finish, and the JVM exits.

Every JVM interview question — from "what is Java bytecode" to "why did my app pause" — is asking about one step of this pipeline.

Pro tip: Three flags turn the architecture from theory into something you can observe on any program: -verbose:class shows every class load as it happens, -XX:+PrintCompilation shows JIT decisions, and -Xlog:gc shows collections. Spend twenty minutes running your own code with all three and this whole article becomes concrete.

Preparing this topic for interviews

Structure beats detail here. Lead with the three subsystems in one sentence each, then let the interviewer choose the branch to descend — class loading (delegation, linking phases), memory (regions and their errors) or execution (interpreter → tiered JIT). Candidates who volunteer the map first, then zoom in on request, consistently read as more senior than those who dump every fact they know about class loaders unprompted.

Frequently Asked Questions

What are the main components of JVM architecture?
Three subsystems: the class loader subsystem, which loads, links and initializes classes; the runtime data areas, which include the heap, thread stacks, metaspace and program counter registers; and the execution engine, which contains the interpreter, the JIT compiler and the garbage collector, plus the native interface for calling C libraries.
What is the classloader delegation model?
Before loading a class itself, every class loader first asks its parent. The request climbs to the bootstrap loader, and only if no parent can supply the class does the child load it. This ensures core classes like java.lang.String always come from the JDK and cannot be spoofed by a class on your classpath.
Is the JVM an interpreter or a compiler?
Both. Bytecode starts out interpreted, which gives fast startup. The JVM profiles execution, and methods that run frequently are compiled to optimized native code by the JIT compilers. This tiered execution means long-running Java code can approach the speed of ahead-of-time compiled languages.
Why does getClassLoader() return null for String.class?
Classes loaded by the bootstrap loader report null because the bootstrap loader is implemented natively inside the JVM and has no Java object representing it. Seeing null for java.lang.String is expected behavior, not an error.
What is the difference between JVM, JRE and JDK?
The JVM is the virtual machine that executes bytecode. The JRE is the JVM plus the class libraries needed to run applications. The JDK is the JRE plus development tools such as javac, jcmd and the debugger. You develop with the JDK; your code runs on the JVM.
What happens during class linking?
Linking has three steps. Verification checks that the bytecode is structurally valid and safe. Preparation allocates memory for static fields and assigns default values like 0 and null. Resolution replaces symbolic references in the constant pool with direct references, and may be performed lazily.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 14 July 2026 LinkedIn
Chat with us