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 |
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
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
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.
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.