JDK vs JRE vs JVM
Every production incident involving "Java is slow," "OutOfMemoryError," or "ClassNotFoundException" eventually traces back to the same platform layer: JDK, JRE, and JVM.
Introduction
Every production incident involving "Java is slow," "OutOfMemoryError," or "ClassNotFoundException" eventually traces back to the same platform layer: JDK, JRE, and JVM. These three terms are not interchangeable — confusing them leads to wrong Docker images, bloated CI pipelines, and architecture reviews that miss the real runtime boundary.
The JDK (Java Development Kit) is what developers install: compiler (javac), debugger (jdb), profiler (jcmd, JFR), and the full standard library. The JRE (Java Runtime Environment) was historically "JVM + libraries to run apps" — since Java 11, Oracle no longer ships a standalone JRE; production images use modular JRE builds (e.g. jlink, Temurin JRE). The JVM (Java Virtual Machine) is the engine: class loading, bytecode verification, interpreter, JIT compiler, garbage collector, and native thread management.
Staff engineers treat the JVM as a managed runtime platform — not a black box. Understanding its internals is how you debug Metaspace leaks at Goldman Sachs, tune GC pauses on Netflix payment services, and right-size Kubernetes pods at Amazon.
Business problem
Enterprise teams pay when JDK/JRE/JVM boundaries are misunderstood:
- Wrong container images: Shipping full JDK (500MB+) to production exposes compiler tools, increases attack surface, and wastes registry bandwidth — when a slim JRE suffices.
- Classloader incidents: Spring Boot fat JARs with duplicate dependencies cause
NoClassDefFoundErrorat 2 AM — root cause is classloader hierarchy, not "random Java bug." - JIT warm-up blindness: Autoscaling on CPU without readiness probes deploys cold JVMs into traffic — p99 latency spikes for 30–90 seconds after every scale-out event.
- Compliance failures: Running EOL JRE without vendor patches (Temurin/Corretto) fails PCI-DSS and SOC2 audits — the JVM is part of your supply chain.
Why this topic exists
Java separates compile-time from run-time deliberately — this is the architectural bet that enables portability and optimization:
- Compile once:
javacproduces bytecode — not native machine code tied to one CPU/OS. - Run anywhere: Each platform ships a JVM that interprets/JITs bytecode to native instructions.
- Optimize at runtime: JIT sees actual hot paths — static AOT compilers cannot match profile-guided optimization on long-lived servers.
- Sandbox execution: Bytecode verifier rejects unsafe instructions before they execute — foundational to enterprise trust in downloaded JARs.
Core concepts
JDK vs JRE vs JVM — the definitive breakdown:
- JDK = JVM + development tools + full API modules. Includes
javac,jar,javadoc,jshell,jfr,jlink,jdeps. - JRE = JVM + runtime libraries needed to execute compiled apps. Deprecated as standalone product; use
jlinkcustom runtime or vendor JRE images. - JVM = Execution engine only. Subsystems: class loader, bytecode verifier, interpreter, JIT (C1/C2), GC, native interface (JNI).
- Bytecode = Platform-independent opcodes in
.classfiles (defined by JVM Specification). Executed by interpreter; hot methods JIT-compiled. - Class loading = Bootstrap → Platform (Extension) → Application class loaders. Delegation model: parent-first by default.
- JIT = Just-In-Time compiler transforms hot bytecode to native code. C1 (client/quick), C2 (server/aggressive), Tiered compilation on modern HotSpot.
Internal architecture
Full Java platform architecture — how JDK, JRE, and JVM nest inside a production deployment:
┌──────────────────────────────── JDK (Development) ────────────────────────────────┐│ Developer Tools: javac · jar · javadoc · jshell · jcmd · jfr · jlink · jdeps │├──────────────────────────────── Runtime Libraries ──────────────────────────────┤│ java.base · java.logging · java.net.http · java.sql · java.xml · ... (modules) │├────────────────────────────── JVM Engine ─────────────────────────────────────────┤│ ││ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────────────┐ ││ │Class Loaders│──▶│ Verifier │──▶│ Interpreter │──▶│ JIT (C1 → C2) │ ││ │ Boot/Plat/ │ │ Stack map │ │ Cold path │ │ HotSpot native code│ ││ │ Application │ │ Safety check │ │ execution │ │ Profile-guided │ ││ └─────────────┘ └──────────────┘ └─────────────┘ └─────────────────────┘ ││ │ │ ││ ▼ ▼ ││ ┌─────────────────────────────────────────────────────────────────────────────┐ ││ │ Runtime Data Areas │ ││ │ Metaspace (classes) │ Heap (objects) │ Stack (frames) │ PC Registers │ ││ └─────────────────────────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ ┌─────────────────────────────────────────────────────────────────────────────┐ ││ │ Garbage Collector (G1 · ZGC · Shenandoah · Parallel) │ ││ └─────────────────────────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ ┌─────────────────────────────────────────────────────────────────────────────┐ ││ │ Native Interface (JNI) · JVM TI · OS Threads · Hardware │ ││ └─────────────────────────────────────────────────────────────────────────────┘ │└───────────────────────────────────────────────────────────────────────────────────┘Production Docker (slim): eclipse-temurin:21-jre-alpine → JVM + runtime libs onlyCI / Developer laptop: eclipse-temurin:21-jdk → full JDK
Three architecture views — bytecode execution pipeline, classloader hierarchy, and JDK component map:
Code walkthrough
Trace bytecode from source to execution — compile, disassemble, and inspect what the JVM actually runs:
- javac (JDK only) — not present in production JRE images by design.
- javap -c -v — reveals opcodes (
lload_0,lcmp) the interpreter/JIT executes. - java (JVM launcher) — triggers class loading → verification → execution pipeline.
// PaymentValidator.javapublic class PaymentValidator {public static boolean isValidAmount(long cents) {return cents > 0 && cents <= 999_999_99L;}public static void main(String[] args) {System.out.println(isValidAmount(1500L));}}// Step 1 — Compile (JDK tool: javac)// javac PaymentValidator.java → PaymentValidator.class// Step 2 — Disassemble bytecode (JDK tool: javap)// javap -c -v PaymentValidator.class/*public static boolean isValidAmount(long);Code:0: lload_0 // load 'cents' param1: lconst_02: lcmp // compare cents > 03: ifle 14 // branch if false6: lload_07: ldc2_w #2 // max amount constant10: lcmp11: ifgt 1414: iconst_1 // return true15: ireturn16: iconst_0 // return false17: ireturn*/// Step 3 — Execute (JVM loads, verifies, runs)// java PaymentValidator
Production example
Production Dockerfile + JVM flags — how a fintech payment service separates JDK (CI) from governance JRE (runtime):
- Multi-stage build — JDK in build stage; only JRE + JAR in production (smaller, safer).
- -XX:+UseContainerSupport — JVM reads cgroup memory/CPU limits (critical on K8s).
- ZGC — sub-millisecond pauses for payment latency SLOs; chosen at JVM layer, not application code.
# ── CI pipeline (GitHub Actions) — full JDK for build ──FROM eclipse-temurin:21-jdk AS buildWORKDIR /appCOPY . .RUN ./gradlew bootJar -x test# ── Production image — JRE only, no javac ──FROM eclipse-temurin:21-jre-alpineRUN addgroup -S app && adduser -S app -G appUSER appCOPY --from=build /app/build/libs/payments.jar /app.jar# JVM tuned for container limits + low-latency GCENTRYPOINT ["java", \"-XX:+UseContainerSupport", \"-XX:MaxRAMPercentage=75.0", \"-XX:+UseZGC", \"-XX:+HeapDumpOnOutOfMemoryError", \"-XX:HeapDumpPath=/tmp/heap.hprof", \"-Xlog:gc*:stdout:time,uptime,level,tags", \"-jar", "/app.jar"]# ── On-call diagnostics (kubectl exec into pod) ──# jcmd 1 VM.flags # verify GC + heap settings# jcmd 1 GC.class_stats # class loading stats# jcmd 1 Compiler.queue # JIT compilation backlog
Enterprise case study
LinkedIn — JVM platform engineering at scale: LinkedIn runs thousands of Java services on Azul Zing/Zulu and Temurin JVMs. Their platform team treats JVM tuning as infrastructure — not per-team artisan craft. When they migrated services to Java 17+, class loading and Metaspace behavior changed with JPMS module boundaries; services using dynamic proxies (Hibernate, Mockito) saw Metaspace growth. Platform engineers standardized on -XX:MaxMetaspaceSize caps, JFR profiling during canaries, and JDK 21 virtual threads for I/O-bound feed services — reducing thread stack memory by 80% on read-heavy paths.
- Problem: Unbounded Metaspace on services with heavy dynamic class generation.
- Root cause: Application classloader retained classes from hot-reloaded Groovy scripts + Hibernate proxies.
- Fix: Metaspace limits, custom classloader lifecycle, JDK 21 virtual threads for I/O concurrency.
- Lesson: JVM internals are not academic — they drive capacity planning and incident response.
Performance considerations
Performance lives in the JVM layer — application code is only half the story:
- JIT warm-up: First 10k–100k invocations run interpreted/C1 — latency higher until C2 compiles. Use readiness probes; pre-warm with synthetic traffic.
- Inlining: C2 inlines hot callee methods — micro-benchmarks that don't warm up mislead. Measure at production scale.
- Escape analysis: JIT allocates objects on stack when proven not to escape — reduces GC pressure without code changes.
- Compilation queue: Code cache full → JIT stops compiling → performance cliff. Monitor with
jcmd Compiler.codecache. - Class loading cost: Spring Boot startup loads 15k–25k classes — Metaspace and startup time dominated by classpath scanning, not business logic.
Security considerations
JVM security architecture — defense in depth before your code runs:
- Bytecode verifier: Rejects stack underflow/overflow, illegal type casts, and private field access at load time — prevents entire classes of memory corruption.
- JDK in production: Shipping
javacto prod enables compile-in-place attacks if RCE exists — use JRE-only images. - Module system (JPMS): Java 9+ encapsulates internal APIs —
sun.misc.Unsafeaccess requires explicit opens (JDK 21 tightening). - JVM supply chain: Pin vendor JDK (Temurin/Corretto) with SHA-verified downloads; CVE patches are JVM-level, not Maven dependency bumps.
Scalability considerations
Scaling Java services requires JVM-aware capacity math:
- Heap sizing:
-XX:MaxRAMPercentage=75on K8s — leave headroom for Metaspace, thread stacks, native memory, and direct buffers. - Metaspace per pod: ~80–150MB typical Spring Boot; dynamic class gen services need explicit caps and monitoring.
- Thread stacks: Platform threads = ~1MB stack each. 500 threads ≈ 500MB before heap. Virtual threads (Java 21) collapse this for I/O workloads.
- JIT code cache: Default 240MB — large apps with many hot methods may need
-XX:ReservedCodeCacheSizeincrease.
Production challenges
Real production failures mapped to JVM subsystems:
- ClassNotFoundException: Wrong classloader context — common in app servers with multiple WARs or Spring Boot DevTools restart classloader.
- NoClassDefFoundError: Class was found at compile time but missing at runtime — dependency scope wrong in Maven/Gradle (
providedvscompile). - Metaspace OOM: Not heap OOM — class metadata exhausted. Hibernate, CGLIB, Groovy, OSGi are frequent culprits.
- JIT deoptimization: Assumption invalidated (e.g. monomorphic call site becomes megamorphic) — performance drops suddenly. Visible in JFR as "Deoptimization" events.
- Container OOMKill: JVM heap + off-heap exceeds pod limit — JVM dies without heap dump. Always set
MaxRAMPercentage, not fixed-Xmxon K8s.
Common mistakes
- Using JDK and JRE interchangeably in Docker — production should never include
javac. - Assuming
Class.forName()uses the same loader as Spring's component scan — it often does not. - Setting
-Xmxequal to container memory limit — native memory and Metaspace need headroom. - Ignoring JIT warm-up in load tests — benchmark after warm-up or results mislead autoscaling decisions.
- Thinking JRE still exists as a separate Oracle download — since Java 11, use vendor JRE images or
jlink.
Debugging guide
JVM diagnostics playbook — map symptoms to subsystems:
- Class loading:
jcmd <pid> VM.classloader_stats— which loaders, how many classes. - JIT state:
jcmd <pid> Compiler.perfmapor JFR "Compilation" events. - Metaspace:
jcmd <pid> VM.metaspace— used vs committed vs limit. - Bytecode inspect:
javap -c -private com.example.MyClasson the deployed JAR class. - Verbose class loading:
-verbose:classat startup (dev only) — trace every loaded class.
# Production triage sequencejava -version # confirm vendor + patch leveljcmd 1 VM.flags # GC, heap, container flagsjcmd 1 VM.classloader_stats # classloader leak suspectjcmd 1 GC.class_stats | head -30 # loaded class countsjcmd 1 VM.metaspace # Metaspace pressurejfr start duration=60s filename=/tmp/rec.jfr# ... reproduce issue ...jfr print --events jdk.Deoptimization,jdk.GarbageCollection /tmp/rec.jfr
Best practices
- Use multi-stage Docker builds: JDK for compile, JRE for runtime — standard at Amazon, Netflix, Goldman.
- Pin vendor JDK (Temurin 21 or Corretto 21) in CI and production with identical patch levels.
- Set -XX:+UseContainerSupport and MaxRAMPercentage on all K8s-deployed JVMs.
- Cap Metaspace with -XX:MaxMetaspaceSize on services using dynamic proxies or scripting engines.
- Enable JFR in production with low overhead — continuous profiling beats post-mortem heap dumps alone.
- Use jlink for edge/serverless when image size matters — custom runtime with only required modules.
Anti-patterns
- Installing full JDK on production servers "just in case" — expands attack surface and image size.
- Creating custom classloaders without understanding delegation — breaks parent visibility and causes CNFE.
- Disabling bytecode verification (
-Xverify:none) — never in production; removes foundational safety. - Running load tests on cold JVMs and declaring SLO failure — warm up first or use JFR to exclude compile phase.
- Using
System.gc()in application code — fights the GC algorithm; tune flags instead.
Staff engineer notes
- In architecture review, ask: "What JDK ships in prod, what GC, what heap percentage of pod limit?" — answers reveal JVM maturity.
- The JRE deprecation is a feature, not a loss — modular
jlinkruntimes are smaller and more secure than monolithic JRE. - Classloader leaks are the #1 cause of Metaspace OOM in long-lived Spring services — understand lifecycle before blaming GC.
- JIT compilation is why Java wins on throughput for services running weeks — but loses on cold start vs GraalVM native-image. Document workload fit in ADRs.
- When interviewing senior candidates, "explain classloader delegation" separates people who ran Java from people who operated it.
Interview questions
Interview preparation
15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is the difference between JDK, JRE, and JVM?
BeginnerModel answer
- JVM executes bytecode
- class loading, verification, interpreter, JIT, GC. JRE was JVM + runtime libraries (deprecated standalone since Java 11). JDK is JRE + development tools: javac, jar, jfr, jlink, jcmd. Developers use JDK; production containers typically ship JRE-only images.
Follow-up probe
Why did Oracle remove the standalone JRE?
2What is bytecode and why not compile directly to machine code?
BeginnerModel answer
- Bytecode is platform-independent intermediate instructions in .class files. Compiling to bytecode enables WORA
- same JAR runs on Linux, Windows, macOS. JVM JIT compiles hot bytecode to native code at runtime with profile data static compilers lack.
Follow-up probe
Name two JVM bytecode instructions.
3Explain the classloader delegation model.
BeginnerModel answer
- When a classloader receives a load request, it delegates to its parent first. Bootstrap loads java.*, Platform loads platform modules, Application loads classpath JARs. Child sees parent classes but not sibling classes
- prevents duplicate core class definitions.
Follow-up probe
What is a classloader leak?
4What does the bytecode verifier check?
BeginnerModel answer
- Stack depth consistency, valid type operations, no illegal access to private members, and control flow integrity. Runs at class load time before any bytecode executes
- foundational JVM security sandbox.
Follow-up probe
Can you disable the verifier?
5What is the difference between JDK tools javac and java?
BeginnerModel answer
- javac compiles .java source to .class bytecode
- development-time only. java is the JVM launcher that loads classes, starts main(), and manages the runtime. java exists in JRE; javac only in JDK.
Follow-up probe
What happens if you run java without a main method?
Intermediate
6Explain interpreter vs JIT compilation in HotSpot.
IntermediateModel answer
- Interpreter executes bytecode directly
- fast startup, slower steady-state. JIT (C1 then C2) compiles hot methods to native code using runtime profiling. Tiered compilation balances startup (C1) with peak throughput (C2). Cold methods stay interpreted.
Follow-up probe
What triggers JIT compilation?
7What are C1 and C2 compilers?
IntermediateModel answer
- C1 (client compiler): fast compile, basic optimizations
- tiers 1-3. C2 (server compiler): aggressive inlining, escape analysis, loop unrolling
- tier 4. Modern HotSpot uses tiered compilation: methods graduate C1 → C2 as invocation counters exceed thresholds.
Follow-up probe
What is deoptimization?
8Why use jlink instead of a full JRE?
IntermediateModel answer
- jlink creates custom runtime images with only required modules
- 40-80MB vs 200MB+ full JRE. Reduces attack surface, container size, and startup classpath scanning. Used in serverless and edge deployments.
Follow-up probe
Which module is always required?
9How does Spring Boot fat JAR class loading work?
IntermediateModel answer
- LaunchedURLClassLoader loads classes from nested JARs inside BOOT-INF/lib/. Parent is AppClassLoader. Custom loader enables 'run java -jar app.jar' with embedded dependencies
- but creates classloader complexity for agents and hot reload.
Follow-up probe
Why does DevTools restart use a separate classloader?
10What JVM flags would you set for a Java 21 payment service on Kubernetes?
IntermediateModel answer
-XX:+UseContainerSupport, -XX:MaxRAMPercentage=75, -XX:+UseZGC, -XX:+HeapDumpOnOutOfMemoryError, -XX:MaxMetaspaceSize=256m, -Xlog:gc* for observability.
Readiness probe waits for JIT warm-up.
JFR enabled for continuous profiling.
Follow-up probe
Why MaxRAMPercentage instead of -Xmx?
Advanced
11Walk through what happens when you run java -jar app.jar.
AdvancedModel answer
MF.
Application classloader loads main class → verifier checks → main() invoked via reflection.
run() scans classpath, creates ApplicationContext (loads thousands of classes into Metaspace), embedded Tomcat starts.
Follow-up probe
Where is most startup time spent?
12Design JVM observability for 500 microservices.
AdvancedModel answer
Standardize base flags: UseContainerSupport, MaxRAMPercentage, GC logging, HeapDumpOnOOM.
g.
Datadog/async-profiler).
jcmd runbooks per alert.
Dashboards: GC pause p99, Metaspace usage, JIT queue depth, class count.
Canary deploys with JFR diff.
ADR per GC choice.
Follow-up probe
How do you detect classloader leaks?
13Compare HotSpot JIT vs GraalVM native-image for a Lambda function.
AdvancedModel answer
- Lambda: cold start dominates, short lifetime, moderate traffic. Native-image: AOT compile at build, ~50ms startup, lower RSS
- wins. HotSpot: 2-5s warm-up, higher peak throughput if function runs long
- loses on cold start. Hybrid: SnapStart (CRaC) on HotSpot for checkpoint/restore.
Follow-up probe
What do you lose with native-image?
14Explain Metaspace vs Heap — when does each OOM?
AdvancedModel answer
- Heap: object instances (new Foo()). Metaspace: class metadata
- method bytecode, constant pool, annotations. Metaspace OOM when too many classes loaded (Hibernate proxies, Groovy, OSGi, classloader leak). Heap OOM when objects retained (memory leak). Different diagnostics, different fixes.
Follow-up probe
Can GC clean Metaspace?
15How would you debug a NoClassDefFoundError in production?
AdvancedModel answer
- 1) Read stack trace
- which class missing. 2) Check if compile-time dep scope is wrong (provided/test). 3) Inspect fat JAR
- is class in BOOT-INF/lib? 4) Classloader context
- thread context classloader vs app loader. 5) jcmd VM.classloader_stats. 6) Compare local vs prod JDK version for removed APIs.
Follow-up probe
Difference from ClassNotFoundException?
Hands-on exercise
Lab: JVM platform discovery — run in the playground, then complete these steps on your local JDK 21:
- Run the playground code — observe class name, classloader type, and runtime version.
- Locally: compile with
javac Main.java, disassemble withjavap -c Main— identify 3 bytecode opcodes. - Run with
java -verbose:class Main 2>&1 | head -20— count how many classes load before main executes. - Write one paragraph: which Docker base image (JDK vs JRE) you would use for CI vs production and why.
JavaJDK vs JRE vs JVM
Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.Architecture trade-offs
- Full JDK vs JRE in Docker: JDK wins for simplicity in small teams; JRE/jlink wins for security, size, and compliance at scale.
- Interpreter-first vs AOT (GraalVM): HotSpot JIT wins sustained throughput; native-image wins cold start and memory for short-lived workloads.
- Default classloader vs custom: Default wins for 99% of apps; custom loaders needed for plugin systems, OSGi, app servers — at cost of complexity and leak risk.
- G1 vs ZGC: G1 default, balanced; ZGC for strict p99 latency on Java 21 with larger heap headroom for concurrent GC threads.
Summary
JDK vs JRE vs JVM is the operating system of Java engineering. You now understand how bytecode flows through class loaders and the verifier, how HotSpot JIT tiered compilation delivers peak throughput, and how enterprises like LinkedIn operationalize JVM tuning at scale. Every future lesson — OOP, collections, concurrency — runs inside this runtime. Next: Java syntax and the type system.
Key takeaways
- JDK = develop (javac, jfr, jlink); JRE = run (modular since Java 11); JVM = execute (classload, verify, interpret, JIT, GC).
- Bytecode is the portability contract — JVM verifies safety, interprets cold paths, JIT-compiles hot paths to native code.
- Classloader delegation (Bootstrap → Platform → Application) governs visibility — leaks here cause Metaspace OOM.
- Production ships JRE-only images with container-aware flags; JDK stays in CI — multi-stage Docker is the enterprise standard.
- Staff engineers debug at the JVM layer: jcmd, JFR, javap, Metaspace caps — not just application stack traces.