DeepReach
Java Core20 interview questions

JVM Evolution: From Java 1.0 to Java 25

A 30-year deep dive into JVM architecture, memory model, garbage collection evolution, and modern Java features — from Write Once Run Anywhere to sub-millisecond ZGC and Virtual Threads.

JVMGCMemoryVirtual ThreadsJITJava 21

JVM Evolution: From Java 1.0 to Java 25

JVM Architecture: The 5 Core Components

The JVM has 5 main components that work together to execute your Java code:

  1. 1.Class Loader Subsystem — Loads .class files through Loading → Linking → Initialization
  2. 2.Runtime Data Areas — Memory structures: Heap, Stack, Metaspace, PC Register, Native Method Stack
  3. 3.Execution Engine — Executes bytecode via Interpreter + JIT Compiler + Garbage Collector
  4. 4.Native Method Interface (JNI) — Bridge between Java and native C/C++ code
  5. 5.Native Method Libraries — Platform-specific C/C++ libraries used by the Execution Engine

Class Loading Mechanism

When you run a Java program, the ClassLoader goes through three phases:

java
public class Employee {
    private static int employeeCount = 0;  // Static variable

    static {
        System.out.println("Static block executed");
        employeeCount = 100;  // Runs during INITIALIZATION phase
    }

    private String name;
    private int id;

    public Employee(String name) {
        this.name = name;
        this.id = ++employeeCount;
    }
}

/* CLASS LOADING PHASES:
   1. LOADING     — ClassLoader reads the .class file into Method Area
   2. LINKING
      2a. Verification — Check bytecode is valid (magic: 0xCAFEBABE)
      2b. Preparation  — Allocate memory for static vars; set defaults (0, null)
      2c. Resolution   — Replace symbolic refs with direct memory addresses
   3. INITIALIZATION — Run static blocks; set static vars to actual values
*/

ClassLoader delegation model: Application CL → Extension CL → Bootstrap CL. Child asks parent first, preventing malicious code from overriding core classes.

How Objects Are Stored in Memory

Every object on the heap has hidden overhead beyond its fields:

OBJECT HEADER (12-16 bytes on 64-bit JVM):
  Mark Word  (8 bytes) — hashcode, GC age, lock status
  Class Ptr  (4 bytes) — points to metadata in Metaspace

INSTANCE DATA:
  Field 1: String name   (8 bytes — reference)
  Field 2: int id        (4 bytes — primitive)
  Field 3: double salary (8 bytes — primitive)
  Padding                (0-7 bytes to align to 8-byte boundary)

Employee("John", 101, 50000.0) = 40 bytes on heap
String "John" object = another ~72 bytes
Total: 112 bytes for what looks like 3 fields!

PC Register & Native Method Stack

Each thread has its own PC Register (Program Counter) tracking the current bytecode instruction address. When a native method is called (e.g., System.currentTimeMillis()), execution switches to the Native Method Stack and the PC becomes undefined until Java code resumes.

java
public static void main(String[] args) {
    int a = 1;        // PC: 0x0000
    int b = 2;        // PC: 0x0001
    int c = a + b;    // PC: 0x0002

    long time = System.currentTimeMillis(); // PC becomes undefined
    // C++ implementation runs on Native Method Stack
    // Returns to PC: 0x0004 after native method returns
}

Execution Engine: Interpreter vs JIT

The Execution Engine has three components working together:

java
public class JITExample {
    public static void main(String[] args) {
        // Calls 1-100: INTERPRETED (slow, ~1x)
        // Calls 101-1000: Still interpreted + JVM profiling
        // Calls 1001-5000: C1 Compiled, basic optimization (~5x)
        // Calls 5001+: C2 Compiled, aggressive optimization (~10-100x)
        for (int i = 0; i < 100_000; i++) {
            calculateSum(100);
        }
    }

    public static int calculateSum(int n) {
        int sum = 0;
        for (int i = 1; i <= n; i++) sum += i;
        return sum;
    }
}

/* Tiered Compilation Levels:
   Level 0: Interpreter
   Level 1: C1 — no profiling
   Level 2: C1 — basic profiling
   Level 3: C1 — full profiling
   Level 4: C2 — fully optimized (most common final destination)

   JIT optimizations: inlining, loop unrolling, dead code elimination,
   escape analysis, scalar replacement, speculative optimizations
*/

Compiled native code lives in the Code Cache (default ~240MB). If it fills up, the JVM falls back to interpretation and performance degrades. Increase with -XX:ReservedCodeCacheSize=256m.

Java 1.0 vs Modern JVM: 30 Years of Evolution

