Skip to main content

JDK vs JRE vs JVM: Differences, Architecture Diagram and How Java Code Runs

JDK vs JRE vs JVM explained with a comparison table, nested diagram, .java to bytecode to JIT flow, real javap output, class loaders, runtime memory areas and why the JRE no longer ships separately. All verified on JDK 25.

JVM (Java Virtual Machine) is the program that runs Java bytecode. JRE (Java Runtime Environment) is the JVM plus the standard class libraries, which is everything needed to run a Java program. JDK (Java Development Kit) is the JRE plus the developer tools such as javac, javap and jlink, which is everything needed to build one. JDK contains JRE, and JRE contains JVM.
JDK = tools (javac, javap, jlink, jar, jcmd…) + JREJRE = class libraries (java.base, java.sql…) + JVMJVM: class loader, runtime data areas, execution engine (interpreter + JIT), garbage collector
The picture is the whole difference. One more fact matters in 2026: since JDK 9 the JDK has no separate jre folder at all, and from Java 11 most vendors ship only the JDK, so “install the JRE” is advice you will see far more often than you can follow. The full-form answer, a comparison table, the architecture, a real javap listing and a real JIT trace are below, all from JDK 25.
Versions. Every program on this page was compiled and run on JDK 25.0.4.1 (Temurin, LTS, build 25.0.4.1+1-LTS). Where Java 8 behaved differently the text says so. The compiler errors and console output are quoted verbatim from those runs; the “Download” links at the end of each section point to the exact .java files. The jlink size comparison and javap listings come from the same JDK.

JDK vs JRE vs JVM: the comparison table

JVMJREJDK
Full formJava Virtual MachineJava Runtime EnvironmentJava Development Kit
PurposeExecutes bytecodeRuns Java programsBuilds and runs Java programs
ContainsClass loader, memory areas, execution engine, GCJVM + core class librariesJRE + javac, javap, jar, jlink, jcmd and more
Can compile?NoNoYes (javac)
Platform specific?Yes, one per OS and CPUYesYes
Who needs itNobody installs it aloneSomeone only running a programDevelopers and build servers
Separate download todayNoSome vendors only; none from Oracle since 11 (check your vendor)Yes, the default download
The last row is the one that surprises people, and it is why the page treats JRE as a concept rather than an installer. Here is what a real JDK 25 install contains. Note what is missing:
$ ls $JAVA_HOME
NOTICE
bin
conf
include
legal
lib
man
release
Output from the Temurin 25.0.4.1 install used for this article. There is no jre directory; the runtime is simply the whole thing. Its bin folder holds both the launcher you run programs with and the tools you build with:
$ ls $JAVA_HOME/bin
jar jarsigner java javac javadoc javap jcmd jconsole jdb jdeprscan jdeps jfr jhsdb jimage jinfo jlink jmap jmod jnativescan jpackage jps jrunscript jshell jstack jstat jstatd jwebserver keytool rmiregistry serialver
java is the JVM launcher; javac, javap, jlink and jdeps are the development tools that make it a JDK. A JRE would be this folder without most of the second group.

How Java code runs, from .java to native code

Java is both compiled and interpreted, which is the part most definitions skip. javac compiles source into bytecode, a compact instruction set for an imaginary machine, stored in .class files. The JVM then loads that bytecode, checks it, and executes it: first by interpreting, then, for methods that run often, by compiling them to native machine code with a JIT (just-in-time) compiler.
Hello.javasourcejavaccompiler.classbytecodeclass loadersload + verifyexecution engineinterpreter + JITC1 JIT (tier 3)quick, instrumentedC2 JIT (tier 4)slow, optimisedBuild time (JDK)Run time (JVM)javac and javap ship in the JDK; the class loaders and JIT ship inside the JVM, which is a part of every runtime.
The top row happens once, on your machine or build server. The bottom row happens every time the program runs, and the JIT is the reason a Java program usually gets faster after it warms up. The two JIT boxes are the real tiers this JDK uses; the next sections show each stage on a real class.

Stage 1: javac produces bytecode (javap shows it)

Here is a deliberately tiny class:
public class HelloJvm {
    private final String name;

    public HelloJvm(String name) {
        this.name = name;
    }

