Before you can write a line of Java, you need to know what is actually running it. Java's toolchain is split into three layers — the JDK, the JRE, and the JVM — and each has a distinct job. Most "it works on my machine" disasters trace back to mixing them up: a program that compiles on your laptop refuses to start on a server, or a build fails because the wrong layer was installed. Knowing which layer does what turns those mystery errors into a one-line fix.
JDK — the developer kit
The Java Development Kit (JDK) is everything a developer needs to write, compile, and debug Java. Its headline tool is the compiler javac, which turns your .java source into bytecode. It also ships a launcher (java), a debugger, the javadoc documentation generator, and javap for inspecting compiled classes. Crucially, the JDK contains the JRE and the JVM nested inside it — so installing a JDK gives you the whole stack. If you are writing or building code, this is the layer you install.
JRE — the runtime
The Java Runtime Environment (JRE) is the layer for a machine that only needs to run Java, never build it. It bundles the JVM together with the standard libraries — the ready-made classes every program leans on — but it ships no compiler. With a JRE you can launch an existing .class file; you cannot create one from .java source. A production server that only ever runs your finished app needs exactly this layer and nothing more.
JVM — the engine
The Java Virtual Machine (JVM) is the process that actually executes your bytecode. It is platform-specific — there is a separate JVM build for Windows, macOS, and each Linux distribution — yet the bytecode it consumes is identical everywhere. That asymmetry is Java's founding promise, write once, run anywhere: the same .class produced on one operating system runs unchanged on any other that has a matching JVM. The JVM also verifies bytecode before running it, which blocks a whole family of low-level memory attacks.
flowchart TD JDK["JDK (development)"] --> JRE["JRE (runtime)"] JRE --> JVM["JVM (execution)"] JDK --> DEV["javac · debugger · javadoc"] JRE --> LIB["Standard libraries"] JVM --> BYTE["Bytecode interpreter / JIT"] style JDK fill:#c2410c,color:#fff style JRE fill:#c2410c,color:#fff style JVM fill:#c2410c,color:#fff
flowchart LR S["Main.java<br/>source code"] --> C["javac<br/>compiler"] C --> B["Main.class<br/>bytecode"] B --> V["JVM<br/>verifier + JIT"] V --> O["program runs"] C -.->|"type errors<br/>stop here"| X["build fails"] style C fill:#c2410c,color:#fff style V fill:#c2410c,color:#fff style X fill:#b91c1c,color:#fff
Compile, then run
Here is the journey of one source file. javac reads Main.java, checks every type, and — if all is well — writes Main.class, a file of compact bytecode instructions. Nothing runs yet; you have only built it. Launching java Main hands that bytecode to the JVM, which loads it, confirms it is well-formed, and executes it statement by statement. The JVM also carries a just-in-time compiler that hot-spots busy code into native machine instructions for speed.
public class Main {
public static void main(String[] args) {
String tool = "javac";
int year = 1995;
System.out.println("Tool: " + tool);
System.out.println("Java appeared in " + year);
}
}
One source file, several classes
A single .java file can declare more than one class, though only one may be public and it must share the file's name. Feed that file to javac and you get a separate .class for every class — not one merged file. That one-to-one mapping between classes and bytecode files is exactly the set of files the classpath later searches through.
public class Main {
public static void main(String[] args) {
Greeter g = new Greeter("Java");
g.sayHi();
}
}
class Greeter {
String who;
Greeter(String who) {
this.who = who;
}
void sayHi() {
System.out.println("Hi from " + who);
}
}
The classpath
When the JVM starts, it needs to know where your compiled .class files live. The classpath is a list of directories and JAR files the JVM searches whenever it encounters a class name. By default the classpath is the current directory, which is enough for a single throwaway file. As soon as you organise code into packages or pull in external libraries, you must set the classpath explicitly — either with the -cp flag on the command line or the CLASSPATH environment variable.
flowchart TD
CMD["java -cp build Main"] --> Q{"is Main.class<br/>on the classpath?"}
Q -->|"yes — found in build/"| OK["class loaded, runs"]
Q -->|"no — wrong folder"| MISS["ClassNotFoundException"]
style Q fill:#3776ab,color:#fff
style OK fill:#15803d,color:#fff
style MISS fill:#b91c1c,color:#fff
Why classpath errors bite at runtime
The classpath is searched lazily, only when the JVM first needs a class it has not yet loaded. That is why a program can build perfectly yet crash on launch: javac found everything at compile time using one classpath, but java is handed a different one and cannot locate a class it needs. With packages the rule is strict and mechanical — a class declared as com.example.app.Main must live at com/example/app/Main.class relative to a classpath root. Move the file, or forget the root, and the JVM will not find it no matter how correct the code is.
package com.example.app;
public class Main {
public static void main(String[] args) {
System.out.println("running from a package");
}
}
What does this program print? Read it carefully and type the exact output (two lines).
public class Main {
public static void main(String[] args) {
String tool = "javac";
int year = 1995;
System.out.println("Tool: " + tool);
System.out.println("Java appeared in " + year);
}
}
toolis the string"javac", joined with"Tool: ".yearis the integer1995, joined with"Java appeared in ".
Method-driven output. This program calls a helper method twice and uses each result differently. Predict both lines.
public class Main {
static String toolchain() {
return "jdk -> jre -> jvm";
}
public static void main(String[] args) {
System.out.println(toolchain().toUpperCase());
System.out.println(toolchain().length());
}
}
toUpperCase()converts every letter in the returned string to capitals.length()counts characters — count the letters, spaces, and the arrow symbols together.
Which component includes the compiler javac?
The compiler lives in the JDK. The JRE and JVM are for running code, not building it.
Your project compiles cleanly with javac, but at runtime java -cp build Main throws NoClassDefFoundError for a helper class. What is the most likely cause?
If it compiled, the code is valid. A NoClassDefFoundError at launch means the JVM could not locate an already-compiled class — a classpath problem, not a syntax one.
You compile Game.class on Linux and copy only that .class file to a Windows machine with a JVM installed. It runs without recompiling. What makes that possible?
javac emits platform-neutral bytecode. The JVM is the platform-specific piece — there is one per OS — so the same .class runs anywhere a matching JVM exists, with no recompilation.
Recap
- The JDK builds code (
javac, debugger); it contains the JRE and the JVM. - The JRE runs finished bytecode plus the standard libraries, but ships no compiler.
- The JVM executes bytecode — platform-specific, while the bytecode itself is not.
javacturns one.javainto.class; one source file can declare several classes, each becoming its own.class.- The classpath is where the JVM looks for classes — set it with
-cp, and packages must mirror the directory structure. - A
ClassNotFoundExceptionat runtime points at the classpath, not the code.
Next you will open a class of your own and start writing the methods that live inside it.