FeatureJava 1.0 (1996)Modern JVM (Java 25)
ExecutionPure interpreterTiered JIT (0→3→4)
GC Pause2–5 seconds (STW)<1ms (ZGC)
Max Heap~512MB practical16TB+
ThreadsGreen threadsMillions of virtual threads
Startup2–3 seconds0.1s with CDS
Throughput1x baseline~1000x faster
Method AreaPermGen (fixed size)Metaspace (dynamic, native)

Part 1: The Early Days (Java 1.0 – 1.4)

Java 1.0 (1996): Write Once, Run Anywhere

Java introduced a virtual machine that abstracts the OS: compile once to bytecode, run on any platform's JVM. The original JVM was a pure interpreter — it read and executed bytecode line by line every time, like translating a recipe from scratch each time you cook.

Java source (.java) → javac → Bytecode (.class) → JVM → Native machine code
                                      │
                                      ├── Windows JVM → Windows native
                                      ├── Mac JVM     → Mac native
                                      └── Linux JVM   → Linux native

Java Memory Model: Heap, Stack, Metaspace

java
public class MemoryExample {
    private static int counter = 0;   // Metaspace (static)
    private String name;               // Heap (instance field)

    public void processData() {
        int localNumber = 42;          // Stack (local primitive)
        StringBuilder sb = new StringBuilder(); // Stack ref → Heap object

        String result = "Hello " + name; // New String object on Heap
        counter++;                        // Access Metaspace
    }
}

Garbage Collection: Mark & Sweep (Java 1.0)

The original GC was brutally simple — and brutally slow:

java
// This innocent loop caused 500ms–2s freezes
for (int i = 0; i < 1_000_000; i++) {
    String data = "Data " + i; // Creates a new String each iteration
    process(data);
    // All these strings need cleanup!
}

/* MARK & SWEEP PHASES (everything stops!):
   1. STOP THE WORLD — application completely freezes
   2. MARK — trace from roots; mark all reachable objects as alive
   3. SWEEP — free memory for unmarked objects
   4. Resume application
   Pause time: 500ms–2 seconds!
*/

Generational GC Revolution (Java 1.3 HotSpot)

The Generational Hypothesis: 95% of objects die young. Solution: separate the heap.

java
public void processRequest() {
    // SHORT-LIVED objects (stay in Young Gen — cleaned quickly)
    String requestId = UUID.randomUUID().toString();
    LocalDateTime timestamp = LocalDateTime.now();
    Map<String, String> headers = new HashMap<>();
    // 95% of objects are like these!
}

public class Application {
    // LONG-LIVED objects (promoted to Old Gen after ~8 GCs)
    private static final Logger logger = Logger.getLogger();
    private static final DataSource dataSource = createDataSource();
}

/* OBJECT LIFECYCLE:
   Birth       → Eden Space (Young Gen)
   Survive 1   → Survivor S0 (age = 1)
   Survive 2   → Survivor S1 (age = 2)
   ...
   Survive 8   → Old Generation (PROMOTED!)
   Minor GC on Young Gen: 10–50ms pause (fast!)
   Major GC on Old Gen: rare, slower but infrequent
*/

Part 2: The Maturation (Java 5 – 8)

Java 5: Generics & Autoboxing Pitfalls

Generics added compile-time type safety. But autoboxing introduced a hidden memory trap:

java
// DANGEROUS: Creates 1 million Integer wrapper objects!
Integer sum = 0;
for (int i = 0; i < 1_000_000; i++) {
    sum += i; // Unbox sum → add i → box result → discard old Integer
    // 1 million Integer objects created and immediately garbage
}

// CORRECT: No objects, just arithmetic
int sum = 0;
for (int i = 0; i < 1_000_000; i++) {
    sum += i; // Pure CPU math
}
// 100x faster, ~60 MB less memory pressure!

// Same trap with collections
List<Integer> slow = new ArrayList<>();  // ~60-80 MB for 1M ints
int[] fast = new int[1_000_000];         // 4 MB — 15-20x more efficient

Java 6: Escape Analysis

The JVM now asks: "Does this object escape the method?" If not, it can allocate on the stack (or eliminate the object entirely):

java
public class EscapeAnalysis {

    // Object ESCAPES — must go on heap (needs GC)
    public Point createPoint() {
        return new Point(10, 20); // Caller uses it → heap allocation
    }

    // Object DOES NOT escape — stack allocated, auto-freed!
    public void processPoint() {
        Point p = new Point(10, 20);
        int distance = p.x + p.y;
        System.out.println(distance);
        // 'p' never leaves — JVM may skip heap allocation entirely
        // "Scalar replacement": JVM rewrites it as: int x=10; int y=20;
        // No object created at all!
    }
}

class Point { int x, y; Point(int x, int y) { this.x = x; this.y = y; } }

