Three abbreviations that sound interchangeable describe three genuinely different things: the program you write, the machine that executes it, and the toolset that produces it. Getting them straight explains why some machines can run Java programs but not build them, and why a container image needs more than you might expect.
What is the JVM?
The Java Virtual Machine is the program that executes compiled Java code. Your source file is translated into bytecode — an intermediate instruction set designed for one platform — and the JVM loads those instructions, checks them, and executes them, translating them into the instructions of whatever machine it happens to be running on.
That indirection is the point. It is why a program compiled once runs on any machine with a compatible JVM, and it is also why the JVM is allowed to do things a native compiler cannot — for example, verifying that an array access is in bounds before it happens, and deallocating memory that is no longer reachable without the program asking.
What is the JRE?
The Java Runtime Environment is the JVM plus the standard class library — the thousands of built-in classes that give you collections, networking, file handling, dates, and everything else you would otherwise have to write yourself — plus the supporting runtime machinery.
Together they are the minimum needed to run a Java program and nothing more. A machine with only the JRE can execute the Greeting.class file you copied onto it, but it cannot compile your source, because there is no compiler in the box.
What is the JDK?
The Java Development Kit is the JRE plus the development tools: javac, javadoc, jshell, jar, jlink and the debugging tools. It is what a developer installs and what a build server needs.
Here is how the pieces line up when you run a program.
Greeting.java --javac--> Greeting.class --java--> JVM --> your program runs
(JDK) (JRE = JVM + standard library)
Read the arrows as questions: "what tool turns source into bytecode?" — the JDK's compiler. "What executes the bytecode?" — the JVM. "What supplies the built-in classes the program uses?" — the standard library that ships with the runtime.
How can I see the difference for myself?
Compile a class, then look at the file the compiler produced. The first four bytes of every class file are the same magic number, and reading them proves that the output really is a structured binary rather than text.
import java.nio.file.Files;
import java.nio.file.Path;
public class Magic {
public static void main(String[] args) throws Exception {
byte[] header = new byte[4];
try (var in = Files.newInputStream(Path.of(args[0]))) {
in.readNBytes(header, 0, 4);
}
int magic = ((header[0] & 0xFF) << 24) | ((header[1] & 0xFF) << 16)
| ((header[2] & 0xFF) << 8) | (header[3] & 0xFF);
System.out.printf("first four bytes: %02X %02X %02X %02X%n",
header[0], header[1], header[2], header[3]);
System.out.println("looks like a class file: " + (magic == 0xCAFEBABE));
}
}
Run it against a class you compiled and the answer is true. What the JVM executes is not your source text, it is this binary form — and that is the whole reason the same bytecode can run on a server, a laptop and a phone.
What do I install where?
- On your machine: the JDK. You need the compiler every day, and the runtime comes with it.
- In a build pipeline: the JDK, because the pipeline compiles before it tests.
- In a container that only runs a finished artifact: a runtime is enough — but modern build tools often want a JDK anyway, so check what the tool expects.
- Never install a JRE and expect
javacto exist. That is the mistake that produces the "javac is not recognized" error on a machine wherejavaworks fine.
Modern toolchains have blurred the line further: some distributions ship a single installer that only calls itself a JDK, and some build outputs bundle their own trimmed runtime so the application does not care what is installed on the host at all. The concepts still describe what is happening underneath, even when the packaging changes.
Can you see all three layers in one example?
Yes, and it is worth doing once so the three names stop competing for the same space in your head. Write a small class, compile it, inspect the artifact, and then run it. Each step belongs to a different component, and the tool you use tells you which one you are looking at.
// 1. the JDK compiles source into bytecode
$ javac -d out src/com/example/Greeting.java
// 2. the artifact is a binary file, not text
$ file out/com/example/Greeting.class
out/com/example/Greeting.class: compiled Java class data, version 61.0
// 3. the JRE runs it
$ java -cp out com.example.Greeting
Hello from Java
// 4. and you can see the instructions the JVM will execute
$ javap -c -p -cp out com.example.Greeting
Step two is the one that surprises people: a class file is binary, and utilities like file recognise it as such. Step four goes further — javap disassembles the class and prints the bytecode instructions, which is a genuinely useful debugging tool when you need to know what a compiler actually produced.
// and the runtime can tell you which implementation loaded it
public class Runtime {
public static void main(String[] args) {
System.out.println("vm name : " + System.getProperty("java.vm.name"));
System.out.println("vendor : " + System.getProperty("java.vm.vendor"));
String home = System.getProperty("java.home");
System.out.println("runtime dir holds a jre folder: "
+ new java.io.File(home, "jre").isDirectory());
}
}
The java.home property points at the runtime that is executing your code, and whether that directory holds a jre folder tells you which layout you are on. That single property is enough to answer "which runtime am I actually using?" on a machine with several installed — a question that comes up constantly in CI logs, where the answer decides whether your build tested what you thought it tested.
Quick reference
- JVM — the virtual machine that executes bytecode.
- JRE — JVM plus the standard class library; enough to run, not to build.
- JDK — JRE plus the development tools; what you install.
- Class files start with the magic bytes
CA FE BA BE. - "java works, javac does not" means a runtime is installed where a JDK is needed.
Try it yourself: compile a class, run the magic-number program on it, and then run it on a text file to watch the check fail. Then find the java launcher and the javac compiler on your machine, note which directory each lives in, and write down which of the three components each one belongs to.