Saturday, April 18, 2026

JVM Internals, Memory Model & Garbage Collection

 

JVM Internals, Memory Model & Garbage Collection

Senior / Principal Java Engineer – Complete Interview Master Guide

HotSpot Architecture · Heap & Non-Heap · Class Loading · JIT · JMM · All GC Algorithms up to Java 21

Prepared for Ashok Koritala – Senior Java Full Stack Developer (15+ yrs)
Java Version Covers Java 8 through Java 21 (LTS)
Total Questions 45 expert-level Q&A; across 9 topic clusters
TopicsJVM architecture · Runtime data areas · Class loading · JIT · JMM · Serial/Parallel/CMS/G1/ZGC/Shenandoah · GC tuning · Virtual Threads

Table of Contents

§1 JVM Architecture & Runtime Data Areas Q1–Q6

§2 Class Loading Subsystem Q7–Q10

§3 JIT Compiler & Execution Engine Q11–Q14

§4 Java Memory Model (JMM) – Core Concepts Q15–Q19

§5 volatile, synchronized & Happens-Before Q20–Q24

§6 GC Fundamentals – Algorithms & Generations Q25–Q28

§7 GC Collectors Deep Dive (Serial ZGC) Q29–Q36

§8 GC Tuning, Flags & Diagnostics Q37–Q41

§9 Java 17–21: ZGC Generational, Loom & ScopedValues Q42–Q45

§1 JVM Architecture & Runtime Data Areas

Q1. Describe the high-level architecture of the HotSpot JVM.

The HotSpot JVM (the reference JVM bundled with OpenJDK/Oracle JDK) has three main subsystems:

Class Loader Subsystem – loads, links, and initialises .class bytecode.

Runtime Data Areas – memory regions managed by the JVM (Heap, Stack, Method Area, PC Register, Native

Stack).

Execution Engine – interprets bytecode and compiles hot methods via JIT (C1/C2 compilers). Includes the

Garbage Collector.

Key Insight: The JVM spec defines behaviour; HotSpot is one implementation. Others include GraalVM, OpenJ9, AzulZing. Always clarify 'HotSpot' in interviews.
Interview Tip: Draw a box diagram: ClassLoader Runtime Data Areas (Heap/Non-Heap) Execution Engine(Interpreter + JIT + GC). Interviewers love this visual.
Q2. What are the JVM Runtime Data Areas? Explain each in detail.

The JVM defines 5 runtime data areas:

1. Heap – largest area; all object instances and arrays live here. Divided into Young Generation (Eden + S0 + S1)

and Old Generation (Tenured). Shared across all threads. Subject to GC.

2. Method Area (Metaspace from Java 8) – stores class metadata: field/method definitions, bytecode, constant

pool, static variables. Per-JVM (shared). In Java 7 and earlier this was PermGen (fixed size on heap); from Java 8

it is Metaspace (native memory, auto-grows).

3. JVM Stack (Thread Stack) – per-thread. Each method invocation creates a Stack Frame containing local

variables, operand stack, frame data, and reference to runtime constant pool. StackOverflowError on overflow;

OutOfMemoryError if JVM cannot create new thread.

4. PC (Program Counter) Register – per-thread. Holds address of current JVM instruction being executed.

Undefined for native methods.

5. Native Method Stack – per-thread. Used for native (JNI) methods. C-stack equivalent.

Area Scope GC'd? OOM Possible? Key Flags
Heap All threads Yes Yes -Xms -Xmx
Metaspace All threads Partial Yes -XX:MaxMetaspaceSize
JVM Stack Per-thread No SOE/OOM -Xss
PC Register Per-thread No No
Native Stack Per-thread No OOM -Xss
■■ Watch Out: PermGen was replaced by Metaspace in Java 8. java.lang.OutOfMemoryError: PermGen space no longer exists. New error: OutOfMemoryError: Metaspace.
Q3. Explain the Heap structure: Young Generation, Old Generation, and how objects age.

The heap is partitioned to exploit object mortality (most objects die young):

Young Generation (~25% of heap by default):

• –
Eden: all new objects are allocated here (TLAB – Thread-Local Allocation Buffer for fast bump-pointer

allocation).

• –
Survivor 0 (S0) and Survivor 1 (S1): objects that survive one Minor GC are copied here. Only one survivor

space is 'active' at a time.

Old (Tenured) Generation (~75%): objects that survive a threshold number of Minor GCs (default: 15 copies,

controlled by -XX:MaxTenuringThreshold) are promoted here.

Minor GC (Young GC): collects only Young Gen. Fast, stop-the-world (STW), frequent.

Major GC: collects Old Gen. Slower, often STW.

Full GC: collects entire heap + Metaspace. Most disruptive.

# Heap layout visualisation:

■■■■■■■■■■■■■■■■■■■■■■■■■■■■ HEAP ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ■ Young Generation Old (Tenured) Generation ■ ■ ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ■ ■■■■■■■■■■■■■■■■■■■■■■■■■ ■ ■ ■ Eden S0 S1 ■ ■ ■ Long-lived objects ■ ■

■ ■
(new obj)(from) (to) ■ ■ ■ (survived 15 GCs) ■ ■ ■ ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ■ ■■■■■■■■■■■■■■■■■■■■■■■■■ ■ ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■-~33% each S0/S1 + Eden ~75% of total heap

Key Insight: TLAB (Thread-Local Allocation Buffer): each thread gets a private slice of Eden for lock-free bump-pointer

allocation. -XX:+UseTLAB (default on). Eliminates synchronisation on allocation.

Q4. What is Metaspace? How does it differ from PermGen and how do you tune it?

PermGen (Java 7 and earlier): fixed-size heap region storing class metadata. Default 64–256 MB. Frequent OutOfMemoryError: PermGen space in apps with heavy class loading (e.g., hot-deploy, dynamic proxies, OSGi).

Metaspace (Java 8+): moved to native (OS) memory outside the Java heap. Grows automatically up to available native memory. Eliminates PermGen OOM for most workloads.

• -XX:MetaspaceSize – initial metaspace size (triggers first GC to resize, not a hard limit).

