JDK vs JRE vs JVM

Three nested layers, not three alternatives — what each actually contains, what's been removed or renamed in recent years, and where a native image changes the equation entirely

← Back to Index

What are JDK, JRE, and JVM — and Why Three Names for "Java"?

"Install Java" is not one action — it's shorthand for one of three genuinely different things, nested inside each other, each solving a different problem: the JVM is the abstract machine that executes bytecode, the JRE is a JVM plus the class libraries needed to actually run a program, and the JDK is a JRE plus the compiler and tools needed to build one in the first place. Confusing them is how a production server ends up with a full development toolchain it doesn't need, or a developer's machine ends up unable to compile anything at all.

// BEFORE — treating "Java" as one indivisible thing
$ javac MyApp.java
bash: javac: command not found
// A JRE-only installation has no compiler at all — "I installed Java" doesn't
// specify WHICH of the three things was actually installed.

// AFTER — knowing exactly which tool does which job in the pipeline
MyApp.java  ──javac (JDK)──▶  MyApp.class  ──java (JVM, inside JRE, inside JDK)──▶  running program
// javac only exists in the JDK. java (the launcher) exists in both the JDK
// and a standalone JRE — but a standalone JRE download hasn't existed as a
// separate artifact from Oracle since Java 11 (more on this below).
The relationship, in one line each
  • JDK = JRE + compiler (javac) + development tools
  • JRE = JVM + core class libraries (java.lang, java.util, etc.)
  • JVM = the abstract machine that loads, verifies, and executes bytecode

JDK — Java Development Kit

The complete kit for writing, compiling, packaging, and debugging Java applications. If you're developing, you always need the JDK — a JRE alone has no compiler and can't build anything.

ToolPurpose
javacCompiles .java source into .class bytecode
javaLaunches the JVM to run a compiled class or JAR
jarPackages classes and resources into a JAR (see WAR vs JAR Files)
javadocGenerates HTML API documentation from source comments
javapDisassembles a class file — inspect bytecode and constant pool directly
jlinkBuilds a custom, minimal runtime image containing only the modules an application actually needs
javac MyApp.java              # compile
java MyApp                    # run
jar cf myapp.jar *.class      # package
javap -c MyApp.class          # inspect the generated bytecode
jvisualvm is no longer bundled with the JDK

Prior to Java 9, VisualVM shipped inside the JDK as jvisualvm. It was decoupled and is now a separate download at visualvm.github.io — a common source of "the tutorial said it would be here and it isn't" confusion on any reasonably current JDK.

JRE — Java Runtime Environment

Everything needed to run a compiled Java application, and nothing to build one: the JVM plus the core class libraries a program depends on at runtime.

  • JVM — the execution engine itself
  • Core librariesjava.lang, java.util, java.io, and the rest of the standard library
  • Supporting runtime files — property files, resource bundles the libraries depend on
Java Plugin and Java Web Start are not deprecated — they're removed

The browser Java Plugin was removed in JDK 9; Java Web Start was removed entirely in JDK 11. Neither is coming back, and no current JDK/JRE distribution includes them — treat any tutorial referencing them as describing a JDK 8-era environment specifically.

Since Java 11, there's no separate JRE download from Oracle

The JDK became the sole primary distribution. For a minimal deployment footprint without shipping a full JDK, build a custom runtime image with jlink — it strips the toolchain down to exactly the modules an application actually needs, often smaller than a full JDK by a wide margin.

JVM — Java Virtual Machine

The abstract machine that actually loads, verifies, and executes bytecode — the component that makes "write once, run anywhere" real, since the same .class file runs unmodified on any platform with a compliant JVM.

What the JVM does

  • Loads .class files via the classloader subsystem
  • Verifies bytecode is structurally valid and doesn't violate the language's safety guarantees before ever executing it
  • Executes — interpreting bytecode directly, or JIT-compiling hot paths to native machine code
  • Manages memory — allocation and garbage collection, entirely transparent to application code

Runtime data areas

AreaScopeHolds
HeapShared across all threadsEvery object instance
StackPer threadMethod call frames, local variables
Method Area (Metaspace)Shared across all threadsClass structures, the constant pool
PC RegisterPer threadThe address of the currently executing instruction
Native Method StackPer threadNative (non-Java) method calls
Metaspace replaced PermGen in Java 8 — and this matters when you hit an OOM

