Java Advanced · Ay 1

JVM Architecture

Goal: understand how the JVM loads, stores and executes your code. Duration: 120 min

1. Big picture

.java ──javac──▶ .class (bytecode) ──▶ JVM
                                        ├── Class Loader subsystem
                                        ├── Runtime data areas (memory)
                                        └── Execution engine (interpreter + JIT + GC)

2. Class loading

Classes are loaded lazily: the first time they are used.

Class loader Loads
Bootstrap core JDK (java.lang.*)
Platform other JDK modules
Application your classpath (your code, libraries)

Phases: Loading → Linking (verify, prepare, resolve) → Initialization (static blocks run).

public class Config {
    static {
        System.out.println("Config class initialized");
    }
    static final String NAME = "app";
}

The static block runs once, when Config is first used.

Delegation model: a loader first asks its parent. That's why you cannot replace java.lang.String with your own class.

Errors you will meet:

  • ClassNotFoundException: loading by name failed (e.g. missing JDBC driver).
  • NoClassDefFoundError: class was present at compile time, missing at runtime (dependency conflict).

3. Runtime memory areas

Area Per Contains
Heap JVM all objects and arrays (GC managed)
Stack thread frames: local variables, method calls
Metaspace JVM class metadata (replaced PermGen)
PC register thread current instruction
Native stack thread native (C) calls
void transfer() {
    int amount = 100;                      // stack (primitive)
    Account acc = new Account("AZ01");     // reference on stack, object on heap
}
  • Deep recursion → StackOverflowError (stack full).
  • Too many live objects → OutOfMemoryError: Java heap space.

4. Bytecode

public int add(int a, int b) { return a + b; }
javac Calc.java
javap -c Calc
public int add(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: iadd
     3: ireturn

The JVM is a stack machine: it pushes values and applies operations.

5. Execution engine: interpreter + JIT

  1. Bytecode is first interpreted (slow but starts fast).
  2. "Hot" methods (called often) are compiled to native code by the JIT compiler (C1 quick, C2 optimizing).
  3. JIT optimizations: inlining, dead-code elimination, escape analysis, loop unrolling.

That's why Java apps "warm up": performance improves after some time. Never benchmark the first seconds; use JMH for benchmarks.

6. Useful JVM flags

java -Xms512m -Xmx2g -jar app.jar          # initial / max heap
java -XX:+PrintFlagsFinal -version | grep HeapSize
java -Xss1m                                  # thread stack size
java -XX:MaxRAMPercentage=75 -jar app.jar   # containers (Docker/K8s)

7. Hands-on

  1. Write a class with a static block and observe when it runs.
  2. Cause a StackOverflowError with recursion; change -Xss and compare depth.
  3. Disassemble a small class with javap -c and explain each instruction.
  4. Print runtime info:
Runtime rt = Runtime.getRuntime();
System.out.printf("Max heap: %d MB, CPUs: %d%n", rt.maxMemory() / 1_048_576, rt.availableProcessors());

8. Interview corner

  • Difference between heap and stack?
  • What is the JIT compiler?
  • ClassNotFoundException vs NoClassDefFoundError?
  • Where are static variables stored? (in the heap, with the class's Class object, since Java 8)

9. Homework

Write a one-page explanation (README in your repo) "What happens when I run java -jar app.jar" from class loading to JIT, with a diagram.

Self-check answers
  • Heap: shared, objects, GC; stack: per thread, frames, locals, freed automatically when methods return.
  • JIT compiles frequently executed bytecode to optimized native code at runtime.
  • CNFE: explicit loading by name failed; NCDFE: class existed at compile time but is missing/failed to initialize at runtime.