JVM Internals

How Java code goes from source to execution β€” and what the JVM does at every step

← Back to Index

JDK, JRE, JVM β€” What Each One Is

These three terms are used interchangeably in conversation but refer to very different things. Confusing them in an interview is a red flag.

/*
 *  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 *  β”‚  JDK  (Java Development Kit)                                β”‚
 *  β”‚                                                             β”‚
 *  β”‚  Everything you need to WRITE and BUILD Java programs.      β”‚
 *  β”‚  Install this on your development machine.                  β”‚
 *  β”‚                                                             β”‚
 *  β”‚  Includes:                                                  β”‚
 *  β”‚  β€’ javac    β€” the compiler (.java β†’ .class)                β”‚
 *  β”‚  β€’ java     β€” the launcher (runs .class files)             β”‚
 *  β”‚  β€’ jar      β€” packages .class files into .jar archives     β”‚
 *  β”‚  β€’ javadoc  β€” generates HTML docs from Javadoc comments    β”‚
 *  β”‚  β€’ jshell   β€” REPL for interactive Java (Java 9+)          β”‚
 *  β”‚  β€’ jconsole / jvisualvm β€” profiling and monitoring tools   β”‚
 *  β”‚  β€’ JRE  (see below)                                        β”‚
 *  β”‚                                                             β”‚
 *  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
 *  β”‚  β”‚  JRE  (Java Runtime Environment)                    β”‚   β”‚
 *  β”‚  β”‚                                                     β”‚   β”‚
 *  β”‚  β”‚  Everything you need to RUN a compiled Java program.β”‚   β”‚
 *  β”‚  β”‚  Install this on production servers / end-user PCs. β”‚   β”‚
 *  β”‚  β”‚  (Since Java 11, JRE is no longer distributed       β”‚   β”‚
 *  β”‚  β”‚   separately β€” you install the JDK everywhere.)     β”‚   β”‚
 *  β”‚  β”‚                                                     β”‚   β”‚
 *  β”‚  β”‚  Includes:                                          β”‚   β”‚
 *  β”‚  β”‚  β€’ Standard library (java.lang, java.util, …)      β”‚   β”‚
 *  β”‚  β”‚  β€’ JVM  (see below)                                β”‚   β”‚
 *  β”‚  β”‚                                                     β”‚   β”‚
 *  β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚   β”‚
 *  β”‚  β”‚  β”‚  JVM  (Java Virtual Machine)                  β”‚ β”‚   β”‚
 *  β”‚  β”‚  β”‚                                               β”‚ β”‚   β”‚
 *  β”‚  β”‚  β”‚  The engine that actually EXECUTES bytecode.  β”‚ β”‚   β”‚
 *  β”‚  β”‚  β”‚  Platform-specific: there's a Windows JVM,    β”‚ β”‚   β”‚
 *  β”‚  β”‚  β”‚  a Linux JVM, a macOS JVM β€” each translates   β”‚ β”‚   β”‚
 *  β”‚  β”‚  β”‚  the same bytecode to native instructions for β”‚ β”‚   β”‚
 *  β”‚  β”‚  β”‚  that OS and CPU.                             β”‚ β”‚   β”‚
 *  β”‚  β”‚  β”‚                                               β”‚ β”‚   β”‚
 *  β”‚  β”‚  β”‚  The JVM is a specification (JVMS).           β”‚ β”‚   β”‚
 *  β”‚  β”‚  β”‚  HotSpot (Oracle/OpenJDK) is its main         β”‚ β”‚   β”‚
 *  β”‚  β”‚  β”‚  implementation. Others: GraalVM, OpenJ9.     β”‚ β”‚   β”‚
 *  β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚   β”‚
 *  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
 *  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 */
Component You need it to… Install on…
JDK Write, compile, run, debug Java code Developer machines, CI/CD servers
JRE Run pre-compiled Java programs Production (Java 8/11 era β€” now just install JDK)
JVM Execute bytecode (part of JRE/JDK) Always bundled β€” not installed separately
JVM is a specification, not a single program

The Java Virtual Machine Specification defines the instruction set, memory model, and behaviour. HotSpot (OpenJDK/Oracle) is the reference implementation β€” what virtually everyone runs. But GraalVM is another full JVM implementation that also supports ahead-of-time compilation to native binaries. OpenJ9 (Eclipse/IBM) is optimised for lower memory footprint in containers. All of them run the same bytecode.

