Fundamentals of the JVM: HotSpot, JIT, Memory Areas, Garbage Collection, and Class Loading
The article explains how the HotSpot JVM executes Java by interpreting bytecode, using client or server JIT compilers to translate hot methods into native code, describes the five runtime memory areas, outlines major garbage‑collection algorithms and tuning options, and details the class‑loading process and hierarchical classloader model.
JVM Basics
The term "JVM" usually refers to the HotSpot virtual machine. Java source code is first compiled to bytecode, which the JVM interprets. HotSpot improves performance by compiling bytecode to native code.
HotSpot contains an interpreter and two compilers (client and server) that work in a mixed mode.
Compilers and Interpreter
• Compiler: translates source code to bytecode. • Interpreter: parses bytecode at runtime. • Client compiler: fast startup, low memory usage, lower execution efficiency; does not enable dynamic compilation by default, suitable for desktop applications. • Server compiler: slower startup, higher memory usage, higher execution efficiency; enables dynamic compilation by default, suitable for server applications.
Example commands to view the current execution engine:
$ java -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode) $ java -Xint -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, interpreted mode) $ java -Xcomp -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, compiled mode)Java Compilation Principles
• Bytecode: the .class file produced by the javac compiler. • Machine code / native code: instructions directly understood by the operating system.
Java follows the "write once, run anywhere" principle: source code is compiled to bytecode, and different JVM implementations translate the bytecode to native instructions for each platform.
JIT (Just‑In‑Time) Compilation
The interpreter translates bytecode line‑by‑line, which is slower than executing native code. JIT compiles frequently executed (hot) code into native code and applies optimizations, improving performance.
Hot code is identified either by sampling stack frames or by per‑method counters. When a method’s counter exceeds a threshold, the JIT compiler is invoked.
Hot‑spot detection mechanisms:
Sampling: periodically checks thread stack tops; methods frequently on the top are considered hot.
Counter‑based (used by HotSpot): each method has a counter that increments on execution; exceeding a threshold marks the method as hot.
Counter = method counter + back‑edge counter. When the combined counter reaches the threshold, compilation is requested.
Method counter records how many times a method is invoked (with decay over time).
Back‑edge counter records loop back‑jumps in bytecode.
JVM Runtime Data Areas
The JVM divides memory into five regions: Method Area, Heap, Java Stack, Native Method Stack, and Program Counter.
Method Area : stores class metadata, constant pool, static variables, and JIT‑compiled native code.
Heap : the largest memory region; all object instances and arrays are allocated here.
Java Stack : thread‑local; each stack frame contains a local variable table, operand stack, dynamic linking information, and return address.
Local variable table holds primitive values, object references, and return addresses. The operand stack is used for intermediate calculations. Dynamic linking resolves method calls at runtime. The return address indicates where execution should continue after a method returns.
Native Method Area : holds methods marked with native , implemented in C/C++ and tied to the thread’s lifecycle.
Program Counter (PC) : a small per‑thread register that records the address of the next bytecode instruction to execute, enabling thread‑switch resumption.
Garbage Collection (GC) Mechanisms
Unlike C/C++, Java delegates memory management to the JVM, eliminating manual free operations. GC automatically reclaims unreachable objects.
Common ways to determine unreachable objects:
Reference counting (rarely used in modern JVMs due to inability to handle cyclic references).
Reachability analysis: starting from GC roots, objects not reachable are considered garbage.
GC roots include:
1. Objects referenced from the Java stack (local variable tables).
2. Static fields in the method area.
3. Constants in the method area.
4. Objects referenced from native code (JNI).GC Algorithms
Copying (Coping)
Mark‑Sweep
Mark‑Compact
Generational collection
Mark‑Sweep : two phases – mark reachable objects, then sweep away unmarked ones. Drawbacks: low space utilization and fragmentation.
Mark‑Compact : improves on Mark‑Sweep by moving live objects together, reducing fragmentation.
Copying Algorithm : divides memory into two equal halves; live objects are copied to the active half, then the other half is cleared. It offers high throughput when object survival rate is low.
Summary of algorithm trade‑offs:
Time complexity: Copying > Mark‑Sweep > Mark‑Compact.
Space complexity: Mark‑Sweep ≈ Mark‑Compact > Copying.
Generational Collection
Objects are divided into Young Generation (low survival rate) and Old Generation (high survival rate). Young generation typically uses the copying algorithm with an Eden:Survivor ratio of 8:1:1.
eden: the area where new objects are allocated.
s1, s2: survivor spaces used during copying.When Eden fills, a Minor GC is triggered. Objects that survive several Minor GCs are promoted to the Old Generation.
Common Garbage Collectors
Young Generation: Serial, ParNew, Parallel Scavenge
Old Generation: Serial Old, Parallel Old, CMS, G1
Examples of JVM options for collectors:
-XX:+UseSerialGC (young generation, single‑threaded)
-XX:+UseParNewGC (young generation, multi‑threaded, works with CMS)
-XX:+UseParallelGC (young generation, multi‑threaded, throughput‑oriented)
-XX:+UseSerialOldGC (old generation, single‑threaded)
-XX:+UseParallelOldGC (old generation, multi‑threaded)
-XX:+UseConcMarkSweepGC (old generation, concurrent, low pause)
-XX:+UseG1GC (default in JDK 9+, combines copying and mark‑compact, works on both generations)
Typical JVM tuning parameters:
-Xmx / -Xms: maximum and initial heap size.
-Xmn: size of the young generation.
-Xss: thread stack size.
-XX:NewRatio, -XX:SurvivorRatio: control the proportion between young and old generations.
-XX:+PrintTenuringDistribution: prints object aging information.
-XX:+HeapDumpOnOutOfMemoryError and -XX:HeapDumpPath: generate heap dumps on OOM.
Memory Tuning Summary
Set initial heap size equal to maximum heap size to reduce GC frequency.
Larger heap improves throughput, but must fit physical memory.
Prefer parallel collectors on multi‑core servers.
Typical young:old ratio is 1:2 or 1:3.
Avoid promoting many objects to the old generation; tune survivor space and tenuring thresholds.
Class Loading Process
Loading → Linking → Initialization.
Loading: JVM reads the .class file, creates a java.lang.Class object in the method area.
Linking: consists of verification, preparation (allocate memory for static fields and set default values), and resolution (replace symbolic references with direct references).
Initialization: executes static initializers and assigns proper values to static fields.
Example illustrating preparation vs. initialization:
class Demo{
private static int a = 10;
}Loading creates the class object; preparation sets a to its default 0 ; resolution resolves symbolic references; initialization assigns 10 to a .
Class Loaders and Delegation Model
Bootstrap ClassLoader: loads core Java classes from jre/lib/rt.jar ; written in native code; has no parent.
Extension ClassLoader: loads JARs from jre/lib/ext ; parent is Bootstrap.
Application (System) ClassLoader: loads classes from the classpath; parent is Extension.
Custom ClassLoader: user‑defined, extends ClassLoader ; parent is Application.
The parent‑delegation model ensures that a class loader first asks its parent to load a class, guaranteeing core API classes cannot be overridden.
Example of a reference‑counting GC case (illustrates why reference counting is insufficient for cycles):
class User{
User user;
}
public static void main(String[] args){
User a = new User();
User b = new User();
a.user = b;
b.user = a;
a = null;
b = null;
// The two objects still reference each other and cannot be reclaimed.
}37 Interactive Technology Team
37 Interactive Technology Center
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.