Before Java 8, the Method Area lived inside a fixed-size region of the heap called PermGen, with its own dedicated failure mode: OutOfMemoryError: PermGen space, commonly triggered by exactly the classloader leaks described on Context & Deployment — an application server hot-redeploying repeatedly without releasing old classloaders. Since Java 8, this area is Metaspace, allocated from native (off-heap) memory rather than the JVM heap, and its own failure mode is OutOfMemoryError: Metaspace. Same underlying leak mechanism, different memory region, different tuning flags (-XX:MaxMetaspaceSize rather than -XX:MaxPermSize) — recognizing which one you're looking at in a stack trace tells you immediately which Java version-era mental model applies.

Execution engine

  • Interpreter — executes bytecode instruction by instruction, no upfront compilation cost
  • JIT compiler — profiles execution and compiles frequently-run ("hot") code paths to native machine code for the rest of that run
  • Garbage collector — reclaims heap memory no longer reachable from any live reference

Side by Side

AspectJDKJREJVM
ContainsJRE + dev toolsJVM + librariesExecution engine only
Has a compilerYes (javac)NoNo
Can run programsYesYesYes — it's the part that actually does it
Target userDevelopersEnd users, legacy deployment targetInternal component, not installed alone
Standalone download todayYes — the primary distributionNot from Oracle since Java 11 — build one with jlink insteadNever installed independently

JDK Distributions and Vendors

All major distributions implement the same OpenJDK-derived specification; they differ in support terms, release cadence, and licensing.

  • Oracle JDK — Oracle's own build; licensing terms for production use have changed more than once (No-Fee Terms and Conditions for recent versions vs. the older subscription-based model for earlier ones) — always verify Oracle's current terms directly before a production deployment, since this has real cost implications that shift by version
  • OpenJDK — the open-source reference implementation everything else derives from
  • Eclipse Temurin — formerly branded AdoptOpenJDK; renamed in 2021 under the Eclipse Adoptium project. Same lineage, new name — not two competing distributions
  • Amazon Corretto — free, production-ready, no-cost long-term support from AWS
  • Azul Zulu — enterprise-support-focused OpenJDK build
  • GraalVM — covered separately below; adds ahead-of-time native compilation on top of a standard JDK
java -version
# openjdk version "21.0.1" 2023-10-17 LTS
# OpenJDK Runtime Environment Temurin-21.0.1+12 (build 21.0.1+12-LTS)
# OpenJDK 64-Bit Server VM Temurin-21.0.1+12 (build 21.0.1+12-LTS, mixed mode, sharing)

javac -version
# javac 21.0.1

Production Reality — GraalVM Native Image Changes the Equation

Everything above assumes a JVM is present and running at all times — interpreting or JIT-compiling bytecode as the program executes. GraalVM Native Image takes a fundamentally different approach: it ahead-of-time compiles a Java application, at build time, into a single standalone native executable that needs no JVM at runtime whatsoever.

AspectStandard JVM (interpreted + JIT)GraalVM Native Image
Startup timeJVM warm-up, JIT profiling ramp-upNear-instant — the executable is already machine code
Memory footprintJVM overhead plus the applicationSubstantially lower — no JVM to host
Build timeFast — javac aloneMuch slower — whole-program ahead-of-time analysis
Reflection, dynamic class loadingFully supportedRequires explicit configuration — the AOT analysis must know about it at build time
Best fitLong-running services where JIT eventually reaches peak throughputServerless functions, CLI tools, containers valuing fast cold-start over peak throughput
Why this matters for the microservices/Kubernetes context already covered in this Bible

Spring Boot 3's native image support (built on GraalVM) exists specifically for the cold-start-sensitive scenarios described on What is an Application Server? — a pod that needs to be ready to serve traffic within milliseconds of starting, rather than after several seconds of JVM and Spring context warm-up. The trade-off is real: heavy reflection use (common in some older libraries) and dynamic proxying can require explicit native-image configuration or simply not work unmodified, which is why this is an opt-in build profile, not a universal default.

Practical Scenarios — What Do You Actually Need?

Writing and compiling code: the JDK. There's no substitute — the compiler only exists there.

Running a compiled application in production: a JRE (if you can still find one for your version), a full JDK (fine, just heavier than necessary), or — the current best practice — a custom runtime image built with jlink containing only the modules the application actually uses.