From Source Code to Execution β€” The Full Journey

What actually happens from the moment you type java MyApp to the first instruction executing. Every step below happens in order, every time.

/*
 *  STEP 1: You write source code
 *  ─────────────────────────────────────────────────────────────
 *  MyApp.java  (human-readable text file)
 *
 *
 *  STEP 2: javac compiles to bytecode
 *  ─────────────────────────────────────────────────────────────
 *  $ javac MyApp.java
 *
 *  javac reads MyApp.java, checks types, resolves imports,
 *  and produces MyApp.class β€” a binary file containing bytecode:
 *  platform-independent instructions for the JVM.
 *
 *  Bytecode is NOT machine code. A CPU can't execute it directly.
 *  It's an intermediate representation designed for the JVM.
 *
 *
 *  STEP 3: java launcher starts the JVM process
 *  ─────────────────────────────────────────────────────────────
 *  $ java MyApp
 *
 *  The OS creates a new process. The JVM executable loads into RAM.
 *  The JVM initialises its internal memory areas (see next section).
 *  The main thread is created and its stack is allocated.
 *
 *
 *  STEP 4: Class Loader loads MyApp.class
 *  ─────────────────────────────────────────────────────────────
 *  The JVM needs MyApp before it can run main().
 *
 *  1. Bootstrap ClassLoader loads java.lang.* (String, Object…)
 *     from the JDK's core modules β€” happens first, always.
 *  2. Application ClassLoader searches the classpath for MyApp.class.
 *  3. Bytecode is read from disk into the Method Area (Metaspace).
 *  4. Bytecode verifier checks: is this valid and safe bytecode?
 *     Malformed bytecode β†’ VerifyError (JVM won't run it).
 *  5. Static fields allocated and set to defaults (0, null, false).
 *  6. Static blocks and field initialisers run (class initialisation).
 *
 *
 *  STEP 5: main() is located and a stack frame is pushed
 *  ─────────────────────────────────────────────────────────────
 *  JVM finds: public static void main(String[] args)
 *  Creates a stack frame on the main thread's stack.
 *  The frame holds: local variables, operand stack, reference to
 *  the constant pool, and the program counter (current instruction).
 *
 *
 *  STEP 6: Execution β€” interpreter and JIT
 *  ─────────────────────────────────────────────────────────────
 *  The Execution Engine runs bytecode:
 *
 *  Initially: the interpreter executes bytecode instructions
 *  one by one β€” simple, always correct, but slow.
 *
 *  Simultaneously: the JIT profiler counts how many times each
 *  method is called. Methods called > threshold (C1: ~2000,
 *  C2: ~10000 in HotSpot) are compiled to native machine code
 *  and cached. Subsequent calls run at native speed.
 *
 *
 *  STEP 7: GC runs in background
 *  ─────────────────────────────────────────────────────────────
 *  As objects are created on the heap and references are dropped,
 *  the Garbage Collector identifies and reclaims unreachable objects.
 *  GC runs on its own threads, pausing application threads as needed.
 *
 *
 *  STEP 8: JVM shutdown
 *  ─────────────────────────────────────────────────────────────
 *  When main() returns (and no non-daemon threads remain),
 *  the JVM runs shutdown hooks (registered via Runtime.addShutdownHook),
 *  finalises resources, and exits β€” returning control to the OS.
 */

Bytecode β€” what it looks like

// Java source
public static int add(int a, int b) { return a + b; }

// Corresponding bytecode (javap -c MyClass)
// public static int add(int, int);
//   Code:
//     0: iload_0    ← push local variable 0 (a) onto operand stack
//     1: iload_1    ← push local variable 1 (b) onto operand stack
//     2: iadd       ← pop two ints, push their sum
//     3: ireturn    ← return top of operand stack as int

// View bytecode yourself:
javap -c MyClass.class     // bytecode instructions
javap -verbose MyClass.class // full detail: constant pool, attributes

JVM Memory Areas

