Java is neither fully compiled nor fully interpreted. It sits in the middle: source code is compiled into bytecode, and the JVM interprets or compiles that bytecode into native machine code at runtime. Understanding that two-step process explains why Java is portable, why compilation catches errors early, and why startup is slower than a pure scripting language. Every professional Java developer should be able to trace this pipeline in their head.
Step 1: write .java source
You write human-readable code in a file ending in .java. This is plain text — you could write it in Notepad — and it contains classes, methods, and statements. The compiler does not care about indentation or comments; it cares about syntax and types. Every public class must live in a file whose name matches the class exactly, including case. public class Main must be in Main.java; main.java will not compile.
Step 2: javac compiles to .class bytecode
The command javac Main.java reads your source and produces Main.class. That .class file is not machine code your CPU understands directly — it is bytecode, a compact intermediate instruction set designed for the JVM. Bytecode is platform-independent: the same .class file can run on Windows, macOS, or Linux without modification. The trade-off is that something else — the JVM — must translate those instructions into real CPU operations at runtime.
flowchart LR SRC["Main.java (source)"] --> JC["javac (compiler)"] JC --> CLS["Main.class (bytecode)"] CLS --> JVM["JVM loads and verifies"] JVM --> INT["Interpreter (cold code)"] JVM --> JIT["JIT compiler (hot code)"] INT --> OUT["Program output"] JIT --> OUT style JC fill:#c2410c,color:#fff style JVM fill:#c2410c,color:#fff style JIT fill:#c2410c,color:#fff
public class Main {
public static void main(String[] args) {
int a = 4;
int b = 6;
int sum = a + b;
System.out.println("Sum: " + sum);
System.out.println("Double: " + (sum * 2));
}
}
Step 3: the JVM loads and executes
The command java Main starts the JVM, which loads Main.class, verifies it, and begins executing the bytecode. Early in execution the JVM may use an interpreter to run bytecode line by line; for code that runs frequently, a JIT compiler translates the bytecode into native machine code so it runs at full speed.
The verification step is strict: the JVM checks that the bytecode does not violate type safety, does not access illegal memory, and does not corrupt the stack. This is part of Java's security story — untrusted code cannot harm the host process.
flowchart TD LOAD["Class loader"] --> VERIFY["Bytecode verifier"] VERIFY --> EXEC["Interpreter / JIT"] style LOAD fill:#c2410c,color:#fff style VERIFY fill:#166534,color:#fff style EXEC fill:#1e293b,color:#fff
The JIT compiler and warm-up
The Just-In-Time (JIT) compiler watches which methods run most often and translates their bytecode into highly optimised native code. After JIT compilation, those hot methods run at roughly the same speed as C or C++. The catch is warm-up time: the JVM must observe your code long enough to decide what is worth compiling.
For long-running servers this cost is negligible; for short scripts it is why Java feels heavy compared to Python. The JIT is why Java benchmarks always specify a warm-up period — measuring cold JVM performance tells you nothing about steady-state speed.
public class Main {
static int doubleIt(int n) {
return n * 2;
}
public static void main(String[] args) {
int x = 5;
System.out.println(doubleIt(x));
System.out.println(doubleIt(x - 2));
}
}
Classpath and class loading
When the JVM starts, it looks for classes on the classpath — a list of directories and .jar files where compiled classes live. The JVM does not search your entire hard drive; it looks only where you tell it to. A ClassNotFoundException almost always means the class exists but the classpath is wrong.
Classes are loaded on demand: the JVM loads Main first, then loads any class Main references only when that reference is actually reached during execution. Understanding this lazy loading model helps explain why a missing dependency only crashes when the code path that needs it is reached, not at startup.
flowchart TD A["Write Main.java"] --> B["javac Main.java"] B --> C["java Main"] C --> D["Edit Main.java"] D --> E["javac Main.java<br/>(don't forget!)"] E --> C style B fill:#166534,color:#fff style E fill:#b45309,color:#fff
What bytecode looks like
You don't need to read bytecode fluently, but peeking at it once is illuminating. After compiling, javap -c Main disassembles the .class file and shows instructions like iconst_2, istore_1, and iadd. These are stack-based operations: push a value, store it, add two values. The JVM executes these instructions against its own stack machine, which is why the same bytecode runs on ARM, x86, or any architecture with a JVM port.
What does this program print? Read it carefully and type the exact output (two lines).
public class Main {
public static void main(String[] args) {
int a = 4;
int b = 6;
int sum = a + b;
System.out.println("Sum: " + sum);
System.out.println("Double: " + (sum * 2));
}
}
a + bis4 + 6 = 10.sum * 2is10 * 2 = 20.Each value is joined with its label using
+.
What is the output of javac Main.java?
javac translates source code into bytecode stored in a .class file. That bytecode is not native machine code — it is designed to be executed by the JVM, which makes it portable across platforms.
Trace this simple program through the pipeline. Predict the exact two-line output.
public class Main {
public static void main(String[] args) {
int x = 3;
int y = x * 4;
System.out.println(y);
System.out.println(y + x);
}
}
x is 3, so y = x * 4 is 12.
y + x is 12 + 3 = 15.
Each println produces one line of output.
The JVM starts at main and follows method calls. Predict the exact two-line output.
public class Main {
static int triple(int n) {
return n * 3;
}
public static void main(String[] args) {
int a = 2;
System.out.println(triple(a));
System.out.println(triple(a + 1));
}
}
triple(2) returns 2 * 3 = 6.
triple(a + 1) is triple(3), which returns 9.
The JVM compiles both methods to bytecode and calls them from main.
You edit Main.java and run java Main, but the output does not reflect your changes. What is the most likely cause?
java runs the existing .class file. If you edit .java without recompiling, the old bytecode still executes. Always run javac after changing source.
Recap
- Java source (
.java) is compiled byjavacinto platform-independent bytecode (.class). - The JVM loads, verifies, and executes bytecode, translating hot paths to native code via the JIT compiler.
- Verification catches type and memory safety violations before execution begins.
- The classpath tells the JVM where to find classes;
ClassNotFoundExceptionmeans the classpath is wrong. - Always recompile after editing source, or
javawill run the old bytecode.
Next you will learn how Java represents text with Strings, and why == on a String rarely does what you expect.