Why this matters
- It explains the single most repeated phrase about Java — write once, run anywhere — in terms of real artefacts instead of marketing.
- Almost every later topic (class loading, garbage collection, the JIT compiler) is a detail of the step where bytecode becomes machine code.
- When a build works on your laptop and fails on a server, knowing which half of the pipeline produced the artefact tells you immediately where to look.
The two-step pipeline
Languages like C compile straight to machine code for one specific processor and operating system. That binary is fast but it only runs where it was built. Java splits the journey in two and puts a portable format in the middle.
What happens, in order
- You write source code in a
.javafile. This is plain text, readable by anyone. javaccompiles it into a.classfile holding bytecode — compact instructions for a machine that does not physically exist.- The
javalauncher starts a JVM, which loads that bytecode and converts it into instructions your actual processor understands. - The same
.classfile is handed to a Windows JVM, a Linux JVM or a macOS JVM. Each one produces different machine code from identical input.
Seeing it for yourself
The compile and run steps are genuinely separate, and you can watch the intermediate file appear.
// Greeting.java
public class Greeting {
public static void main(String[] args) {
System.out.println("Compiled once, runs anywhere.");
}
}
javac Greeting.java # produces Greeting.class — bytecode, not machine code
java Greeting # the JVM loads the bytecode and runs it
Bytecode is not secret. Ask the JDK to print it back as text and you can see the stack-based instruction set the JVM is designed around:
javap -c Greeting
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out
3: ldc #13 // String Compiled once, runs anywhere.
5: invokevirtual #15 // Method java/io/PrintStream.println
8: return
Those four instructions are what ships. Nothing in them mentions Windows, Linux, x86 or ARM.
Platform independent, but not platform free
This is the distinction that separates a careful answer from a rehearsed one.
| Aspect | Bytecode (.class) | JVM |
|---|---|---|
| Portability | Identical on every platform | A different build per operating system and processor |
| Who produces it | javac, once | Oracle, Amazon, Azul and others |
| What it contains | Platform-neutral instructions | Native code for one specific platform |
| If it is missing | Nothing to run | Bytecode cannot be executed at all |
Portability
Bytecode (.class)Identical on every platformJVMA different build per operating system and processorWho produces it
Bytecode (.class)javac, onceJVMOracle, Amazon, Azul and othersWhat it contains
Bytecode (.class)Platform-neutral instructionsJVMNative code for one specific platformIf it is missing
Bytecode (.class)Nothing to runJVMBytecode cannot be executed at all
Your program is portable. The thing that runs it very deliberately is not.
Where speed comes from
A common objection is that translating bytecode at run time must be slow. Early Java was indeed slower, because the JVM interpreted one instruction at a time. Modern JVMs do something smarter.
- The interpreter starts executing immediately, so there is no long warm-up before output appears.
- Meanwhile the JVM counts how often each method runs. Methods that run frequently are called hot.
- The just-in-time compiler compiles hot methods to optimised native code and caches the result, so subsequent calls skip interpretation entirely.
- Because it optimises using real measurements — which branch actually gets taken, which type actually arrives — it can sometimes beat a compiler that only had the source to look at.
Common misreadings
- "Bytecode is machine code." It is not. No processor executes bytecode directly; the JVM always translates it first.
- "Java is interpreted." Only at first. Hot paths get compiled to native code, which is why a long-running server speeds up after warm-up.
- "Write once, run anywhere means no testing." Operating systems still differ in file paths, line endings, default character sets and thread scheduling. Portability covers the language, not every assumption your code makes.
- "A newer JVM can run anything." The runtime refuses bytecode compiled by a newer JDK than itself. Compiling on Java 21 and running on Java 17 fails with
UnsupportedClassVersionError.
Quick recall
Everything you need if you only revisit this box.
- Java compiles once to bytecode, then a per-platform JVM translates that bytecode to native code at run time.
javacproduces.classfiles; thejavalauncher starts a JVM that consumes them.- Programs are platform independent; the JVM is platform dependent. That trade is the whole mechanism.
- Bytecode is inspectable with
javap -c— it is a documented instruction set, not an obfuscated blob. - Performance comes from the JIT compiler promoting hot methods to cached native code, using runtime measurements the compiler never had.
- Newer bytecode cannot run on an older JVM; the version check is strict and one-directional.
Test yourself
Answer these before moving on — recall is what makes it stick.