/*
 *  JVM MEMORY LAYOUT
 *  ═══════════════════════════════════════════════════════════════
 *
 *  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 *  β”‚  METASPACE  (native memory β€” not in heap)                   β”‚
 *  β”‚  Class metadata, method bytecode, constant pools,           β”‚
 *  β”‚  static variables, vtables.                                 β”‚
 *  β”‚  No fixed limit by default (can grow to exhaust native RAM).β”‚
 *  β”‚  -XX:MaxMetaspaceSize=256m to cap it.                       β”‚
 *  β”‚  Replaced PermGen (Java 7 and earlier).                     β”‚
 *  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 *
 *  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 *  β”‚  HEAP  (GC-managed, shared by all threads)                  β”‚
 *  β”‚                                                             β”‚
 *  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
 *  β”‚  β”‚  YOUNG GENERATION     β”‚  OLD GENERATION (Tenured)   β”‚   β”‚
 *  β”‚  β”‚                       β”‚                             β”‚   β”‚
 *  β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β” β”‚  Long-lived objects         β”‚   β”‚
 *  β”‚  β”‚  β”‚ Eden β”‚ S0   β”‚ S1 β”‚ β”‚  that survived many GCs     β”‚   β”‚
 *  β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”˜ β”‚                             β”‚   β”‚
 *  β”‚  β”‚  New objects born hereβ”‚                             β”‚   β”‚
 *  β”‚  β”‚  Minor GC runs here   β”‚  Major/Full GC runs here    β”‚   β”‚
 *  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
 *  β”‚                                                             β”‚
 *  β”‚  -Xms = initial heap size    -Xmx = max heap size          β”‚
 *  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 *
 *  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  (one per thread)
 *  β”‚  THREAD STACK                β”‚
 *  β”‚  Stack frames (one per call) β”‚
 *  β”‚  Local variables             β”‚
 *  β”‚  Operand stack               β”‚
 *  β”‚  Return address              β”‚
 *  β”‚  -Xss = stack size per threadβ”‚
 *  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 *
 *  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
 *  β”‚  CODE CACHE  (native memory)                               β”‚
 *  β”‚  JIT-compiled native code β€” kept here for fast reuse       β”‚
 *  β”‚  -XX:ReservedCodeCacheSize=256m to size it                 β”‚
 *  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 */

The call stack in detail

// Each method call creates a new stack frame.
// Each frame contains: local variable table, operand stack, constant pool ref, PC.

public static void main(String[] args) {   // frame 1 pushed
    int x = calculate(10);                    // calls calculate β†’ frame 2 pushed
    System.out.println(x);
}
static int calculate(int n) {               // frame 2
    return multiply(n, 2);                    // calls multiply β†’ frame 3 pushed
}
static int multiply(int a, int b) {          // frame 3 β€” top of stack
    return a * b;                              // returns β†’ frame 3 popped
}                                              // frame 2 resumes β†’ popped
                                               // frame 1 resumes, prints, exits

/*
 *  Stack overflow = too many nested calls (usually infinite recursion):
 *  java.lang.StackOverflowError
 *
 *  Each frame costs memory. Default stack is 512KB–1MB.
 *  Deep recursion (e.g. tree traversal on large graphs) may need: -Xss4m
 *  Better: convert to iteration with an explicit Deque<> stack.
 */

Heap generational layout β€” why it matters

/*
 *  The generational hypothesis: most objects die young.
 *  Java exploits this by dividing the heap into generations.
 *
 *  Object lifecycle:
 *
 *  1. new Person()  β†’  allocated in Eden (Young Gen)
 *
 *  2. Minor GC fires (Young Gen full):
 *     - Live objects copied to Survivor space (S0 or S1)
 *     - Dead objects reclaimed immediately (no scanning Old Gen)
 *     - Very fast: typically < 100ms
 *
 *  3. Objects that survive N minor GCs (default: 15)
 *     are promoted to Old Gen (Tenured).
 *
 *  4. Major/Full GC fires (Old Gen nearly full):
 *     - Scans entire heap
 *     - Much slower: can be seconds for large heaps
 *     - Stop-the-world pauses hurt latency
 *
 *  5. Modern GCs (G1, ZGC, Shenandoah) reduce stop-the-world
 *     by doing most work concurrently with application threads.
 */