Fastest possible cold start in a container/serverless context: consider a GraalVM Native Image build instead of shipping any JVM at all — covered above.

Best Practices and Common Pitfalls

✅ Do

  • Use jlink to build a minimal runtime image for production deployment rather than shipping a full JDK unnecessarily
  • Verify Oracle's current licensing terms directly before choosing Oracle JDK for a production deployment — terms have changed multiple times and vary by version
  • Recognize OutOfMemoryError: Metaspace as today's equivalent of the old PermGen exhaustion — same underlying classloader-leak class of bug, different memory region
  • Consider GraalVM Native Image for genuinely cold-start-sensitive deployments (serverless, scale-to-zero containers), not as a default choice for every service
  • Manage multiple installed JDK versions with a tool like SDKMAN! or jEnv rather than manually juggling JAVA_HOME

❌ Don't

  • Don't assume "I installed Java" specifies which of JDK/JRE/JVM is actually present — a JRE-only environment has no compiler
  • Don't follow a tutorial referencing the Java Plugin or Java Web Start as if they're available today — both are fully removed, not merely deprecated
  • Don't assume AdoptOpenJDK and Eclipse Temurin are two different distributions to choose between — they're the same lineage under a 2021 rename
  • Don't reach for GraalVM Native Image by default — the build-time cost and reflection limitations are real trade-offs, not free wins

Interview Questions

🎓 Junior level

Q: What's the relationship between JDK, JRE, and JVM?
They're nested, not alternatives: the JVM executes bytecode; the JRE is a JVM plus the core class libraries needed to actually run a program; the JDK is a JRE plus the compiler and development tools needed to build one. Having the JDK means you already have everything the JRE and JVM provide.

Q: Why can't you compile Java code with only a JRE installed?
The JRE contains the JVM and runtime libraries, but not javac, the compiler — that tool exists only in the JDK. A JRE-only environment can run already-compiled .class files and JARs, but has no way to turn .java source into bytecode.

Q: What makes Java "write once, run anywhere"?
Source code compiles to platform-independent bytecode, not native machine code. Any platform with a compliant JVM can load and execute that same bytecode, translating it to that platform's native instructions internally — the bytecode itself never changes across platforms.

🔥 Senior level

Q: A long-running application server repeatedly throws OutOfMemoryError: Metaspace after many hot redeployments. What does this tell you about the JVM version and the likely root cause?
Metaspace only exists as a concept since Java 8 — an application still reporting PermGen-related errors would indicate an older JVM. The underlying cause is almost always a classloader leak: each hot redeploy should make the previous deployment's entire classloader — and every class structure loaded through it, which lives in Metaspace — eligible for garbage collection, but something outside that deployment's own lifecycle (an unreleased thread, a registered JDBC driver, a ThreadLocal on a shared pool thread) still references it. This is the exact mechanism covered in depth on Context & Deployment; the fix is a symmetric cleanup in contextDestroyed(), not simply raising -XX:MaxMetaspaceSize, which only delays the same failure.

Q: Why does building a GraalVM Native Image require explicit configuration for reflection-heavy code, when the same code runs fine on a standard JVM?
A standard JVM resolves reflective calls dynamically at runtime — the class being reflected on doesn't need to be known in advance. Native Image performs a whole-program, ahead-of-time analysis at build time to determine exactly which classes, methods, and fields the final executable needs, and anything reached only through reflection is invisible to that static analysis unless explicitly declared via reflection configuration metadata. Frameworks and libraries built around heavy runtime reflection (older ORM proxies, some AOP implementations) can silently fail at native-image runtime — not at build time — if this configuration is incomplete, which is why native-image adoption requires validating the full dependency tree, not just the application's own code.

Q: Why did Oracle move the Method Area out of the heap and into native memory (Metaspace) in Java 8, rather than simply resizing PermGen?
PermGen had a fixed maximum size that had to be set upfront (-XX:MaxPermSize) and was difficult to size correctly in advance — applications with many classes or heavy dynamic class generation (common with certain frameworks and application servers hosting multiple deployments) routinely exhausted it regardless of how generously it was configured. Moving class metadata to Metaspace, backed by native memory that grows dynamically up to the available system memory (or an optional cap), removed the need to pre-size this region at all for most applications and eliminated an entire historically common class of tuning-related outage — at the cost of needing to actively monitor native memory consumption instead, since "unbounded by default" cuts both ways.