JDK, JRE and the JVM

Know what the JDK, the JRE and the JVM each do, which one your laptop and your server need, and how source code becomes running bytecode.

Syntaxjavac -d out src/Main.java # JDK: compile to bytecode java -cp out Main # JVM: run the class file java -version # which runtime is on your PATH

Three names come up as soon as you install Java. They are layers, each one containing the next.

  • JVM (Java Virtual Machine): the program that runs compiled Java. It loads class files, verifies them, interprets the bytecode, compiles hot methods to machine code with its just-in-time (JIT) compiler and manages memory with a garbage collector.
  • JRE (Java Runtime Environment): the JVM plus the standard class library (java.lang, java.util, java.net.http and the rest). Enough to run a program, not to build one.
  • JDK (Java Development Kit): the runtime plus the tools: javac (compiler), jar, jshell, javadoc, jlink, jcmd and others.

From source to running code

javac turns .java files into .class files containing bytecode: instructions for the JVM, not for your CPU. The same class file runs on Linux, macOS and Windows. Each class file records the version it was compiled for; Java 25 writes class file version 69, and an older JVM refuses to load it with an UnsupportedClassVersionError. That error in a deploy log almost always means the build used a newer JDK than the server.

Which one do you install?

On your machine, install a JDK. On servers and in containers you only need a runtime. Oracle stopped shipping a separate JRE with Java 11, but some distributions, such as Eclipse Temurin, still publish JRE builds and container images. You can also build a trimmed runtime containing only the modules your service needs with jlink, which a later lesson covers.

Distributions

OpenJDK is the open-source project; Eclipse Temurin, Amazon Corretto, Oracle JDK, Azul Zulu and the Microsoft Build of OpenJDK are builds of it. For the same version they behave the same for application code. Pick one with the support and licence terms your team wants, and use the same feature version in development, CI and production.

Mistakes people make

  • Having several JDKs installed and running a different one than expected. java -version and javac -version should print the same feature number.
  • Assuming the JVM sees the whole host. In a container, the JVM reads the container's CPU and memory limits and sizes the heap and garbage collector from them.
  • Treating the first requests' speed as the steady state. The JIT compiles hot code while the service runs, so latency usually drops after warm-up.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Diagnose an UnsupportedClassVersionError after a CI pipeline upgraded its JDK but the servers did not.
  • Choose a JDK distribution and pin the same feature version in development, CI and the container image.
  • Fail a service's startup early with a clear log line when it is launched on an unsupported Java version.

More examples

Compile a class and read its bytecode header

Every class file starts with the bytes CAFEBABE followed by its version, and 69 is Java 25. A JVM older than 25 rejects this file, which is what UnsupportedClassVersionError reports.

Example · java
void main() throws Exception {
    Path dir = Files.createTempDirectory("jdk-demo");
    Path source = dir.resolve("Greeter.java");
    Files.writeString(source, "class Greeter { String hello() { return \"hi\"; } }");

    // The same compiler the javac command runs, called in-process.
    var javac = java.util.spi.ToolProvider.findFirst("javac").orElseThrow();
    int exitCode = javac.run(System.out, System.err, "-d", dir.toString(), source.toString());
    IO.println("javac exit code: " + exitCode);

    byte[] bytes = Files.readAllBytes(dir.resolve("Greeter.class"));
    IO.println("magic: %02X%02X%02X%02X".formatted(bytes[0], bytes[1], bytes[2], bytes[3]));
    IO.println("class file version: " + (((bytes[6] & 0xFF) << 8) | (bytes[7] & 0xFF)));

    Files.delete(dir.resolve("Greeter.class"));
    Files.delete(source);
    Files.delete(dir);
}

// Output:
// javac exit code: 0
// magic: CAFEBABE
// class file version: 69

Check a minimum Java version at startup

Runtime.Version parses and compares version strings correctly, unlike comparing them as text. A service can fail fast with a clear message instead of an obscure error later.

Example · java
boolean supported(Runtime.Version running, Runtime.Version required) {
    return running.compareTo(required) >= 0;
}

void main() {
    Runtime.Version required = Runtime.Version.parse("25");
    for (String candidate : List.of("21.0.8", "25.0.1+8", "27")) {
        Runtime.Version v = Runtime.Version.parse(candidate);
        IO.println(candidate + " feature=" + v.feature() + " update=" + v.update()
            + " supported=" + supported(v, required));
    }
    IO.println("this JVM supported=" + supported(Runtime.version(), required));
}

// Output:
// 21.0.8 feature=21 update=8 supported=false
// 25.0.1+8 feature=25 update=1 supported=true
// 27 feature=27 update=0 supported=true
// this JVM supported=true

The JVM loads classes only when they are first used

The static initialiser runs once, at the first use of Config, not at startup. Lazy loading is part of why large services spend their first seconds loading classes, and why the first request can be slower than the rest.

Example · java
class Config {
    static {
        IO.println("Config class loaded");
    }

    static String databaseUrl() {
        return "jdbc:postgresql://db.internal/app";
    }
}

void main() {
    IO.println("main started");
    IO.println(Config.databaseUrl());
    IO.println(Config.databaseUrl());
}

// Output:
// main started
// Config class loaded
// jdbc:postgresql://db.internal/app
// jdbc:postgresql://db.internal/app

Discussion

  • Be the first to comment on this lesson.