// Key JVM flags for heap tuning
-Xms512m                      // initial heap size
-Xmx4g                        // max heap size (set equal to Xms to avoid resize)
-XX:NewRatio=3                 // Old:Young ratio (3 = 75% old, 25% young)
-XX:MaxRAMPercentage=75.0      // use 75% of container RAM (critical in Kubernetes)
-XX:+UseZGC                    // use ZGC (sub-ms pauses, Java 15+)

Class Loading

Classes are loaded lazily β€” the JVM loads a class the first time something needs it, not at startup. Class loading happens in three phases.

/*
 *  PHASE 1: LOADING
 *  Find the .class file (on classpath, in a JAR, from network, from DB…),
 *  read the bytes, and create a java.lang.Class object in Metaspace.
 *
 *  PHASE 2: LINKING
 *  (a) Verification: bytecode verifier checks structural validity.
 *      Is the constant pool consistent? Do type references resolve?
 *      Is there any stack under/overflow? β†’ VerifyError if not.
 *
 *  (b) Preparation: allocate storage for static fields in Metaspace
 *      and set them to their JVM defaults (0, null, false).
 *      NOT the initialiser values yet.
 *
 *  (c) Resolution: replace symbolic references (class names, method names)
 *      in the constant pool with direct memory references.
 *      Can be lazy (deferred until first use) or eager.
 *
 *  PHASE 3: INITIALISATION
 *  Run the class initialiser: static field initialisers and static blocks,
 *  in textual order. Only one thread initialises a class β€” the JVM
 *  guarantees this and uses a lock on the Class object.
 */

class Config {
    static int    timeout = 30;                // Prep: 0 β†’ Init: 30
    static String host;                          // Prep: null

    static {
        host = System.getenv("HOST");          // Init: read env var
        if (host == null) host = "localhost";   // default
    }
}
// Config is initialised ONCE, on first reference, thread-safely.
// All subsequent accesses see the fully initialised state.

ClassLoader hierarchy and parent delegation

/*
 *  Bootstrap ClassLoader  (C++, part of JVM itself)
 *  β”‚  Loads: java.lang.*, java.util.*, etc. from the JDK modules
 *  β”‚  Has no parent. Returns null when asked for its parent.
 *  β”‚
 *  β”œβ”€β”€ Platform ClassLoader  (was Extension CL, renamed Java 9+)
 *  β”‚   Loads: java.se modules, optional/extension modules
 *  β”‚
 *  └── Application ClassLoader
 *      Loads: everything on your --classpath / --module-path
 *      Your .class files, your JAR dependencies
 *
 *  Parent delegation: when asked to load com.example.Foo,
 *  Application CL first asks Platform CL, who asks Bootstrap CL.
 *  Bootstrap says "not mine" β†’ Platform says "not mine"
 *  β†’ Application finds and loads it.
 *
 *  Why: prevents you from accidentally (or maliciously) replacing
 *  core classes like java.lang.String with your own version.
 *
 *  Custom ClassLoader: used by frameworks (Spring, OSGi, hot-reload)
 *  to load classes from non-standard locations or isolate plugins.
 */

// Inspect the ClassLoader chain
Class<?> cls = String.class;
cls.getClassLoader();        // null β€” Bootstrap loaded it

Class<?> mine = MyApp.class;
mine.getClassLoader();       // jdk.internal.loader.ClassLoaders$AppClassLoader
mine.getClassLoader().getParent();  // PlatformClassLoader

Execution Engine β€” Interpreter, JIT, and Tiered Compilation

/*
 *  HotSpot uses TIERED COMPILATION β€” four tiers:
 *
 *  Tier 0: Interpreter
 *    Executes bytecode directly. No compilation overhead.
 *    Slow (~10x native), but starts immediately.
 *    Profiler data collected here.
 *
 *  Tier 1-3: C1 compiler (client compiler)
 *    Triggered after ~2,000 invocations (configurable).
 *    Fast compilation, limited optimisations.
 *    Good for: methods that are called often but not CPU-critical.
 *    Still collects profiling data.
 *
 *  Tier 4: C2 compiler (server compiler)
 *    Triggered after ~10,000-15,000 invocations.
 *    Slow to compile, aggressive optimisations.
 *    Produces highly optimised native code.
 *    Used for "hot" methods β€” the real performance gain.
 *
 *  The path: interpreted β†’ C1 (with profiling) β†’ C2 (optimised)
 */