• -XX:MaxMetaspaceSize – hard cap (default: unlimited – set this in production!).

• -XX:MinMetaspaceFreeRatio / -XX:MaxMetaspaceFreeRatio – control how aggressively metaspace

grows/shrinks.

• Class metadata is GC'd when its ClassLoader is collected (e.g., in hot-redeploy scenarios).

■■ Watch Out: Not setting -XX:MaxMetaspaceSize in production can allow Metaspace to consume all native memory, causing an OS-level OOM kill. Always cap it.

Q5. What is a Stack Frame? What does it contain?

Every method invocation pushes a new Stack Frame onto the thread's JVM Stack. A frame contains:

Local Variable Array – slot 0 is 'this' for instance methods. Holds primitives and object references. Long/double

occupy 2 slots.

Operand Stack – working area for bytecode instructions (push/pop operands). Like a CPU register file.

Frame Data – reference to the runtime constant pool of the current class; exception table for try-catch dispatch.

Return Address – where to resume after the method returns.

// Bytecode for: int add(int a, int b) { return a + b; }

iload_1 // push local[1] (a) onto operand stack iload_2 // push local[2] (b) onto operand stack iadd // pop two, push sum

ireturn // pop result, return to caller's frame

Interview Tip: Understanding the operand stack is key for questions about bytecode, lambda capture, and method

handles. Mention it when discussing how lambdas are compiled.

Q6. What is the Code Cache and Compressed OOPs?

Code Cache: native memory region where the JIT compiler stores compiled native code. Divided (Java 9+) into three segments: non-method code (JVM internals), profiled code (C1 compiled), non-profiled code (C2 compiled). Flags: -XX:InitialCodeCacheSize, -XX:ReservedCodeCacheSize (default 240 MB). If full, JIT compilation stops: 'CodeCache is full. Compiler has been disabled.'

Compressed OOPs (Ordinary Object Pointers): on 64-bit JVMs with heap <= 32 GB, HotSpot uses 32-bit references internally (instead of 64-bit), halving reference memory usage. The JVM scales pointers by 8 (3-bit shift) to address 32 GB. Enabled by default: -XX:+UseCompressedOops. Disabled above 32 GB heap.

■■ Watch Out: Setting -Xmx32g disables Compressed OOPs (heap >32 GB threshold). If you need just above 32 GB, the sweet spot is -Xmx31g to keep CompressedOops. Disabling it increases memory usage by ~20–40%.

§2 Class Loading Subsystem

Q7. Describe the class loading process: Loading, Linking, Initialisation.

The JVM loads a class lazily (first active use). Three phases:

1. Loading – ClassLoader reads the .class file, creates a binary representation, and creates a java.lang.Class

object in the Heap (as of Java 8).

2. Linking – three sub-phases:

• –
Verification: bytecode verifier checks structural correctness (no stack underflow, valid type references). Can

be skipped with -Xverify:none (dangerous).

• –
Preparation: allocates memory for static fields, sets them to default values (0/null/false). No user code runs.

• –
Resolution: replaces symbolic references (class/method/field names in constant pool) with direct references

(memory addresses). May be lazy (invokedynamic).

3. Initialisation – executes the class initialiser which runs static initialisers and static variable assignments in

source-code order.

Key Insight: Class initialisation is thread-safe and performed only once, protected by a class-level lock. This is why the

Holder idiom for singletons is thread-safe without explicit synchronisation.

Q8. Explain the ClassLoader hierarchy and delegation model.

Bootstrap ClassLoader (C++ in HotSpot, null in Java): loads core Java classes (java.lang.*, java.util.*) from the

JDK's modules (Java 9+) or rt.jar (Java 8).

Platform ClassLoader (formerly Extension CL, Java 9+): loads Java SE platform APIs (previously

$JAVA_HOME/lib/ext).

Application (System) ClassLoader: loads classes from -classpath / --module-path.

User-defined ClassLoaders: application servers, OSGi, hot-reload (Spring DevTools) use custom CLs for

isolation.

Parent Delegation Model: when asked to load a class, a ClassLoader first delegates to its parent. Only if the parent cannot find it does the child attempt to load. Prevents user code from overriding core classes.

// Breaking parent delegation for hot-reload (simplified): protected Class loadClass(String name, boolean resolve) { if (isAppClass(name)) {

// Load directly (check our own path FIRST)

Class c = findClass(name); // reads .class from disk if (resolve) resolveClass(c); return c; } return super.loadClass(name, resolve); // delegate for JDK classes }

■■ Watch Out: Two classes loaded by different ClassLoaders are DIFFERENT types even if identical bytecode. This is why ClassCastException appears in app servers after hot redeploy.

Q9. What triggers class initialisation? What is the difference between active and passive use?

Active use (triggers initialisation):

• Creating an instance (new), calling a static method, reading/writing a static field (not a compile-time constant).

• Reflection: Class.forName() (with initialize=true).

• JVM startup class (main method class).

• A subclass being initialised triggers parent class initialisation.
Passive use (does NOT trigger initialisation):

• Accessing a static final compile-time constant (inlined by javac).

• Creating an array of type T: new T[10] – does not initialise T.

• Class.forName() with initialize=false.

class Config {

static final int MAX = 100; // compile-time constant – no init static final String HOST; // NOT a compile-time constant

static { HOST = System.getenv('HOST'); System.out.println('INIT'); } }

// Accessing Config.MAX
NO 'INIT' printed (javac inlines 100) // Accessing Config.HOST 'INIT' printed

Q10. How does Java 9 module system (JPMS) change class loading?

Java 9 introduced the Java Platform Module System (JPMS / Project Jigsaw). Key changes to class loading:

• rt.jar and tools.jar removed; JDK split into ~70 named modules.

• Bootstrap CL now loads from module image (jimage file); 'null' parent still applies for core modules.

• Strong encapsulation: packages not exported by a module cannot be accessed even via reflection without

--add-opens.

• Unnamed module: all classpath code lives here; can access all exported packages.

• Automatic modules: JARs placed on the module path without a module-info.class get an auto-generated name.

