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).
- 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.
| Tool | Purpose |
|---|---|
javac | Compiles .java source into .class bytecode |
java | Launches the JVM to run a compiled class or JAR |
jar | Packages classes and resources into a JAR (see WAR vs JAR Files) |
javadoc | Generates HTML API documentation from source comments |
javap | Disassembles a class file — inspect bytecode and constant pool directly |
jlink | Builds 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 JDKPrior 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 libraries —
java.lang,java.util,java.io, and the rest of the standard library - Supporting runtime files — property files, resource bundles the libraries depend on
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.
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
.classfiles 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
| Area | Scope | Holds |
|---|---|---|
| Heap | Shared across all threads | Every object instance |
| Stack | Per thread | Method call frames, local variables |
| Method Area (Metaspace) | Shared across all threads | Class structures, the constant pool |
| PC Register | Per thread | The address of the currently executing instruction |
| Native Method Stack | Per thread | Native (non-Java) method calls |
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
| Aspect | JDK | JRE | JVM |
|---|---|---|---|
| Contains | JRE + dev tools | JVM + libraries | Execution engine only |
| Has a compiler | Yes (javac) | No | No |
| Can run programs | Yes | Yes | Yes — it's the part that actually does it |
| Target user | Developers | End users, legacy deployment target | Internal component, not installed alone |
| Standalone download today | Yes — the primary distribution | Not from Oracle since Java 11 — build one with jlink instead | Never 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.
| Aspect | Standard JVM (interpreted + JIT) | GraalVM Native Image |
|---|---|---|
| Startup time | JVM warm-up, JIT profiling ramp-up | Near-instant — the executable is already machine code |
| Memory footprint | JVM overhead plus the application | Substantially lower — no JVM to host |
| Build time | Fast — javac alone | Much slower — whole-program ahead-of-time analysis |
| Reflection, dynamic class loading | Fully supported | Requires explicit configuration — the AOT analysis must know about it at build time |
| Best fit | Long-running services where JIT eventually reaches peak throughput | Serverless functions, CLI tools, containers valuing fast cold-start over peak throughput |
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
jlinkto 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: Metaspaceas 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
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.
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.