String Pool & Interning

Strings are special: literal strings share a single pooled instance.

java
String s1 = "Hello";          // Goes into String Pool
String s2 = "Hello";          // Reuses same pooled object
System.out.println(s1 == s2); // true — same reference!

String s3 = new String("Hello"); // Forces new heap object
System.out.println(s1 == s3);    // false

String s4 = s3.intern();      // Returns pooled version
System.out.println(s1 == s4); // true

// Memory impact: 100,000 duplicate string literals
// Without pool: 100,000 × ~42 bytes = 4.2 MB
// With pool:    1 object + 100,000 × 8-byte refs = ~0.8 MB

Java 8: Lambdas & Streams

Lambdas use invokedynamic — no extra .class file, lower memory overhead than anonymous inner classes. Streams add lazy evaluation for memory efficiency:

java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

// Eager (traditional): creates intermediate lists
List<String> filtered = new ArrayList<>();
for (String n : names) { if (n.startsWith("A")) filtered.add(n); }

// Lazy (streams): no intermediate collections
List<String> result = names.stream()
    .filter(n -> n.startsWith("A"))  // Doesn't execute yet
    .map(String::toUpperCase)         // Doesn't execute yet
    .collect(Collectors.toList());    // NOW executes everything, one element at a time

Part 3: Garbage Collection Deep Dive

Object Lifecycle Through GC

java
public class ObjectLifecycleDemo {
    public static void main(String[] args) {
        List<Data> survivors = new ArrayList<>();

        for (int iter = 0; iter < 100; iter++) {
            for (int i = 0; i < 1000; i++) {
                Data temp = new Data(iter, i);
                if (i % 100 == 0) survivors.add(temp); // 10 survive
                // 990 die immediately — garbage in Eden
            }
            // Minor GC: 990 Eden objects cleaned (fast!)
            // 10 survivors move to Survivor space, age++
            // After ~8 Minor GCs → promoted to Old Gen
        }
    }
}

G1 GC: The Default Collector (Java 9+)

G1 divides the heap into ~2048 equal-sized regions (1–32MB each). "Garbage First" = always collect the region with the most garbage first.

java
// Run with: java -XX:+UseG1GC -Xlog:gc* -Xmx4g MyApp
// Sample output:
// [GC pause (G1 Evacuation Pause) (young)] 52ms
// [GC pause (G1 Evacuation Pause) (young)] 48ms
// [GC pause (G1 Evacuation Pause) (mixed)] 95ms
// Consistent, predictable pauses — application keeps running!
bash
# G1 configuration
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xmx4g MyApp

# With GC logging
java -XX:+UseG1GC -Xlog:gc*:file=gc.log -Xmx4g MyApp

ZGC: Sub-Millisecond Pauses (Java 15+ Production)

ZGC uses colored pointers — 4 metadata bits in each 64-bit reference encode GC state. Almost all GC work runs concurrently with the application.

java
// With G1 on 16GB heap: pauses 200–500ms
// With ZGC on 16GB heap: pauses <1ms

// ZGC pause sample:
// [Pause Mark Start]    0.123ms
// [Pause Mark End]      0.089ms
// [Pause Relocate Start] 0.156ms
// 100–300x better than G1!
bash
# Enable ZGC
java -XX:+UseZGC -Xmx16g MyApp

# Generational ZGC (Java 21+, best of both worlds)
java -XX:+UseZGC -XX:+ZGenerational -Xmx16g MyApp

Common Memory Leaks & Prevention

java
// LEAK #1: Unbounded static collection
private static Map<String, Object> cache = new HashMap<>();
// Fix: use bounded cache with eviction
private static final Cache<String, Object> cache = CacheBuilder.newBuilder()
    .maximumSize(10_000).expireAfterAccess(10, TimeUnit.MINUTES).build();

// LEAK #2: Unclosed resources
// Fix: always use try-with-resources
try (InputStream is = new FileInputStream("file.txt")) {
    processData(is);
} // Automatically closed even on exception

// LEAK #3: Non-static inner class holds outer reference
public static class StaticInner { /* no implicit outer ref */ }

// LEAK #4: ThreadLocal in thread pools — always remove!
try {
    threadData.get().add(new byte[1024 * 1024]);
    // process...
} finally {
    threadData.remove(); // Critical!
}
bash
# Heap dump on OOM for post-mortem analysis
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof MyApp

# Manual heap dump from running JVM
jmap -dump:live,format=b,file=heap.bin <pid>
# Analyze with Eclipse MAT or VisualVM

Part 4: Modern Java (17, 21, 25)

Java 21: Virtual Threads Revolution