    public String greet() {
        return "Hello, " + name;
    }

    public static void main(String[] args) {
        System.out.println(new HelloJvm("JVM").greet());
    }
}
Source: HelloJvm.java. Compile it and ask javap to disassemble the methods:
$ javac HelloJvm.java && javap -c -p HelloJvm
Compiled from "HelloJvm.java"
public class HelloJvm {
  private final java.lang.String name;

  public HelloJvm(java.lang.String);
    Code:
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_1
         6: putfield      #7                  // Field name:Ljava/lang/String;
         9: return

  public java.lang.String greet();
    Code:
         0: aload_0
         1: getfield      #7                  // Field name:Ljava/lang/String;
         4: invokedynamic #13,  0             // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         9: areturn

  public static void main(java.lang.String[]);
    Code:
         0: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #8                  // class HelloJvm
         6: dup
         7: ldc           #23                 // String JVM
         9: invokespecial #25                 // Method "<init>":(Ljava/lang/String;)V
        12: invokevirtual #28                 // Method greet:()Ljava/lang/String;
        15: invokevirtual #32                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        18: return
}
Read the greet() method: getfield pushes the name field, invokedynamic builds the string concatenation, areturn returns it. Java source became stack-machine instructions, and nothing here is specific to Windows, Linux or any CPU. That portability is the “write once, run anywhere” claim in concrete form. The class file also records which Java version produced it:
$ javap -v HelloJvm | head -16
  Compiled from "HelloJvm.java"
public class HelloJvm
  minor version: 0
  major version: 69
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // HelloJvm
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 3, attributes: 3
Constant pool:
major version: 69 is Java 25 (Java 8 is 52). A JVM refuses a class file newer than it understands with an UnsupportedClassVersionError, which is the real-world reason “it works on my machine” fails when a build JDK is newer than the runtime JDK.

Stage 2: class loaders find and load classes

The JVM does not load every class up front. Class loaders fetch a class the first time it is needed. There are three built-in layers, and a small program can show you which one loaded what:
public class ClassLoaderDemo {
    public static void main(String[] args) throws Exception {
        System.out.println("String       loaded by: " + String.class.getClassLoader());
        System.out.println("java.sql.Driver by:    " + java.sql.Driver.class.getClassLoader());
        System.out.println("ClassLoaderDemo by:    " + ClassLoaderDemo.class.getClassLoader());
        System.out.println("parent of app loader:  " + ClassLoaderDemo.class.getClassLoader().getParent());
        Runtime rt = Runtime.getRuntime();
        System.out.println("max heap (MB):         " + rt.maxMemory() / (1024 * 1024));
        System.out.println("processors:            " + rt.availableProcessors());
    }
}
Source: ClassLoaderDemo.java.
$ javac ClassLoaderDemo.java && java ClassLoaderDemo
String       loaded by: null
java.sql.Driver by:    jdk.internal.loader.ClassLoaders$PlatformClassLoader@58644d46
ClassLoaderDemo by:    jdk.internal.loader.ClassLoaders$AppClassLoader@7c84929b
parent of app loader:  jdk.internal.loader.ClassLoaders$PlatformClassLoader@58644d46
max heap (MB):         1496
processors:            2
String prints null because the bootstrap loader, written in native code, has no Java object to print. java.sql.Driver comes from the platform loader. Your own class comes from the application loader, whose parent is the platform loader. Lookups go to the parent first, which is why you cannot replace java.lang.String by dropping your own copy on the classpath. (The @hash suffixes and heap size differ on every machine.)
Going deeper: how many classes does a trivial program load?

Starting HelloJvm with -Xlog:class+load and counting the lines gives a number in the hundreds:

$ java -Xlog:class+load HelloJvm | grep -c "source:"
618

The first lines show most come “from the shared objects file”, a pre-built class-data-sharing archive that makes startup fast:

$ java -Xlog:class+load HelloJvm | grep -E "HelloJvm|java.lang.String "
[0.021s][info][class,load] java.lang.String source: shared objects file
[0.052s][info][class,load] HelloJvm source: file:/tmp/work/cls2/

Your class is the one loaded from a plain file: path.

