JVM

Deep dive into the Java Virtual Machine — class loading, memory model, JIT compilation, and how the JVM turns bytecode into running software.

Last reviewed: January 2025

·Java 8Java 11Java 17Java 21
✅
Quick Understanding: The JVM is the engine that runs your Java program. It loads your .class files, manages memory automatically, and compiles frequently-used code to native machine instructions for speed. Understanding the JVM is what separates junior developers from senior ones in production debugging.

🔍 What Is It?

The JVM (Java Virtual Machine) is an abstract computing machine with three main subsystems:

Class Loader Subsystem

  • →Bootstrap ClassLoader — loads core Java classes (java.lang.*) from rt.jar
  • →Extension ClassLoader — loads from jre/lib/ext
  • →Application ClassLoader — loads your application classes from classpath
  • →Custom ClassLoaders — used by OSGi, application servers, hot-reload frameworks

Runtime Data Areas

  • →Heap — shared across all threads; objects and arrays live here; GC manages it
  • →Stack — per-thread; holds stack frames (local variables, operand stack, frame data)
  • →PC Register — per-thread; points to current bytecode instruction
  • →Native Method Stack — for native (C/C++) method calls via JNI
  • →Metaspace — class structures, method metadata; grows dynamically

Execution Engine

  • →Interpreter — executes bytecode instruction by instruction (slow but immediate)
  • →JIT Compiler — compiles hot methods to native code (fast after warmup)
  • →Garbage Collector — reclaims unreachable objects

JIT Compilation Tiers (HotSpot):

  • →Tier 1: Simple JIT compilation
  • →Tier 2: Limited profiling
  • →Tier 3: Full profiling
  • →Tier 4: Aggressive optimizations (C2 compiler)

🎯 Why Do We Need It?

The JVM gives Java several superpowers:

  • →Platform independence — same bytecode runs on any OS with a JVM
  • →Automatic memory management — no manual malloc/free, no dangling pointers
  • →Security — bytecode verification prevents buffer overflows and type confusion
  • →Performance — JIT compiler achieves near-C++ speed for long-running apps
  • →Observability — JVM exposes metrics via JMX, making monitoring straightforward

For enterprise developers, understanding JVM internals is essential for diagnosing production incidents — OOM errors, GC pauses, thread deadlocks, and CPU spikes all require JVM knowledge to debug.

🏗️ What Happens in Real Projects?

You interact with the JVM daily even if you don't realize it:

  • →Configuring heap in Docker: ENV JAVA_OPTS="-Xmx2g -Xms2g"
  • →Analyzing heap dumps with Eclipse MAT or VisualVM when production OOMs
  • →Reading thread dumps to diagnose deadlocks
  • →Tuning GC when response times degrade
  • →Using jstack, jmap, jstat for live JVM diagnostics
  • →JVM flags in Kubernetes pod specs via environment variables

⚙️ How It Works

Class Loading (3 phases):

  1. Loading — finds and reads the .class file
  2. Linking — Verification (is bytecode valid?), Preparation (allocate static fields), Resolution (resolve symbolic references)
  3. Initialization — run static initializers, set static field values

Garbage Collection (simplified):

  1. Objects are allocated in Eden (young generation)
  2. Surviving objects move to Survivor spaces (S0/S1)
  3. Long-lived objects are promoted to Old Generation (tenured)
  4. Minor GC: cleans young generation (fast, frequent)
  5. Major/Full GC: cleans old generation (slow, infrequent)

JIT Compilation:

  1. Methods start interpreted
  2. HotSpot counts invocations (default: 10,000 for C2)
  3. Hot methods are compiled to native code
  4. Compiled code is cached in Code Cache
  5. Deoptimization can revert to interpreter if assumptions change

💻 Example

JVM Diagnostics in Practice — Essential JVM diagnostic commands every senior developer uses.

bash
# Find Java process ID
jps -l

# Thread dump — diagnose deadlocks, stuck threads
jstack <pid> > thread-dump.txt

# Heap dump — diagnose OOM, memory leaks
jmap -dump:format=b,file=heap.hprof <pid>

# GC stats — live GC activity
jstat -gcutil <pid> 1000   # every 1 second

# JVM flags in use
java -XX:+PrintFlagsFinal -version | grep HeapSize

# Heap histogram (top object types by count)
jmap -histo <pid> | head -30

🤔 Common Confusions

❓ What is the difference between Stack and Heap?

Stack stores method call frames and primitive local variables — it is thread-private, fast, and automatically freed when the method returns. Heap stores all objects — it is shared across threads, managed by GC, and is where memory leaks occur. When you do new Object(), the object goes on the heap; the reference variable goes on the stack.

❓ Does the JVM run one thread or many?

Many. Beyond your application threads, the JVM runs GC threads, JIT compiler threads, finalizer thread, and signal dispatcher threads. A typical JVM process has 20–50 threads even for a simple app. Use jstack to see them all.

❓ What replaced PermGen in Java 8?

Metaspace. PermGen was a fixed-size region in the heap for class metadata — it caused frequent OutOfMemoryError: PermGen space. Metaspace is native memory (off-heap) and grows dynamically. It can still OOM if you have classloader leaks, but it is much more manageable.

🚨 Common Production Issues

GC pause causing request timeouts — long Stop-The-World pauses freeze all threads. Switch to G1GC or ZGC for low-latency requirements.

ClassLoader leak in application servers — redeploying WAR files without proper cleanup causes Metaspace to grow until OOM. Always test hot redeployment.

Code Cache full — JIT stops compiling new methods, app falls back to interpreter and slows down. Fix: -XX:ReservedCodeCacheSize=512m.

Native memory leak — off-heap memory (DirectByteBuffer, JNI) not released. Use -XX:NativeMemoryTracking=summary to diagnose.

✅ Best Practices

Use G1GC (-XX:+UseG1GC) for most applications; ZGC (-XX:+UseZGC) for sub-millisecond pause requirements.

Always capture thread dumps and heap dumps when production incidents occur.

Set -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof in all production JVMs.

Monitor GC pause time, not just frequency.

Use container-aware JVM flags in Docker: -XX:+UseContainerSupport (default Java 10+).

🏛️ Architect's Note

Senior Architect's Perspective

The JVM is a shared resource. In microservices, each service gets its own JVM process — this is intentional isolation. In a monolith on an app server (Tomcat, WildFly), multiple apps share one JVM, which means one misbehaving app can OOM the entire server. Container-per-service architectures solve this at the cost of more memory overhead. The JVM overhead (typically 200–500MB base) is the main reason Node.js and Go gained popularity for microservices.

💬 Frequently Asked Questions

How do I read a thread dump?

Look for threads in BLOCKED state (waiting for a lock held by another thread) — these indicate deadlocks or contention. WAITING threads are parked waiting for a signal. RUNNABLE threads are actively executing. Tools like FastThread.io or IBM Thread Analyzer visualize thread dumps automatically.

What is Stop-The-World in GC?

A Stop-The-World (STW) pause halts all application threads so the GC can safely inspect the heap. Modern GCs (G1, ZGC, Shenandoah) minimize STW by doing most work concurrently. ZGC achieves sub-millisecond STW pauses even on multi-terabyte heaps.

🎯 Key Takeaways

1JVM has three main subsystems: Class Loader, Runtime Data Areas, Execution Engine
2Heap is shared (GC managed); Stack is per-thread (auto freed)
3JIT compiles hot methods to native code after ~10,000 invocations
4Always configure -XX:+HeapDumpOnOutOfMemoryError in production
5G1GC is the default and best general-purpose GC for Java 9+
6jstack, jmap, jstat are your first tools for production JVM diagnosis

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages