Java Virtual Machine Deep Dive: Core Concepts and Runtime Structure

What Is the JVM?

The Java Virtual Machine (JVM) is the cornerstone of Java's cross‑platform capability. It executes compiled Java bytecode and translates it into native machine instructions. The most widely used JVM implementation is HotSpot (originally from Sun Microsystems). Other notable implementations include JRockit (BEA) and J9 (IBM).

JDK, JRE, and JVM Relationship

JDK (Java Development Kit) is the full development environment, including the JRE and tools like javac, java, and jar. JRE (Java Runtime Enviroment) contains the JVM plus essential class libraries. The hierarchy is: JDK > JRE > JVM.

JVM and the Operating System

The JVM sits between the Java programming language and the underlying OS. Bytecode is the intermediate representation that the JVM interprets or compiles, enabling platform independence.

JVM Architecture Overview

The JVM defines several runtime data areas. Some are created at JVM startup and destroyed at shutdown; others are created per thread and exist only for the thread’s lifetime.

  • Thread‑private areas: Program Counter (PC), Java Virtual Machine Stack (VMS), Native Method Stack (NMS).
  • Thread‑shared areas: Heap, Method Area.

Class Loading Subsystem

Types of Class Loaders

Conceptually, all class loaders that derive from java.lang.ClassLoader are considered user‑defined loaders. In practice, three built‑in loaders exist:

  • Bootstrap ClassLoader – Written in C/C++, embedded in the JVM. It loads core Java libraries (e.g., rt.jar) and has no parent.
  • Extension ClassLoader – Written in Java, loads classes from jre/lib/ext directories. Its parent is the Bootstrap loader.
  • Application ClassLoader – Written in Java, loads classes from the classpath. It is the default loader and its parent is the Extension loader.

Custom class loaders can be created for purposes like isolation, encryption, or loading from non‑standard sources.

Parent‑Delegation Model

Class loading follows a parent‑first delegation pattern. When a request arrives, it is delegated upward until the Bootstrap loader is reached. If the parent can load the class, it does; otherwise, the child loader attempts loading. This prevents duplicate loading and protects core APIs from tampering.

Advantages:

  • Avoids reloading the same class.
  • Secures the integrity of core Java libraries.

Phases of Class Loading

  1. Loading – Obtains the binary byte stream of a class (by its fully qualified name), transforms the static structure into a runtime data structure in the method area, and creates a java.lang.Class object as an access entry.
  2. Verification – Ensures the bytecode conforms to the JVM specification (format, metadata, bytecode, symbol references).
  3. Preparation – Allocates memory for class variables (static fields) and sets them to their default zero values. (Instance variables are not included; they are allocated on the heap later.)
  4. Resolution – Replaces symbolic references in the constant pool with direct references (pointers or offsets). This phase may be deferred after initialization in some cases.
  5. Initialization – Executes the <clinit> method, which merges all static variable assignments and static initializer blocks. The JVM ensures that the parent class is initialized before the child.

Only the following six situations trigger immediate initialization: new, getstatic, putstatic, invokestatic, reflection, and when a superclass is not yet initialized. Any other use is considered passive and does not trigger initialization.

Runtime Data Areas

Program Counter (PC) Register

Each thread has a private PC register that holds the address of the currently executing JVM instruction (or an undefined value for native methods). It is the only area in the JVM that does not experience OutOfMemoryError and never undergoes garbage collection.

Java Virtual Machine Stack

Each thread creates a private stack that stores stack frames. A new frame is pushed for every method call and popped upon return. The stack holds:

  • Local Variable Table – Stores method parameters and local variables (primitives, object references, returnAddress). Slot reuse is allowed for expired variables. This table is thread‑safe and fixed at compile time.
  • Operand Stack – A LIFO stack used for intermediate computations. Bytecode instructions push/pop values here. Its depth is determined at compile time (max_stack).
  • Dynamic Linking – Each frame holds a reference to the runtime constant pool. Symbolic references are resolved into direct references at runtime (e.g., invokedynamic).
  • Method Return Address – Stores the state needed to resume the caller. A method can exit normally (via a return instruction) or abnormally (by throwing an unhandled expection).

Stack Overflow: Occurs when a thread requests a stack size larger than the permitted maximum. OutOfMemoryError may arise if the stack cannot be expanded (e.g., when creating new threads).

Native Method Stack

This area is analogous to the Java stack but serves native methods (written in C/C++). It interacts with the JNI (Java Native Interface). Exceptions: StackOverflowError and OutOfMemoryError can be thrown.

Heap

The heap is the largest memory region in the JVM, created at startup. All threads share it, and nearly all object instances are allocated here. It is the primary target for garbage collection.

Heap Structure

The heap is logically divided into generations (though physical layout may vary):

  • Young Generation – Further split into Eden and two Survivor spaces (S0, S1). New objects are allocated in Eden. Minor GC occurs here, copying surviving objects to Survivor spaces.
  • Old Generation – Objects that survive many Minor GC cycles (default threshold of 15) are promoted here. Major GC (Full GC) handles this space.
  • Metaspace (JDK 8+) – Replaces the permanent generation. It stores class metadata and is allocated in native memory, not the Java heap. Prior to JDK 8, the permanent generation was part of the heap.

Heap size can be set with -Xms (initial) and -Xmx (maximum). By default, initial is 1/64 of physical memory, maximum is 1/4. The young:old ratio defaults to 1:2 (i.e., young occupies one‑third of the heap).

TLAB (Thread Local Allocation Buffer)

To reduce contention when multiple threads alloctae objects in the heap, each thread can have its own local buffer within Eden. TLAB is enabled by default and improves allocation throughput.

Escape Analysis

If the JIT compiler determines that an object does not escape the method (i.e., it is only used locally), it may allocate the object on the stack instead of the heap. This eliminates garbage collection overhead. Escape analysis also enables lock elision (removing unnecessary synchronization) and scalar replacement (splitting an object into its primitive fields).

Method Area

The method area (a logical part of the heap in the JVM specification) stores:

  • Type information (class/interface metadata).
  • Constants, static variables.
  • Code cache from the JIT compiler.
  • Runtime constant pool (derived from the class file’s constant pool).

Implementation: In HotSpot, the method area was part of the permanent generation until JDK 7, but since JDK 8 it resides in the native-metaspace. Metaspace size can be controlled via -XX:MetaspaceSize and -XX:MaxMetaspaceSize. If left unlimited, it can consume all available system memory.

Garbage Collection Basics

Garbage collection (GC) focuses on the heap. Minor GC (Young GC) happens frequently, while Major GC (Full GC) is less common. Full GC triggers include: explicit System.gc() calls, insufficient space in the old generation, insufficient space in the method area, promotion failures from survivor spaces, and average promotion size exceeding old‑generation capacity.

Common GC algorithms:

  • Mark‑Sweep – Simple but produces fragmentation.
  • Copying – Used in the young generation; fast but wastes one survivor space.
  • Mark‑Compact – Used in the old generation; avoids fragmentation but is slower.
  • Generational – Combines the above: copying for young, mark‑compact for old.

Memory issues are diagnosed by distinguishing a memory leak (objects unreachable by GC but still referenced) from a memory overflow (genuine need for more memory). Tools like heap dumps and profilers (MAT, JProfiler) help trace the root cause.

Tags: JVM HotSpot ClassLoader parent-delegation runtime-data-areas

Posted on Fri, 31 Jul 2026 16:53:59 +0000 by USMarineNCO