JVM Architecture in Java

In the last lesson you saw the Java compilation and execution process. Now let’s look beneath the surface. What actually runs your program is not the operating system directly. It is a program called the JVM, the Java Virtual Machine.

🤔 Why does Java need a virtual machine?

A normal compiled program is locked to one platform. Here is why that hurts:

  • Each operating system and chip speaks its own machine language (the low-level instructions a real processor runs).
  • Code built for one of them runs only on that one platform.
  • So you would compile, test, and debug separately for every OS and chip your users have.

Java fixes this with a middle step:

  • Compiling Java gives you bytecode, a universal instruction set no real processor runs directly.
  • The JVM on each machine reads that bytecode and runs it locally.
  • So the same compiled file runs anywhere a JVM exists. That is “write once, run anywhere”.

Picture bytecode as a recipe in a common language, and the JVM as a local cook who reads it and uses the local kitchen. The recipe never changes; only the kitchen does.

This is the journey of a Java file from source code to a running program.

Hello.java --(javac compiler)--> Hello.class (bytecode) --(JVM)--> runs anywhere

Compile a tiny program and look at what comes out.

// ✅ javac produces a .class file full of bytecode, not machine code
// javac Hello.java
// ls
// Hello.java Hello.class <-- this .class file is the portable bytecode

You can copy that .class file to Windows, Mac, or Linux. As long as each has a JVM, your program runs the same way.

🧩 What is the JVM?

The JVM (Java Virtual Machine) is the program that loads and runs Java bytecode. Why “virtual”:

  • It acts like a computer living inside your real computer.
  • It has its own way to load code, its own managed memory, and its own way to run instructions.
  • Every OS has its own JVM build, but they all accept the exact same bytecode. That shared agreement keeps your code portable.

People mix up JVM, JRE, and JDK. They are nested boxes:

  • JVM: the engine that runs bytecode.
  • JRE (Java Runtime Environment): JVM + standard libraries. Enough to run Java programs.
  • JDK (Java Development Kit): JRE + build tools like the javac compiler. Needed to write Java.

So JDK contains the JRE, and the JRE contains the JVM. Inside the JVM, three parts run your code:

  • Class loader: finds your classes and brings them into memory.
  • Runtime data areas: the memory the JVM uses while running.
  • Execution engine: actually runs the bytecode.

Here is how those three subsystems fit together inside the JVM.

Execution Engine

Interpreter

JIT Compiler

Garbage Collector

Runtime Data Areas (memory)

Method Area / Metaspace

Heap

Java Stacks

PC Registers

Native Method Stack

.class bytecode files

Class Loader Subsystem

Program runs on the host machine

📥 The class loader subsystem

The class loader finds .class files and loads them into the JVM when needed. Classes load on demand, the first time they are used, not all at once at startup. That keeps startup light and skips code you never touch.

It works in three steps:

  • Loading: reads the .class file and brings the bytecode into memory.
  • Linking: verifies the bytecode is valid and safe, prepares it, and resolves references to other classes.
  • Initialization: sets up static variables and runs static blocks.

The verification step is about safety:

  • Bytecode can come from anywhere, including files you did not write.
  • So before any code runs, the JVM checks instructions are well formed and types line up.
  • A broken or hostile .class would otherwise crash the JVM or corrupt memory. Verification refuses it at the door.

A later lesson covers how multiple class loaders cooperate.

🧠 The runtime data areas (memory)

The JVM splits its memory into several areas, each with one job. Knowing them lets you reason about memory errors and performance.

  • The heap stores every object you create with new. One heap per JVM, shared by all threads. A new Student() or new ArrayList<>() lives here.
  • The Java stacks store method calls and local variables. Each thread gets its own. A method call pushes a frame with its locals; the frame pops when the method returns.
  • The method area (Metaspace in modern Java) holds class-level info: method code, field and method details, and static variables. Shared by all threads.
  • The PC registers track which bytecode instruction each thread is running. One per thread, since each thread is at a different point.
  • The native method stack is used when Java calls into C or C++ through native interfaces. One per thread.

Group them by sharing:

  • Shared by all threads: heap and method area (Metaspace). Same objects and class data for everyone.
  • Private to each thread: Java stack, PC register, native method stack.

This connects to code you write. In Student alex = new Student();, the object lives on the heap while the alex reference and other locals live on the stack.

This is the line that shows the heap-and-stack split in action.

// ✅ the object data lives on the heap; the reference lives on the stack
Student alex = new Student(); // object -> heap, "alex" reference -> stack
int score = 90; // primitive local -> stack

Heap and stack are the big two

Of all the memory areas, the heap (objects) and the stack (method calls and locals) are the ones you will deal with most. They are the key to understanding memory and the errors OutOfMemoryError (heap is full of objects) and StackOverflowError (stack is full of method calls, often from runaway recursion). The next lesson covers them in depth.

⚙️ The execution engine