// See JIT compilation in action:
java -XX:+PrintCompilation MyApp
// Output like: 42   3       4  java.util.HashMap::hash (20 bytes)
//              ^    ^    ^  ^      method name             size
//         stamp  id tier  
// tier 4 = C2 compiled

// Force interpretation (disable JIT) β€” for reproducible benchmarking:
java -Xint MyApp

// Force all compilation at start (AOT-like warm-up trick):
java -XX:CompileThreshold=1 MyApp

Key JIT optimisations

/*
 *  INLINING: the most impactful optimisation.
 *  Small methods are inlined into callers β€” eliminates call overhead
 *  and enables further optimisations on the combined code.
 *  Default threshold: 35 bytecodes (-XX:MaxInlineSize=35).
 *
 *  ESCAPE ANALYSIS: if an object doesn't "escape" the method
 *  (no reference stored outside it), the JIT can allocate it on the
 *  stack instead of the heap β€” or eliminate it entirely.
 *
 *  Example: StringBuilder in string concatenation is often eliminated.
 *
 *  NULL CHECKS ELISION: after a null check, the JIT removes subsequent
 *  checks on the same reference within the same scope.
 *
 *  DEVIRTUALISATION: if profiling shows that a virtual call always
 *  goes to one concrete implementation, the JIT replaces it with a
 *  direct call (and inlines it). Falls back if a new subclass appears.
 *
 *  LOOP UNROLLING / VECTORISATION: loops over arrays are transformed
 *  to use SIMD CPU instructions (via Java's Vector API, Java 17+).
 *
 *  DEOPTIMISATION: if an assumption (e.g. "only one subclass") becomes
 *  invalid, the JIT reverts to interpreted mode β€” "bails out" β€” and
 *  recompiles with updated profile data.
 */

Garbage Collection β€” Overview

The GC's job: find objects that are no longer reachable from any live thread and reclaim their memory. The challenge: doing this without pausing the application for too long.

/*
 *  GC ROOTS β€” starting points for reachability tracing:
 *  β€’ Local variables and parameters on thread stacks
 *  β€’ Static fields of loaded classes
 *  β€’ JNI references (native code holding Java objects)
 *
 *  REACHABILITY ALGORITHM (mark phase):
 *  Start from all GC roots, follow every reference.
 *  Any object you reach = LIVE. Everything else = DEAD.
 *
 *  COLLECTION ALGORITHM (sweep/compact):
 *  Varies by GC: copy survivors, compact in-place, or mark-and-sweep.
 */

// GC algorithms available in modern JVMs:
-XX:+UseSerialGC        // single-threaded, tiny footprint β€” embedded only
-XX:+UseG1GC            // default since Java 9: balanced throughput+latency
-XX:+UseZGC             // Java 15+ production: sub-ms pauses, any heap size
-XX:+UseShenandoahGC    // Red Hat: similar goals to ZGC
-XX:+UseParallelGC      // max throughput, larger pauses β€” batch jobs

// G1 tuning β€” the most common production choice:
-XX:MaxGCPauseMillis=200   // target pause goal (not a hard guarantee)
-XX:G1HeapRegionSize=16m   // region size (1MB–32MB, power of 2)

// Enable GC logging (essential in production):
-Xlog:gc*:file=/var/log/app-gc.log:time,uptime:filecount=5,filesize=20m
GC selection guide (2026)

Default (most services): G1GC β€” good balance, well understood.
Latency-sensitive (trading, gaming, real-time): ZGC β€” pauses under 1ms regardless of heap size.
Throughput-first (batch, analytics): ParallelGC β€” maximises work done, longer pauses acceptable.
Microservices in containers: ZGC or G1 with -XX:MaxRAMPercentage=75.
Virtual threads (Java 21): More short-lived objects from continuations β€” ZGC handles this better than G1 at scale.

Essential JVM Flags for Production

# ── Memory ──────────────────────────────────────────────────────
-Xms2g -Xmx2g          # Fix heap size (avoids resize pauses) β€” set equal in prod
-XX:MaxRAMPercentage=75 # Container-aware: use 75% of container limit (Java 10+)
-XX:MaxMetaspaceSize=256m # Cap Metaspace growth (prevent OOM from class leaks)
-Xss512k               # Reduce stack per thread (default 1MB) β€” more threads in RAM