Virtual Threads (Project Loom) are the biggest concurrency change in Java's history:

java
// OLD: Platform threads — 1MB each, limited to ~10K threads
ExecutorService executor = Executors.newFixedThreadPool(1000);
// 1000 threads = 1GB just for stacks; 9000 tasks wait in queue

// NEW: Virtual threads — ~1KB each, millions possible!
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000_000; i++) {
        final int taskId = i;
        executor.submit(() -> performIOTask(taskId));
    }
} // Processes all 1 million tasks!

// Virtual thread per request — simple blocking code that scales
ServerSocket server = new ServerSocket(8080);
while (true) {
    Socket client = server.accept();
    Thread.startVirtualThread(() -> handleRequest(client));
    // Handles millions of concurrent connections!
}
Platform ThreadsVirtual Threads
Memory per thread~1 MB~1 KB
Max concurrent~10,000Millions
Context switchExpensive (OS)Cheap (JVM)
Best forCPU-intensive workI/O-bound work
Available sinceAlwaysJava 21 (stable)

Java Version Recommendations (2025)

  • Java 8/11: Legacy — upgrade when possible
  • Java 17: Current production default (LTS), excellent stability
  • Java 21: Best choice for new projects — Virtual Threads, Sequenced Collections, Record Patterns (LTS)
  • Java 25: Cutting edge — wait for 25.0.1+ for production

Optimization Quick Wins

java
// 1. Use primitives for large numeric arrays (15-20x less memory)
int[] fast = new int[1_000_000];        // 4 MB
List<Integer> slow = new ArrayList<>();  // ~60-80 MB

// 2. StringBuilder for string concatenation in loops (1000x faster)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) sb.append("Item ").append(i);
String result = sb.toString();

// 3. Always try-with-resources for I/O
try (InputStream is = new FileInputStream("file.txt")) { /*...*/ }

// 4. Help GC by clearing collections when done
List<LargeObject> temp = buildList();
processAll(temp);
temp.clear(); // GC knows you're done
temp = null;

// 5. Hoist constants out of loops
String constant = "CONSTANT"; // ONE object
for (int i = 0; i < 1_000_000; i++) { use(constant); } // NOT in loop!

// 6. Avoid autoboxing in hot paths — use int, long, double not Integer, Long, Double

Essential JVM Tuning Parameters

bash
# Set -Xms = -Xmx in production to avoid runtime heap resizing
java -Xms8g -Xmx8g MyApp

# GC selection
-XX:+UseG1GC           # Default (Java 9+); good for 4–64GB heaps
-XX:+UseZGC            # Ultra-low latency; great for 8GB–16TB
-XX:+UseParallelGC     # Maximum throughput for batch jobs

# G1 tuning
-XX:MaxGCPauseMillis=200      # Pause time target
-XX:G1HeapRegionSize=4m       # Region size

# Generational ZGC (Java 21+)
-XX:+UseZGC -XX:+ZGenerational

# Container-aware memory (Docker/Kubernetes)
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0

# GC logging & diagnostics
-Xlog:gc*:file=gc-%t.log
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/heap.hprof

# Production template
java -Xms8g -Xmx8g \
     -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
     -Xlog:gc*:file=gc.log \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/heap.hprof \
     -jar myapp.jar

Interview Questions

  1. 1.What are the 5 core components of the JVM and what does each do?
  2. 2.Explain the three phases of class loading. When does a static block run?
  3. 3.Why does every Java object have at least 12–16 bytes of overhead?
  4. 4.What is the PC Register and when does it become undefined?
  5. 5.What is tiered JIT compilation? What are the 5 levels?
  6. 6.How did Java 1.0's interpreter differ from a modern JIT-compiled JVM?
  7. 7.What is the Generational Hypothesis and how does it shape heap layout?
  8. 8.Walk through an object's lifecycle from Eden to Old Generation.
  9. 9.Why did the original Mark & Sweep GC cause 2-second application pauses?
  10. 10.What is Metaspace and why did it replace PermGen in Java 8?
  11. 11.Why is autoboxing in a tight loop dangerous? How do you fix it?
  12. 12.Explain escape analysis. What is scalar replacement?
  13. 13.How does the String Pool save memory? When should you call intern()?
  14. 14.Why are lambdas more memory-efficient than anonymous inner classes?
  15. 15.How does G1 GC differ from the original generational GC?
  16. 16.How does ZGC achieve sub-millisecond pauses using colored pointers?
  17. 17.Name four common Java memory leak patterns and their fixes.
  18. 18.When would you choose ZGC over G1 GC?
  19. 19.What are Virtual Threads? How do they differ from platform threads in memory cost?
  20. 20.What JVM flags would you set for a latency-sensitive production service?