The question everyone gets half right
"What is the difference between JDK, JRE and JVM" is asked in almost every Java viva and written test. Most students answer "JDK is for development, JRE is for running". True, and not enough. Here is the full picture, because it also explains why Java behaves the way it does.
Java does not compile to machine code
A C compiler produces instructions for one specific CPU and one operating system. Compile on Linux, and the binary will not run on Windows.
javac produces something different: bytecode. Bytecode is the instruction set of an imaginary machine, and it is identical on every platform. Your .class file is the same bytes on a laptop, a server and an Android build machine.
Something on the target machine then has to execute that bytecode. That something is the JVM.
JVM: the Java Virtual Machine
The JVM is a program that reads bytecode and executes it. It is written in C++ and it is different for every platform — there is a Windows JVM, a Linux JVM, a macOS JVM. Each one understands the same bytecode.
That is the whole "write once, run anywhere" claim, and it is precise: the bytecode is portable because the JVM is not.
The JVM does four jobs.
- Class loading — finds
.classfiles and loads them into memory when first used, not before. - Bytecode verification — checks the bytecode is safe before running it. This is why a Java program cannot corrupt memory the way a C program can.
- Execution — interprets the bytecode, and the JIT compiler (just-in-time) converts frequently-run methods into real machine code while the program runs. That is why a Java loop is slow for the first few thousand iterations and fast afterwards.
- Garbage collection — automatically frees objects nothing refers to any more. No
free, nodelete.
JRE: the Java Runtime Environment
JRE = JVM + the standard class libraries.
Your program calls System.out.println, ArrayList, String. That code has to exist somewhere. It ships in the JRE. So the JRE is what a user who only wants to run Java programs needs.
JDK: the Java Development Kit
JDK = JRE + development tools.
The tools are javac (the compiler), jar (the packager), javadoc (the documentation generator), jdb (the debugger), and jshell (an interactive shell, from Java 9 onward).
The nesting, from biggest to smallest:
| Contains | JDK | JRE | JVM |
|---|---|---|---|
Compiler javac |
yes | no | no |
| Class libraries | yes | yes | no |
| The execution engine | yes | yes | yes |
One sentence for the viva: the JDK compiles, the JRE supplies the libraries, the JVM executes.
From Java 11 onward Oracle stopped shipping a separate JRE download. You install the JDK, and you build a trimmed runtime with jlink if you need one. The three concepts are still exactly as described, and are still examined.
The full journey of one program
public class Hello {
public static void main(String[] args) {
System.out.println("Hello from the JVM");
}
}
Save it as Hello.java. Then:
javac Hello.java— the compiler checks your syntax and types and writesHello.class, which contains bytecode, not machine code.java Hello— note: no.classextension. This starts a JVM, which loadsHello.class, verifies it, findsmain, and runs it.
From Java 11 you can also run java Hello.java directly for a single file; it compiles in memory and runs. Handy for practice, not for projects.
You can read the bytecode yourself:
/* After compiling, run:
*
* javap -c Hello
*
* Part of the output:
* 0: getstatic #2 // Field java/lang/System.out
* 3: ldc #3 // String Hello from the JVM
* 5: invokevirtual #4 // Method println
* 8: return
*/
public class Hello {
public static void main(String[] args) {
System.out.println("Hello from the JVM");
}
}
Do this once. Seeing that your println became four instructions makes bytecode real instead of a word in a definition.
The public class name must match the file name exactly, including capital letters. public class Hello must be in Hello.java. Save it as hello.java and javac refuses with "class Hello is public, should be declared in a file named Hello.java". On Windows this sometimes appears to work because the file system ignores case; on the Linux machine in your lab it will not.
Where memory lives
The JVM divides its memory into areas, and two of them come up constantly.
Heap — every object you create with new lives here. It is shared by all threads and it is what the garbage collector cleans.
Stack — one per thread. Holds method frames: local variables, parameters, and where to return. Method call, frame pushed; method return, frame popped. A runaway recursion fills it and you get StackOverflowError.
public class Memory {
public static void main(String[] args) {
int x = 5; // x is on the stack
String s = new String("hi"); // the object is on the heap,
// s (the reference) is on the stack
int[] a = new int[3]; // array object on the heap
System.out.println(x + " " + s + " " + a.length);
}
}
x holds the value 5 itself. s and a hold references — the address of an object on the heap. That difference decides how assignment, == and method calls behave, and it is the subject of lesson 3.
Garbage collection in one paragraph
When no reference to an object remains, the object is unreachable and the garbage collector may free it. You never write free. You also cannot force collection — System.gc() is a suggestion the JVM can ignore.
This removes the entire category of C bugs: dangling pointers, double free, most leaks. Java can still leak, in one specific way: keeping a reference you no longer need, typically in a long-lived collection. The object is still reachable, so the collector must keep it. "I put things into a static list and never removed them" is the standard Java leak.