Stage 3: the execution engine, interpreter and JIT

Bytecode starts out interpreted, which is quick to begin and slow to run. The JVM counts how often each method and loop runs, and compiles the hot ones. This program calls a four-byte method 200,000 times:
public class JitDemo {
    static int square(int x) { return x * x; }

    public static void main(String[] args) {
        System.out.println("java.version      = " + System.getProperty("java.version"));
        System.out.println("java.vm.name      = " + System.getProperty("java.vm.name"));
        System.out.println("java.class.version= " + System.getProperty("java.class.version"));
        System.out.println("Runtime.version() = " + Runtime.version());
        long sum = 0;
        for (int i = 0; i < 200_000; i++) {
            sum += square(i);
        }
        System.out.println("sum = " + sum);
    }
}
Source: JitDemo.java. Run it normally, then with -XX:+PrintCompilation and filter to our own methods:
$ java JitDemo.java
java.version      = 25.0.4.1
java.vm.name      = OpenJDK 64-Bit Server VM
java.class.version= 69.0
Runtime.version() = 25.0.4.1+1-LTS
sum = 17450708985056
$ java -XX:+PrintCompilation JitDemo | grep -E "JitDemo::"
80   70       3       JitDemo::square (4 bytes)
80   71       4       JitDemo::square (4 bytes)
81   70       3       JitDemo::square (4 bytes)   made not entrant: not used
83   72 %     3       JitDemo::main @ 69 (102 bytes)
Read the columns as: milliseconds since start, compile id, the tier number, then the method. square was compiled at tier 3 by C1 almost at once, then recompiled at tier 4 by C2 as it kept running, and the tier-3 version was marked made not entrant once the better one existed. The % on the main line means “on-stack replacement”: the loop was hot enough that the JVM swapped in compiled code in the middle of running it. Exact numbers and ids change from run to run; the sequence is what to take away.

The JVM’s insides: runtime data areas

Shared by every threadHeapobjects, arrays, string poolMetaspaceclass metadata, method dataCode cacheJIT-compiled native codeOne set per threadJava stackone frame per method callPC registernext instructionNative method stackfor native codeObjects live on the heap and are garbage collected; each thread has its own stack, which is why local variables are thread-safe.
The top row is shared, the bottom row is private to each thread. That split explains two common interview answers: why local variables need no locking (they live on a thread’s own stack) and why shared mutable objects do (they live on the shared heap), the theme of Top 40 Java Concurrency Interview Questions. The heap is where collections and strings live; see Top 40 Java Collections Interview Questions for how HashMap uses it and 30 Java String Programs for Interviews for the string pool.
Metaspace, not PermGen. Before Java 8 class metadata lived in a fixed-size “PermGen” area that famously caused OutOfMemoryError: PermGen space. Java 8 replaced it with Metaspace in native memory, and interviewers still ask about PermGen. The honest answer for any current JDK is that PermGen no longer exists.

Why the JRE stopped shipping separately

Until Java 8 the JDK installed a JRE inside itself (a jre folder) and a user could also download the JRE alone. Java 9 broke the runtime into modules, and java --list-modules on this JDK shows 69 of them. Once the runtime is modular, a fixed “one size JRE” stops making sense: an application can declare which modules it needs and ship exactly those. The tool that does it is jlink. Here it builds a runtime containing only java.base, then runs the same HelloJvm class on it:
$ jdeps --print-module-deps HelloJvm.class
java.base
$ jlink --add-modules java.base --strip-debug --no-header-files --no-man-pages --output rt
$ du -sh rt $JAVA_HOME
56M	rt
303M	/tmp/tools/jdk-25.0.4.1+1
$ rt/bin/java --list-modules
[email protected]
$ rt/bin/java -cp . HelloJvm
Hello, JVM
jdeps reported that the class needs only java.base. The custom runtime is 56 MB against 303 MB for the full JDK, contains one module, and ran the program. That is the modern replacement for a JRE: a runtime built for one application. From Java 11 onward Oracle’s own downloads are JDK-only, but this is a vendor decision, not a Java rule, and some vendors still publish JRE-only packages (the Adoptium download API answered with a redirect for a Java 25 JRE when I checked). If you need a small runtime, use jlink or a container base image built from one.
Download the code. HelloJvm.java · JitDemo.java · ClassLoaderDemo.java.