■■ Watch Out: Many frameworks (Spring, Hibernate, Mockito) required --add-opens workarounds for Java 9–11 migration. Java 17 made strong encapsulation the default (JEP 396/403).

§3 JIT Compiler & Execution Engine

Q11. How does HotSpot's JIT compilation pipeline work (Interpreter C1 C2)?

HotSpot uses a tiered compilation strategy (default since Java 7):

Tier 0 – Interpreter: bytecode interpreted, collects profiling data (invocation counts, branch statistics).

Tier 1 – C1 (Client compiler): compiles lightly profiled methods to native code quickly. Fast startup, moderate

optimisation.

Tier 2/3 – C1 + profiling: compile with full profiling instrumentation.

Tier 4 – C2 (Server compiler): heavily optimising JIT for 'hot' methods. Inlining, escape analysis, loop unrolling,

auto-vectorisation, dead-code elimination.

Methods become 'hot' after ~10,000 invocations (CompileThreshold). C2 may deoptimise back to interpreter if a speculative assumption is violated (e.g., a virtual call target changes).

# Key JIT flags:

-XX:+TieredCompilation # default on (Java 8+) -XX:CompileThreshold=10000 # invocations before C2 compilation -XX:+PrintCompilation # log each compilation

-XX:+PrintInlining # show inlining decisions

-XX:MaxInlineSize=35 # max bytecode size for inlining (default 35 bytes)

Key Insight: GraalVM's Graal JIT (written in Java) can replace C2 via -XX:+UseJVMCICompiler. It produces better

code for some workloads and enables Ahead-of-Time (native-image) compilation.

Q12. What is Escape Analysis and what optimisations does it enable?

Escape Analysis: the JIT determines whether an object reference can 'escape' the current method or thread. If not, optimisations apply:

Stack allocation: non-escaping objects are allocated on the stack instead of the heap, eliminating GC pressure

entirely.

Scalar replacement: an object's fields are replaced by separate local variables (scalars) in registers, eliminating

the object allocation.

Lock elision: synchronisation on a non-escaping object is removed (no other thread can see it).

// This Point object may never be heap-allocated:

