PrepZone Logo
PrepZone

How Java Runs Anywhere

Bytecode, the compile-run cycle, and why one build artefact works on every operating system.

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 .java file. This is plain text, readable by anyone.
  • javac compiles it into a .class file holding bytecode — compact instructions for a machine that does not physically exist.
  • The java launcher starts a JVM, which loads that bytecode and converts it into instructions your actual processor understands.
  • The same .class file is handed to a Windows JVM, a Linux JVM or a macOS JVM. Each one produces different machine code from identical input.
Hello.javaSource you write
Hello.classBytecode
JVMWindows / Linux / macOS
Machine codeRuns on the CPU
One compile step produces bytecode. Every operating system then runs that same bytecode through its own JVM.

Seeing it for yourself

The compile and run steps are genuinely separate, and you can watch the intermediate file appear.

Java
// Greeting.java
public class Greeting {
    public static void main(String[] args) {
        System.out.println("Compiled once, runs anywhere.");
    }
}
Java
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:

Java
javap -c Greeting
Java
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.

AspectBytecode (.class)JVM
PortabilityIdentical on every platformA different build per operating system and processor
Who produces itjavac, onceOracle, Amazon, Azul and others
What it containsPlatform-neutral instructionsNative code for one specific platform
If it is missingNothing to runBytecode cannot be executed at all
  • Portability

    Bytecode (.class)Identical on every platform
    JVMA different build per operating system and processor
  • Who produces it

    Bytecode (.class)javac, once
    JVMOracle, Amazon, Azul and others
  • What it contains

    Bytecode (.class)Platform-neutral instructions
    JVMNative code for one specific platform
  • If it is missing

    Bytecode (.class)Nothing to run
    JVMBytecode 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.
  • javac produces .class files; the java launcher 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.