# ── GC ──────────────────────────────────────────────────────────
-XX:+UseZGC            # Low-latency GC (Java 15+ production, Java 21 gen ZGC)
-XX:+UseG1GC           # Balanced default (Java 9+)
-XX:MaxGCPauseMillis=100 # G1 pause target
-Xlog:gc*:file=gc.log  # GC log β€” must-have for production diagnosis

# ── Diagnostics ─────────────────────────────────────────────────
-XX:+HeapDumpOnOutOfMemoryError  # Dump heap on OOM β€” essential for post-mortem
-XX:HeapDumpPath=/dumps/oom.hprof
-XX:+ExitOnOutOfMemoryError      # Kill JVM on OOM (let k8s restart it) β€” safer than limping
-XX:+PrintFlagsFinal -version    # Print all JVM flags β€” use for auditing defaults

# ── JIT ─────────────────────────────────────────────────────────
-XX:ReservedCodeCacheSize=256m  # Code cache for JIT-compiled methods
-XX:+TieredCompilation          # Default on Java 8+ β€” use C1+C2 tiered

# ── Container / Kubernetes ───────────────────────────────────────
-XX:+UseContainerSupport        # Default on Java 10+ β€” reads cgroup CPU/memory limits
-XX:ActiveProcessorCount=2      # Override if availableProcessors() is wrong

Interview Questions

πŸŽ“ Junior level

Q: What is the difference between JDK, JRE, and JVM?
JVM is the engine that executes bytecode β€” platform-specific, handles memory and GC. JRE = JVM + standard library β€” what you need to run Java programs. JDK = JRE + compiler (javac) + tools β€” what you need to develop. Since Java 11, JRE is no longer distributed separately; you install the JDK everywhere.

Q: What is bytecode and why does it exist?
Bytecode is the intermediate binary format produced by javac. It's not native machine code β€” a CPU can't execute it directly. The JVM translates it to native instructions at runtime. This is what enables "Write Once, Run Anywhere": the same .class file runs on any OS and CPU that has a JVM, without recompilation.

Q: What is the difference between heap and stack?
The stack is per-thread, holds method frames (local variables, return addresses), is LIFO, and is reclaimed automatically when a method returns. The heap is shared by all threads, holds all objects created with new, and is managed by the Garbage Collector. A stack overflow means too many nested method calls. An OutOfMemoryError means the heap is full.

πŸ”₯ Senior level

Q: What is tiered compilation and why does Java have a "warm-up" period?
HotSpot uses four compilation tiers. Initially, bytecode is interpreted (slow but immediate). The C1 compiler kicks in after ~2,000 invocations, producing lightly optimised native code while still profiling. The C2 compiler kicks in after ~10,000–15,000 invocations, applying aggressive optimisations based on the profiling data. This is why Java services perform worse in the first minutes after startup β€” the JIT hasn't had enough data to compile hot paths. Solutions: warm-up load in staging before prod traffic, GraalVM native image (compiles ahead-of-time, no warm-up), or JVM snapshots with CRaC (Checkpoint/Restore, Java 21+).

Q: What is escape analysis and what can the JIT do with it?
Escape analysis determines whether an object reference can "escape" the current method or thread β€” i.e., be stored in a field, returned, or passed to another thread. If an object doesn't escape: (1) Stack allocation β€” the JIT allocates it on the thread stack instead of the heap, eliminating GC pressure; (2) Scalar replacement β€” the object is decomposed into its individual fields, which the CPU keeps in registers; (3) Lock elision β€” if a synchronised block on the object is unreachable by other threads, the JIT removes the lock entirely. A common example: the StringBuilder created by string concatenation in a loop is often stack-allocated or eliminated.

Q: Why is PermGen gone and what replaced it?
PermGen (Permanent Generation) was a fixed-size region of the heap (pre-Java 8) that stored class metadata. Its fixed size caused OutOfMemoryError: PermGen space in applications with many classes or heavy use of CGLIB proxies (Spring, Hibernate). Java 8 replaced it with Metaspace: same purpose but uses native memory (outside the heap) and grows dynamically by default. This eliminated most PermGen OOMs β€” but also means Metaspace can grow unbounded if classes leak (common with custom ClassLoaders in frameworks). Always set -XX:MaxMetaspaceSize in production.