public double distance(double x1, double y1, double x2, double y2) { Point p = new Point(x2 - x1, y2 - y1); // does not escape

return Math.sqrt(p.x*p.x + p.y*p.y); // JIT: scalar-replaces p.x, p.y }

Interview Tip: Ask interviewers: 'Does escape analysis mean we no longer need object pooling?' Answer: mostly yes

for small, short-lived objects, but pool large objects or those with expensive initialisation.

Q13. What is On-Stack Replacement (OSR)?

On-Stack Replacement (OSR) allows the JVM to replace an actively running interpreted method with its compiled version mid-execution – specifically at loop back edges.

Without OSR, a long-running loop in an interpreted method would never benefit from JIT compilation until the method returned and was re-entered. OSR patches the stack frame in place, replacing interpreter state with compiled state at the loop back-edge.

Key Insight: OSR-compiled code is sometimes less optimised than normally-compiled code because the JIT must

conform to the interpreter's variable layout at the OSR entry point.

Q14. What is the String Pool (interning) and how does it interact with the heap?

String Pool (Intern Pool): a HashSet-like structure holding canonical String instances. String literals in .class files are automatically interned. String.intern() explicitly interns.

Java 6 and earlier: intern pool lived in PermGen – limited, caused PermGen OOM.

Java 7+: intern pool moved to the main heap – can be GC'd, no size limit.

• String literals (compile-time constants) are automatically pooled: "Hello" == "Hello" is true.

• new String('Hello') creates a new heap object NOT in the pool.

• new String('Hello').intern() returns the pooled reference.

String a = 'Hello'; // from pool

String b = 'Hello'; // same pool reference

String c = new String('Hello'); // new heap object

System.out.println(a == b); // true (same pool ref)

System.out.println(a == c); // false (different objects) System.out.println(a == c.intern()); // true (c.intern() returns pool ref)

■■ Watch Out: Aggressive use of String.intern() in high-throughput apps can cause contention on the intern table. Consider alternatives like Guava's Interner with weakKeys.

§4 Java Memory Model (JMM) – Core Concepts

Q15. What is the Java Memory Model and why was it redesigned in Java 5 (JSR-133)?

The JMM is a specification that defines how threads interact through memory: which reads/writes are visible, what orderings are permitted, and what constitutes a data race.

Pre-Java 5 problems: the original JMM (JDK 1.0) was underspecified and broken. volatile did not guarantee ordering; Double-Checked Locking was broken; final fields could be seen in an uninitialised state. JSR-133 (Java 5) rewrote the JMM with formal happens-before semantics.

Visibility: when is a write by thread A seen by thread B?

Ordering: what reorderings can the JIT/CPU perform?

Atomicity: which operations are indivisible?

Key Insight: The JMM is intentionally abstract – it does not prescribe specific CPU instructions. Compliance is the

JVM's responsibility. The JMM maps to hardware memory models (TSO on x86, relaxed on ARM/POWER).

Q16. Explain the main-memory / working-memory model and the 8 inter-thread actions.

Every thread has its own working memory (conceptually: CPU registers + caches). The main memory holds the master copy of all variables. Threads operate on copies; writes may not be immediately visible to others.

The JMM defines 8 inter-thread actions that participate in happens-before ordering:

Action Description
lock Acquire a monitor (enter synchronized block/method)
unlock Release a monitor (exit synchronized block/method)
volatile read Read of a volatile-declared field
volatile write Write to a volatile-declared field
thread start Thread.start() – HB all actions in the new thread
thread join Thread.join() – all thread actions HB join() return
final field write Constructor write of final field (freeze action)
normal read/write Ordinary reads/writes – no cross-thread guarantee
Interview Tip: When asked 'what guarantees does synchronized give?', walk through unlock HB lock and the visibilityflush that entails.
Q17. What are all 8 Happens-Before rules?

Happens-before (HB) is a partial order. If A HB B, A's effects are guaranteed visible to B. The JMM defines exactly these 8 rules:

Program Order: every action in a thread HB every subsequent action in that thread.

Monitor Lock: an unlock of monitor M HB every subsequent lock of M.

Volatile Variable: a volatile write to V HB every subsequent volatile read of V.

Thread Start: Thread.start() HB every action in the started thread.

Thread Termination: all thread actions HB Thread.join() returning / isAlive()=false.

Interruption: Thread.interrupt() HB the interrupted thread detecting the interrupt.

Finalizer: end of constructor HB start of object's finalizer.

Transitivity: if A HB B and B HB C, then A HB C.

Key Insight: Transitivity is the power tool: it allows piggyback visibility. A volatile write 'carries along' all prior writes in

the same thread.

Q18. What is instruction reordering? What three sources cause it?

Compiler reordering: javac and JIT reorder independent statements for better register utilisation.

CPU out-of-order execution: modern CPUs (Intel, ARM) execute instructions out of program order using

reservation stations. In-order commit but out-of-order execute.

Memory subsystem reordering: store buffers and cache coherence protocols mean writes become visible to

other CPUs in different orders. x86 has Total Store Order (TSO) – weaker than SC. ARM has an even weaker

model.

Key Insight: The JMM permits any reordering that preserves single-thread semantics (as-if-serial). Only

synchronisation constructs constrain cross-thread reordering.

Q19. How does the JMM handle final fields and safe publication?

The JMM provides a freeze action for final fields: when the constructor of an object completes, any thread that obtains the object reference through a safely published channel is guaranteed to see the final fields correctly initialised – no synchronisation needed.

• Safe publication channels: static initialiser, volatile field, AtomicReference, properly synchronised field.

• Non-final fields get NO freeze guarantee – even if written in the constructor.

• Leaking 'this' from a constructor breaks the freeze even for final fields.

public class ImmutablePoint {

public final int x, y;

ImmutablePoint(int x, int y) { this.x = x; this.y = y; } // Any thread that sees a non-null reference to ImmutablePoint // will see correct x and y -- no volatile/synchronized needed }

■■ Watch Out: Publishing via a non-volatile, non-final field is unsafe. Another thread may see a non-null reference to a partially constructed object with default field values.

§5 volatile, synchronized & Happens-Before in Practice

Q20. What guarantees does volatile provide and what does it NOT guarantee?

Visibility: volatile write HB every subsequent volatile read of same variable.

Ordering barrier: no instruction can be reordered across a volatile access.

Atomicity for single R/W: volatile long/double reads and writes are atomic (important on 32-bit JVMs).

Does NOT guarantee: compound operations (i++ = read + increment + write is not atomic), mutual exclusion, or atomicity of multi-variable invariants.

volatile boolean running = true; // safe: single write, multiple reads volatile int version = 0; // safe: single writer volatile int counter = 0; counter++; // UNSAFE: read-modify-write, NOT atomic

// Fix: use AtomicInteger or synchronized

Interview Tip: Classic interview trap: 'Is volatile sufficient for a counter?' Never – use AtomicInteger.

Q21. Describe the memory barriers inserted by the JIT for volatile.

StoreStore before volatile write: all prior stores must complete first.

StoreLoad after volatile write: write visible before any later load (most expensive).

LoadLoad before volatile read: no preceding load can be reordered after.

LoadStore after volatile read: no following store can reorder before.

On x86 (TSO), only StoreLoad requires an actual hardware fence (MFENCE/LOCK XCHG). LoadLoad and LoadStore are free. ARM/POWER require all four explicit barriers.

Key Insight: Java 9+ VarHandles expose acquire/release modes: acquire = LoadLoad+LoadStore, release =

StoreStore+LoadStore. On x86 these are essentially free, giving real ARM/POWER speedups.

Q22. What three guarantees does synchronized provide?

Mutual Exclusion: at most one thread holds the monitor at a time.

Visibility: unlock HB subsequent lock of same monitor – all writes in the critical section are flushed and visible to

the next thread that acquires the same lock.

Ordering: operations within the block cannot be reordered outside it.

class BankAccount { private long balance; // synchronized provides: atomicity + visibility + ordering public synchronized void deposit(long amount) { balance += amount; }

public synchronized long getBalance() { return balance; } }

■■ Watch Out: Two methods synchronised on DIFFERENT objects provide NO mutual exclusion between them. And static synchronized uses Class lock, not instance lock – mixing them is a common bug.

Q23. Explain Double-Checked Locking (DCL) and how volatile fixes it.

// BROKEN pre-Java 5 (without volatile): private static Singleton instance;

public static Singleton getInstance() { if (instance == null) { // check 1 (no lock) synchronized (Singleton.class) { if (instance == null) { // check 2 (locked)

instance = new Singleton(); // reorderable: 3 sub-ops! } // 1.alloc, 2.write ref, 3.init

} }

return instance; // may return partially-constructed object! }

// FIXED: volatile prevents reordering of write-ref before constructor completes private static volatile Singleton instance;

// BEST: Holder idiom (no volatile needed – class init is thread-safe) private static class Holder {

static final Singleton INSTANCE = new Singleton(); }

public static Singleton getInstance() { return Holder.INSTANCE; }

Key Insight: The Holder idiom leverages the JVM's class-initialisation lock. Prefer it over DCL.

Q24. Compare volatile vs AtomicInteger vs synchronized vs ReentrantLock.

Construct Visibility Atomicity Mutual Excl.Overhead Best For
volatile Yes Single R/W onlyNo Very low Flags, status fields
AtomicInteger Yes CAS compound opsNo (lock-free)Low Counters, CAS patterns
synchronized Yes Yes (block) Yes Medium Multi-var invariants
ReentrantLock Yes Yes (block) Yes Medium tryLock, conditions, fair queue
LongAdder Yes Yes (striped) No Very low High-throughput counters
StampedLock Yes Yes Yes Low (opt.read)Read-heavy, non-reentrant
Interview Tip: LongAdder vs AtomicLong: under high contention, LongAdder is 10–20x faster. Use AtomicLong whenyou need current value frequently; LongAdder when you only need sum at the end.

§6 GC Fundamentals – Algorithms & Generations

Q25. What are the four fundamental GC algorithm types?

1. Mark-and-Sweep: (a) Mark phase: trace from GC roots, mark all live objects. (b) Sweep phase: reclaim

unmarked (dead) objects. Problem: fragmentation.

2. Mark-Sweep-Compact: after sweep, compact live objects to one end. Eliminates fragmentation; enables fast

bump-pointer allocation. Slower (move cost).

3. Mark-Copy (Copying GC): divide memory into two semi-spaces. Copy live objects to 'to-space'; reclaim entire

'from-space'. Fast allocation, no fragmentation. Wastes 50% of memory. Used for Young Gen (Eden
Survivor).

4. Generational GC: exploits the weak generational hypothesis – most objects die young. Divide heap into

Young and Old generations; collect Young frequently (cheap), Old infrequently (expensive).

GC Roots: the starting points for reachability tracing: thread stacks, static fields, JNI references, class objects. Any object reachable from a GC root is live.

Key Insight: All modern JVM GCs are generational (except ZGC which is non-generational until Java 21). They

combine Mark-Copy for Young Gen and Mark-Sweep-Compact for Old Gen.

Q26. What is Stop-The-World (STW) and why is it needed?

A Stop-The-World pause suspends all application (mutator) threads while the GC performs certain phases. This ensures a consistent heap snapshot – without STW, objects could be modified during GC, invalidating the mark or move.

Why STW is needed: (a) Initial mark: identify GC roots. (b) Re-mark (final mark): catch objects modified during

concurrent marking. (c) Object relocation: moving objects requires updating all references.

• Modern GCs (G1, ZGC, Shenandoah) minimise STW by doing most work concurrently.

• ZGC achieves sub-millisecond STW pauses by using load barriers and coloured pointers to handle concurrent

relocation.

GC STW Phases Typical Max Pause
Serial / Parallel All phases STW 100ms – seconds
CMS Initial+Remark STW 10–200ms
G1 Initial+Remark+Cleanup 5–200ms (target configurable)
ZGC (Java 11–20) Initial+Final mark+Relocate roots <1ms – ~10ms
ZGC (Java 21 Gen) Per-generation roots Sub-millisecond
Shenandoah Initial+Final mark+Roots <10ms
Q27. Explain object promotion, allocation failure, and when a Full GC is triggered.

Object Promotion: objects surviving MaxTenuringThreshold (default 15) Minor GCs are promoted to Old Gen.

Also promoted immediately if too large for Eden (large objects bypass Young Gen).

Allocation Failure: Eden is full Minor GC triggered. If after Minor GC the surviving objects don't fit in Survivor

space
premature promotion to Old Gen.

Promotion Failure: Old Gen has insufficient space to receive promotions Full GC.

Explicit System.gc(): requests (but doesn't force) a Full GC. Disable with -XX:+DisableExplicitGC (common in

production).

Concurrent Mode Failure (CMS): CMS cannot finish concurrent sweep before Old Gen fills falls back to serial

Full GC.

Humongous allocation (G1): objects > region_size/2 go directly to Humongous regions; if insufficient, G1

triggers a Full GC.

■■ Watch Out: Frequent Full GCs are a serious production problem. Root causes: memory leak (heap sizing), premature promotion (survivor space too small), excessive large object allocation.

Q28. What are GC Roots and what is the Tri-Colour Marking algorithm?

GC Roots (always considered live):

• Thread stack local variables and operand stack references.

• Static fields of loaded classes.

• JNI global references.

• Synchroniser objects (monitors with waiters).

• Class objects in Metaspace.

Tri-Colour Marking: concurrent GCs use three colours to track marking progress:

White: not yet visited. At end of marking = garbage.

Grey: discovered (reachable from root) but children not yet scanned. In the worklist.

Black: visited and all children scanned. Definitely live.

Invariant: no black object may point directly to a white object (Strong Tri-Colour Invariant). Violated by mutator (application) writing a reference while concurrent marking is running. Requires a write barrier to re-grey the black object or track the white pointer.

Key Insight: CMS, G1, Shenandoah, and ZGC all use variants of tri-colour marking. The difference is in their write/load

barrier implementations.

§7 GC Collectors Deep Dive: Serial ZGC

Q29. Describe the Serial GC.

• Single-threaded: one GC thread for both Young and Old collection.

• Young: Mark-Copy (stop-the-world). Old: Mark-Sweep-Compact (STW).

• Enabled with -XX:+UseSerialGC.

• Best for: single-core machines, small heap (<100 MB), embedded/containerised microservices where heap is tiny

and pause length is acceptable.

• Not suitable for multi-core servers or latency-sensitive apps.

Q30. Describe Parallel GC (Throughput Collector).

• Multi-threaded version of Serial: uses N GC threads (default: # CPU cores) for Minor and Major GC.

• Still stop-the-world for all phases.

• Goal: maximise throughput (minimise GC CPU time fraction), not pause time.

• Default GC in Java 8 (-XX:+UseParallelGC).

• Flags: -XX:ParallelGCThreads=N, -XX:GCTimeRatio=99 (1% GC overhead target), -XX:MaxGCPauseMillis=200

(soft goal, may be violated).

• Best for: batch jobs, throughput-oriented apps, large heaps where pause variance is acceptable.

Q31. Describe CMS (Concurrent Mark-Sweep) GC and its phases.

CMS (deprecated Java 9, removed Java 14) was the first low-latency GC in HotSpot:

Initial Mark (STW): mark objects directly reachable from GC roots. Short pause.

Concurrent Mark: trace entire object graph concurrently with application threads.

Concurrent Preclean: identify objects modified during concurrent mark (dirty cards).

Remark (STW): re-scan modified objects. Can be long if mutation rate is high.

Concurrent Sweep: reclaim dead objects concurrently. No compaction.

Concurrent Reset: reset data structures for next cycle.

Problems with CMS: fragmentation (no compaction) leading to promotion failures; concurrent mode failure if

collection can't keep up; high CPU usage from concurrent phases.

• Enabled with -XX:+UseConcMarkSweepGC (Java 8).

Q32. Describe G1 GC (Garbage-First) in detail – default since Java 9.

G1 (default since Java 9, -XX:+UseG1GC) is a region-based, generational, mostly-concurrent collector designed to replace CMS:

Regions: heap divided into equal-sized regions (1–32 MB). Regions are dynamically assigned as Eden, Survivor,

Old, or Humongous (>region_size/2).

Remembered Sets (RSet): each region tracks incoming references from other regions. Enables collecting

individual regions without scanning the entire heap.

Collection Sets (CSet): G1 selects the regions with the most garbage (highest 'garbage ratio') to collect first –

hence 'Garbage-First'.
G1 Collection Cycle Phases:

Young-only phase: periodic Evacuation Pauses (STW) collect Eden + Survivor regions.

Concurrent Start: triggers when Old Gen occupancy exceeds InitiatingHeapOccupancyPercent (IHOP, default

45%).

Concurrent Mark (mostly concurrent): tri-colour marking.

Remark (STW): finalise liveness data.

Cleanup (STW + concurrent): reclaim fully-dead regions, sort regions by GC efficiency.

Mixed GC: collect Young + selected Old regions to achieve heap reclamation target.

# Key G1 flags:

-XX:+UseG1GC # default Java 9+

-XX:MaxGCPauseMillis=200 # target pause (default 200ms, soft goal) -XX:G1HeapRegionSize=16m # region size (1–32 MB) -XX:InitiatingHeapOccupancyPercent=45 # when to start concurrent mark -XX:G1NewSizePercent=5 # min young gen %

-XX:G1MaxNewSizePercent=60 # max young gen % -XX:G1MixedGCCountTarget=8 # mixed GC rounds per cycle -XX:G1ReservePercent=10 # heap reserve for promotion

Key Insight: G1 Full GC fallback (Java 10+) is parallel but still stop-the-world. Triggered by humongous allocation

failure or concurrent marking unable to finish.

Q33. What is ZGC and what makes it achieve sub-millisecond pauses?

ZGC (Java 11 experimental, Java 15 production, Java 21 generational) is a scalable, low-latency GC with sub-millisecond STW pauses regardless of heap size (up to 16 TB):

Coloured Pointers: ZGC stores GC metadata in 4 bits of the 64-bit object pointer (Marked0, Marked1,

Remapped, Finalizable). The JVM loads barriers check these bits.

Load Barriers: every object reference load from heap goes through a load barrier that checks if the reference is

valid (not stale after relocation). If stale, it's fixed up. This allows concurrent relocation without STW.

Concurrent Relocation: objects are moved (compacted) while the application runs. Load barriers ensure

mutators always see updated references.

No generational structure (Java 11–20): entire heap treated uniformly. This simplifies design but means more

work per cycle.

# ZGC flags:

-XX:+UseZGC # enable ZGC (Java 15+ production)

-XX:+ZGenerational # enable Generational ZGC (Java 21, default in future) -Xms4g -Xmx16g # ZGC works well with large heaps

-XX:SoftMaxHeapSize=12g # ZGC tries to keep heap below this -XX:ZCollectionInterval=2 # force GC every N seconds (0=disabled) -XX:+ZProactive # proactively GC before memory pressure

Interview Tip: ZGC trade-off: very low latency but higher memory overhead (coloured pointer metadata, relocation pages). Higher CPU usage from load barriers. Not always the best choice for CPU-bound, throughput-sensitive workloads.

Q34. What is Shenandoah GC and how does it compare to ZGC?

Shenandoah (Red Hat, Java 12+, backported to Java 8/11) is a concurrent, low-pause GC focused on consistent low latency:

Brooks Forwarding Pointers: every object has an extra word (forwarding pointer) that initially points to itself.

During concurrent evacuation, it's updated to point to the new copy.

Write barriers: Shenandoah uses write barriers (not load barriers like ZGC) to intercept mutations during

concurrent operations.

Generational Shenandoah: experimental in Java 21, planned as default in future.

Feature ZGC Shenandoah
Barrier type Load barrier (reference check) Write barrier (store intercept)
Concurrent relocation Yes (coloured pointers) Yes (forwarding pointers)
Heap size Up to 16 TB Up to tested TBs
Memory overhead Moderate (coloured ptr bits) Slightly higher (fwd pointer/obj)
Generational (Java 21) Yes (ZGenerational, default soon) Experimental
Pause target Sub-millisecond Sub-10ms typical
Best for Latency-critical, large heap Latency-critical, memory constrained
Q35. What is Epsilon GC and when would you use it?

Epsilon GC (Java 11, -XX:+UseEpsilonGC) is a no-op garbage collector. It allocates memory but never reclaims it. When the heap is exhausted, the JVM exits with OOM.

Use cases: performance testing (measure allocation rate without GC noise), ultra-short-lived processes (CLI

tools, AWS Lambda with predictable memory), memory pressure testing.

• Explicitly NOT for production long-running services.

Interview Tip: Epsilon is a great interview signal: mentioning it shows you know the full GC landscape and understand

performance testing methodology.

Q36. What is Generational ZGC (Java 21) and why is it significant?

Generational ZGC (JEP 439, GA in Java 21, -XX:+UseZGC -XX:+ZGenerational) adds generational support to ZGC, treating the weak generational hypothesis:

• Heap split into
Young Generation and Old Generation regions.

• Young Gen collected more frequently (like G1 Minor GC) with lower overhead.

• Result: ~40% lower memory overhead and ~20% higher throughput vs non-generational ZGC, while maintaining

sub-millisecond pause guarantees.

• Expected to become the default GC in a future JDK release.

• Enable with: -XX:+UseZGC -XX:+ZGenerational (Java 21).

Key Insight: From Java 21, the recommended high-performance production GC is Generational ZGC for

latency-sensitive apps (trading, APIs, real-time) and G1 for general-purpose balanced workloads.

§8 GC Tuning, Flags & Diagnostics

Q37. What are the essential JVM flags for GC tuning?

Flag Default Purpose
-Xms / -Xmx 1/4 RAM / 1/4 RAMMin/max heap size. Set equal in production to avoid resize pauses.
-Xss 512k–1m Thread stack size. Reduce for many threads.
-XX:NewRatio=N 2 Old:Young ratio. NewRatio=2 Young=1/3 of heap.
-XX:SurvivorRatio=N 8 Eden:Survivor ratio. 8 Eden=8/10 of Young.
-XX:MaxTenuringThreshold=N 15 GC cycles before promotion to Old Gen.
-XX:MaxGCPauseMillis=N 200 G1/ZGC pause target (soft goal).
-XX:+UseStringDeduplication off G1: deduplicate identical Strings (reduce heap).
-XX:+DisableExplicitGC off Ignore System.gc() calls.
-XX:+AlwaysPreTouch off Pre-allocate heap pages at JVM start (reduce latency spikes).
-XX:+UseCompressedOops on (<32GB) Use 32-bit object pointers (saves ~20% memory).
Q38. How do you enable and interpret GC logging in Java 9–21?

# Java 9+ unified logging (replaces -verbose:gc, -XX:+PrintGCDetails): -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=50m

# Minimal (pause times only): -Xlog:gc:stdout:time

# Detailed G1 logging: -Xlog:gc+heap=debug,gc+ergo=debug,gc+age=debug:file=gc.log:time,uptime

# Analyse with:

# GCEasy (gcease.io), GCViewer, or JDK Mission Control (JMC)

Key metrics to watch: pause duration (P99/P999), GC frequency, allocation rate, promotion rate, heap occupancy after GC (indicates memory pressure or leak).

Key Insight: In Java 11+, Java Flight Recorder (JFR) is the preferred low-overhead profiling tool. jcmd JFR.start

duration=60s filename=rec.jfr

Q39. How do you diagnose a memory leak in a Java application?

Step 1 – Observe symptoms: heap usage grows after Full GCs; OOM errors; increasing GC frequency; GC

overhead limit exceeded (98% time in GC).

Step 2 – Enable GC logging: verify heap is not reclaimed after Full GC.

Step 3 – Heap dump: jmap -dump:live,format=b,file=heap.hprof or -XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=/tmp/

Step 4 – Analyse dump: Eclipse MAT (dominant tree, leak suspects), VisualVM, JDK Mission Control, IntelliJ

heap analyser.

Step 5 – Common causes: static collections growing unbounded; ThreadLocal not removed; inner classes holding outer class references; classloader leak (redeploy); unclosed streams/connections; large caches without eviction (use WeakHashMap or Caffeine).

# Heap dump commands:

jmap -dump:live,format=b,file=heap.hprof $(pgrep java) jcmd GC.heap_dump /tmp/heap.hprof

# -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/ # in JVM args

# Object histogram (quick check without full dump): jmap -histo:live | head -30

jcmd GC.class_histogram | head -30

Interview Tip: In interviews, walk through the full triage: observe log dump analyse fix. Mention MAT's 'leak

suspects' report and the 'retained heap' concept.

Q40. What is GC ergonomics and how does the JVM auto-tune itself?

GC Ergonomics: since Java 5, the JVM automatically selects GC parameters based on observed behaviour to meet configured goals. For G1 and ZGC:

• Heap sizing: JVM grows/shrinks heap between -Xms and -Xmx based on occupancy after GC.

• Young Gen sizing: G1 adjusts Young Gen size to meet MaxGCPauseMillis target.

• IHOP adaptation: G1 adapts InitiatingHeapOccupancyPercent based on promotion rate.

• -XX:+UseAdaptiveSizePolicy (default on for Parallel/G1): auto-tunes NewRatio, SurvivorRatio,

MaxTenuringThreshold.

Container awareness (Java 10+): JVM detects cgroup memory and CPU limits in Docker/K8s. Sets heap to 25% of container memory limit by default. Override with -XX:MaxRAMPercentage=75.0 (common production setting).

# Recommended container/K8s JVM settings (Java 17+): -XX:+UseContainerSupport # default on Java 10+ -XX:MaxRAMPercentage=75.0 # use 75% of container memory for heap -XX:InitialRAMPercentage=50.0 # start at 50% -XX:+ExitOnOutOfMemoryError # fail fast vs limp-along -XX:+HeapDumpOnOutOfMemoryError

Q41. What is Safepoint and how does it affect GC pauses?

A safepoint is a point in execution where the JVM can safely inspect or modify the heap state. All application threads must reach a safepoint before a STW GC phase can begin.

• Common safepoint locations: backward branches of loops, method returns, certain bytecodes.

• JIT-compiled code has safepoint polls (register test instructions) at these locations.

Time-to-safepoint (TTSP): the delay from requesting a safepoint to all threads reaching one. Can add

10–100ms to perceived pause.

• Long safepoints: caused by counted loops without safepoint polls (JIT removes them for performance), JNI calls,

sleep/park operations.

• -Xlog:safepoint: log safepoint statistics.

• -XX:+SafepointTimeout -XX:SafepointTimeoutDelay=1000: warn on slow safepoints.

■■ Watch Out: A 200ms GC pause reported in logs might actually be 250ms wall-clock due to TTSP. Use -Xlog:safepoint to identify threads slow to reach safepoint.

§9 Java 17–21: Generational ZGC, Virtual Threads & ScopedValues

Q42. How do Virtual Threads (Java 21, JEP 444) interact with the JVM memory model and GC?

Virtual threads are JVM-managed lightweight threads scheduled on a pool of carrier (platform/OS) threads. Key interactions with JVM internals:

Stack storage: virtual thread stacks are heap-allocated (as StackChunk objects), not native OS memory. This

allows millions of threads without proportional native memory use.

GC impact: stack frames of blocked virtual threads are GC roots. With millions of VTs, this can increase GC root

scanning time. G1 and ZGC handle this well; profiling with JFR will show if it's an issue.

JMM unchanged: virtual threads obey all happens-before rules identically to platform threads.

Pinning: a VT is pinned (cannot unmount from carrier) inside synchronized blocks that block, and during JNI.

Java 21 reduces pinning for most I/O. Java 24 (JEP 491) removes synchronization pinning.

// Java 21 – create a million virtual threads:

try (var exec = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 1_000_000).forEach(i -> exec.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); // parks VT, frees carrier return processRequest(); })); } // executor auto-closed; waits for all tasks

Interview Tip: For new Java 21 services, replace cached thread pools with newVirtualThreadPerTaskExecutor(). Use

structured concurrency (StructuredTaskScope) for fork-join patterns.

Q43. What are ScopedValues (Java 21) and how do they improve on ThreadLocal?

Aspect ThreadLocal ScopedValue (Java 21)
Mutability Mutable (set/get/remove) Immutable within scope
Cleanup Manual remove() required Auto-cleaned on scope exit
Inheritance Manual (InheritableThreadLocal) Automatic for child VTs
Memory Per-thread hash map (leaks) Stack-like scope, no leak
Performance Hash map lookup O(1) amortised Faster (no map overhead)
Best for Legacy / mutable per-thread data Request context, user ID, MDC
static final ScopedValue CURRENT_USER = ScopedValue.newInstance(); static final ScopedValue REQUEST_ID = ScopedValue.newInstance();

// Bind for the duration of the lambda:

ScopedValue .where(CURRENT_USER, user) .where(REQUEST_ID, UUID.randomUUID().toString()) .run(() -> {

handleRequest(); // anywhere in call tree: CURRENT_USER.get() });

// Values gone after run() – no remove() needed

Q44. What is Structured Concurrency (Java 21 Preview, JEP 453)?

Structured Concurrency treats a group of concurrent tasks as a single unit of work, with a strict parent-child lifetime relationship – matching the structured programming model (if/for/while) but for threads:

• A StructuredTaskScope opens a 'scope'. Child tasks run within the scope.

• The scope owner waits for all children before proceeding (scope.join()).

• If a child fails, the scope can cancel remaining children (ShutdownOnFailure).

• Or return on first success and cancel others (ShutdownOnSuccess).

• Eliminates the problem of 'orphaned' async tasks and simplifies error propagation.

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Subtask user = scope.fork(() -> fetchUser(id));

Subtask order = scope.fork(() -> fetchOrder(id)); scope.join().throwIfFailed(); // waits; cancels both if either fails return new Response(user.get(), order.get());

} // scope closed; both tasks guaranteed done

Key Insight: Structured Concurrency + Virtual Threads + ScopedValues = Project Loom's full vision. Together they

enable synchronous-style code with async performance.

Q45. What are the key JVM improvements across Java 8 21 you must know?

Java JEP/Feature Impact on JVM/GC
8 Metaspace replaces PermGen Eliminates PermGen OOM; native memory growth
8 G1 GC (not yet default) Region-based, pause targets
9 G1 becomes default GC Better balanced throughput/latency
9 Compact Strings Latin-1 Strings use byte[] not char[] – 50% memory saving
9 JPMS (Jigsaw) Modular ClassLoader hierarchy
10 Parallel Full GC for G1 Reduces G1 Full GC pause
10 Container-aware heap sizing Reads cgroup limits in Docker/K8s
11 ZGC (experimental) Sub-millisecond pauses
12 Shenandoah GC Low-latency alternative to ZGC
13 ZGC: uncommit unused memory Returns heap to OS dynamically
14 CMS GC removed G1/ZGC are successors
15 ZGC production-ready Sub-ms at scale
15 Biased locking disabled (JEP 374) Simplification; removed Java 21
16 Elastic Metaspace (JEP 387) Returns metaspace to OS promptly
17 Unified logging improvements Better GC log tooling
21 Generational ZGC (JEP 439) Best-of-both: ZGC + generational
21 Virtual Threads GA (JEP 444) Heap-allocated stacks; GC-aware
21 ScopedValues Preview (JEP 446) Replace ThreadLocal for VTs

Quick-Reference Cheat Sheet

Heap Young (Eden+S0+S1) + Old All objects; GC'd; -Xms/-Xmx
Metaspace (Java 8+) Class metadata; native memory; -XX:MaxMetaspaceSize
JVM Stack Per-thread frames; local vars + operand stack; -Xss
Serial GC (-XX:+UseSerialGC) Single-thread; small heaps; embedded
Parallel GC (Java 8 default) Multi-thread STW; throughput-oriented
CMS (removed Java 14) Concurrent sweep; no compaction; fragmentation risk
G1 GC (Java 9+ default) Region-based; pause targets; balanced; -XX:MaxGCPauseMillis
ZGC (Java 15+ production) Sub-ms pauses; coloured pointers; load barriers; 16TB heap
Generational ZGC (Java 21) -XX:+UseZGC -XX:+ZGenerational; best latency+throughput
Shenandoah Concurrent relocation; forwarding pointers; Red Hat
Epsilon GC No-op GC; testing/short-lived processes only
TLAB Per-thread Eden slice; lock-free bump-ptr allocation
Compressed OOPs 32-bit refs on 64-bit JVM (<32GB heap); saves ~20% memory
Escape Analysis + Stack Alloc JIT: non-escaping objects on stack; no GC pressure
Safepoint All threads paused; STW GC; watch TTSP in logs
volatile Visibility + ordering; NOT mutual exclusion or compound ops
synchronized Mutex + visibility; unlock HB lock; intrinsic monitor
Happens-Before (8 rules) Program order, monitor, volatile, start, join, interrupt, finalizer, transitivity
VarHandle (Java 9) Type-safe acquire/release/volatile access; replaces Unsafe
Virtual Threads (Java 21) Heap stacks; park not block; millions of threads
GC Selector: GC Selection Guide (Java 21): Latency-critical (APIs, trading) Generational ZGC. General-purpose (microservices, web) G1 (default). Batch/throughput Parallel GC. Testing/ephemeral Epsilon.

Memory-constrained low-latency
Shenandoah.

Ashok Koritala · JVM Internals, Memory Model & GC Interview Guide · Java 8–21 · Confidential