The execution engine runs the bytecode, turning each instruction into real action on the host machine. It has a few cooperating pieces:

  • Interpreter: runs bytecode one instruction at a time. Quick to start, but slow for code that runs many times.
  • JIT compiler (Just-In-Time): spots code that runs often (“hot” code) and compiles it to native machine code that runs at near-native speed.
  • Garbage collector: finds unused objects on the heap and reclaims their memory automatically.

Interpreter and JIT work together, and that is why Java feels the way it does:

  • The JVM interprets first, so your program starts right away.
  • It counts how often each method and loop runs.
  • Once code crosses a “hot” threshold, the JIT compiles it to native code in the background, and the fast version runs from then on.
  • So you get quick startup and strong long-running speed, without compiling everything up front or wasting effort on run-once code. The JVM decides what is hot; you manage none of it.

The garbage collector frees memory for you, so forgotten manual frees no longer cause leaks:

  • It periodically scans the heap for objects nothing refers to anymore.
  • It frees that memory for reuse.
  • It runs automatically, so you almost never call it directly.

This loop runs the same method enough times that the JIT compiler will compile it to native code partway through.

// ✅ the JVM interprets this at first, then the JIT compiles the hot loop
public class Demo {
static int square(int n) {
return n * n; // called millions of times -> becomes "hot"
}
public static void main(String[] args) {
long total = 0;
for (int i = 0; i < 50_000_000; i++) {
total += square(i); // early runs interpreted, later runs native
}
System.out.println(total);
}
}

The first iterations are interpreted. The JVM marks square as hot, the JIT compiles it, and later iterations run as native code, far faster. Any temporary objects become eligible for the garbage collector.

Output

1249999937500000000

⚠️ Common Mistakes

A few JVM misunderstandings to clear up early:

  • Thinking the JVM runs your source directly. It never reads .java. It only runs the .class bytecode javac produced.

This is the wrong mental model versus the right one.

// ❌ wrong: imagining the JVM reads and runs Hello.java line by line
// ✅ right: javac turns Hello.java into Hello.class bytecode, then the JVM runs the bytecode
  • Thinking Java compiles straight to machine code for your OS. It compiles to bytecode, which the JVM runs. That extra layer is what enables cross-platform running.

  • Confusing the JVM, JRE, and JDK. They are nested, not the same.

This comment lays out the difference so it sticks.

// ❌ wrong: using "JVM", "JRE", and "JDK" as if they mean the same thing
// ✅ right:
// JVM = the engine that runs bytecode
// JRE = JVM + standard libraries (enough to RUN programs)
// JDK = JRE + compiler and tools (needed to BUILD programs)
  • Assuming the interpreter is the only engine. The JIT also turns hot code into native machine code, which is a big reason long-running Java is fast.

  • Believing you must free memory by hand. The garbage collector reclaims unused heap objects automatically.

✅ Best Practices

Habits to keep your mental model of the JVM accurate:

  • Remember the bytecode step. It is why the same .class runs on any system with a JVM.
  • Keep the three subsystems clear. Class loader, runtime data areas, and execution engine each have one job.
  • Keep heap and stack straight. Objects on the heap; method calls and locals on the stack.
  • Know which areas are shared. Heap and method area are shared by all threads; stack, PC register, and native method stack are per thread.
  • Trust the JIT and garbage collector. They optimize hot code and reclaim memory automatically, so you rarely tune them at first.

🧩 What You’ve Learned

Nicely done. Let’s recap the JVM architecture.

  • ✅ The JVM runs Java bytecode, which is why the same compiled code runs on any system (“write once, run anywhere”).
  • ✅ Java compiles .java source into .class bytecode with javac, and the JVM runs that bytecode, not the source.
  • ✅ The class loader loads, links (verifies and prepares), and initializes classes on demand.
  • ✅ The runtime data areas are the JVM’s memory: the heap and method area (shared by all threads), and the stack, PC register, and native method stack (private to each thread).
  • ✅ The execution engine runs bytecode using an interpreter, a JIT compiler for hot code, and a garbage collector for memory.
  • ✅ The JVM, JRE, and JDK are nested: JDK contains the JRE, which contains the JVM.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does the Java compiler produce?

    Why: javac compiles Java to bytecode (.class), which the JVM runs on any system.

  2. 2

    What is the job of the class loader?

    Why: The class loader brings .class files into memory, verifies them, and initializes them.

  3. 3

    Where are objects created with new stored?

    Why: Objects live on the heap, which is shared by all threads.

  4. 4

    What does the JIT compiler do?

    Why: The Just-In-Time compiler turns hot bytecode into native machine code for better performance.

🚀 What’s Next?

We mentioned the heap as the JVM’s main memory area for objects. It is worth understanding deeply, because it explains how objects are stored and why memory errors happen. Let’s explore Java heap memory next.

Java Heap Memory

Share & Connect