Run a runnable one with java File.java (JDK 11 or later; JDK 25 recommended). Files starting with Broken are meant to fail to compile.

Common interview questions

1. What is the difference between JDK, JRE and JVM?

JVM executes bytecode. JRE is the JVM plus the standard libraries needed to run a program. JDK is the JRE plus development tools such as javac, javap and jlink. JDK contains JRE contains JVM.

2. What is the full form of JDK, JRE and JVM?

Java Development Kit, Java Runtime Environment and Java Virtual Machine.

3. Is Java compiled or interpreted?

Both. javac compiles source to bytecode; the JVM interprets that bytecode and JIT-compiles hot methods to native code at run time.

4. What is bytecode and why does it make Java portable?

Bytecode is the instruction set of the JVM, stored in .class files. The same bytecode runs on any OS and CPU that has a JVM, because the JVM, not the bytecode, is platform-specific.

5. Is the JVM platform independent?

No. The bytecode is platform independent; each platform needs its own JVM build, which is why there are separate downloads for Windows, Linux and macOS.

6. What are the main components of the JVM?

The class loader subsystem, the runtime data areas (heap, metaspace, thread stacks, PC registers, native stacks), and the execution engine (interpreter, JIT compilers and garbage collector).

7. What is the JIT compiler?

A compiler inside the JVM that turns frequently executed bytecode into native machine code during execution. HotSpot has C1 (fast, lightly optimised) and C2 (slower, heavily optimised) and moves hot code from one to the other, visible with -XX:+PrintCompilation.

8. What are the types of class loaders?

Bootstrap (core classes), platform (other JDK modules) and application (your classpath). Delegation goes to the parent first.

9. Where are objects and local variables stored?

Objects and arrays are on the heap, shared between threads. Local variables and call frames are on each thread’s own stack.

10. What happened to PermGen?

Java 8 replaced it with Metaspace, which lives in native memory and grows on demand.

11. Why is there no standalone JRE download from Java 11?

Because Java 9 modularised the runtime and added jlink, so applications can build a runtime containing only the modules they use. Oracle stopped offering a JRE-only download from Java 11; some vendors still publish one.

12. How do you find which Java version a .class file needs?

Run javap -v File and read major version (52 for Java 8, 69 for Java 25) or run it and read the UnsupportedClassVersionError message.

Common mistakes

  • Installing a JRE to compile. javac only exists in a JDK. “javac is not recognized” almost always means a runtime-only install or a stale PATH.
  • Different JDK for build and run. Compiling with a newer JDK than you run on gives UnsupportedClassVersionError. Compile with --release N for the oldest target.
  • Saying Java is only interpreted, or only compiled. Both stages exist; mention bytecode and the JIT.
  • Saying the JVM is platform independent. The bytecode is; the JVM is not.
  • Quoting PermGen for modern JDKs. It was removed in Java 8.
  • Benchmarking a cold program. The first calls run in the interpreter; measure after warm-up (or use JMH).

FAQ

What is the difference between JDK, JRE and JVM?

The JVM runs bytecode, the JRE is the JVM plus the class libraries needed to run a program, and the JDK is the JRE plus tools such as javac, javap and jlink for building programs.

What is the full form of JDK, JRE and JVM?

JDK is Java Development Kit, JRE is Java Runtime Environment and JVM is Java Virtual Machine.

JDK JRE JVM diagram: how do they relate?

They are nested: the JDK contains the JRE, and the JRE contains the JVM. The JDK adds development tools, the JRE adds the standard libraries, and the JVM contains the class loader, memory areas and execution engine.

Do I need to install the JRE separately?

Usually not. Install a JDK, which includes everything needed to run Java programs. Since JDK 9 the JDK has no separate jre folder, and if you want a small runtime you can build one with jlink.

Which is bigger, JDK or JRE?

The JDK, because it includes the JRE plus the development tools. On Temurin 25 the full JDK is about 303 MB, while a jlink runtime containing only java.base was 56 MB.

Where to go next

Other articles in this Java fundamentals series:

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.