How Java Works
You've written a Java program. You press run. Output appears on the screen.
But what actually happened between those two moments?
Most tutorials skip this entirely. They tell you what to write but never why it works the way it does. Understanding the journey from source code to output makes you a better debugger, a better developer, and someone who can confidently explain Java in any interview.
👉 What you'll learn here:
- ›What happens when you write a
.javafile - ›What the compiler does and what Bytecode is
- ›How the JVM reads and executes your code
- ›What the JIT compiler is and why it matters
- ›Why all of this makes Java platform-independent
🧘 This page explains concepts — not syntax.
You won't need to memorize code here. Read it like understanding how a car engine works before learning to drive. You don't need to know the mechanics to drive — but knowing them makes you a far better driver.
The Big Picture First
Java's execution has three distinct stages. Every Java program goes through all three, every single time.
Each stage has a specific tool, a specific input, and a specific output. Let's walk through each one.
Stage 1 — Write: The Source File
Everything starts when you write Java code in a text file with a .java extension.
1public class HelloWorld {
2 public static void main(String[] args) {
3 System.out.println("Hello, World!");
4 }
5}This file — HelloWorld.java — is called the source file. It is plain text that humans can read.
The computer cannot execute this directly. No processor in the world understands public class or System.out.println. Before it can run, it must be translated — and that's what the compiler does.
💡 Rule: The filename must match the class name exactly, including capitalization. HelloWorld.java must contain public class HelloWorld. If they don't match, the compiler throws an error before anything else happens.
Stage 2 — Compile: Source Code Becomes Bytecode
When you run the compiler command:
javac HelloWorld.java
The Java Compiler (javac) reads your .java file and translates it into Bytecode — a new file called HelloWorld.class.
What exactly is Bytecode?
Bytecode is not machine code. It is not the 0s and 1s your processor understands directly.
Think of it as a universal middle language — one that no OS speaks natively, but that every JVM on every OS knows how to read. It sits halfway between human-readable Java and machine-executable instructions.
This is the key insight:
- ›Before Java: You compile once, get machine code. That code runs only on the OS it was compiled for.
- ›With Java: You compile once, get Bytecode. That Bytecode runs on any OS that has a JVM.
The compiler also does several important things during this stage besides translating:
- ›Syntax checking — catches typos and grammatical errors in your code
- ›Type checking — verifies you're using variables correctly (int stays int, String stays String)
- ›Error reporting — tells you exactly which line has a problem before anything runs
💡 Real-world analogy: Bytecode is like sheet music. A pianist in India and a pianist in Brazil can both read the same sheet music and play the same song — even though they speak different languages. The sheet music is the universal format; the pianist is the JVM.
Stage 3 — Run: The JVM Executes Bytecode
When you run the command:
java HelloWorld
The JVM (Java Virtual Machine) takes over. It reads HelloWorld.class and executes it. But this isn't a single step — the JVM does several things in sequence.
Step 1 — Class Loader
The Class Loader finds your .class file and loads it into the JVM's memory. It also loads any other classes your program depends on — like System, String, or ArrayList from the standard library.
Step 2 — Bytecode Verifier
Before executing a single instruction, the JVM runs a security check on the loaded Bytecode. It confirms the code won't do anything dangerous — like accessing memory it shouldn't, or corrupting the stack.
This is part of why Java is considered a secure language. Even if someone gave you a tampered .class file, the verifier catches it before it runs.
Step 3 — Interpreter and JIT Compiler
This is where your code actually executes.
The JVM starts with an Interpreter — it reads Bytecode one instruction at a time and converts each to native machine code on the fly. This is straightforward but slow for code that runs repeatedly.
This is where the JIT Compiler (Just-In-Time Compiler) comes in.
The JIT Compiler — Why Java Is Fast
The JIT Compiler is one of the most important — and most misunderstood — parts of Java.
Here's how it works:
The JVM watches your running program and identifies "hot" code — sections that execute frequently (like a loop that runs 10,000 times, or a method called constantly). Once it identifies these hot sections, the JIT Compiler compiles them directly into native machine code — bypassing the interpreter entirely for future executions.
The result: Java programs get faster the longer they run. A server application running for hours or days will be significantly faster than the same program at startup — because the JIT has had time to optimize the hot paths.
This is why Java is fast enough to power Netflix, Amazon, and high-frequency trading systems — despite being an interpreted language at startup.
🔗 Deep dive into the JIT and JVM internals: → JVM Architecture · JIT Compiler
The Complete Picture — Write Once, Run Anywhere
Now that you understand each stage, here's how they combine to make Java platform-independent:
The Bytecode you compile on your Mac is identical to the Bytecode that runs on a Windows server or an Android phone. Each platform has its own JVM — but they all speak the same Bytecode language. This is what Write Once, Run Anywhere actually means in practice.
Platform Dependent vs Platform Independent
It helps to see the contrast clearly:
| C / C++ | Java | |
|---|---|---|
| Compilation output | Native machine code (OS-specific) | Bytecode (universal) |
| Runs on | Only the OS it was compiled for | Any OS with a JVM |
| Recompile for each OS? | ✅ Yes | ❌ No |
| Platform independent? | ❌ No | ✅ Yes |
| Speed after warmup | ⚡ Fastest (no JVM layer) | ⚡ Very fast (JIT compiled) |
What Happens When There's an Error?
Errors in Java can happen at two different stages — and knowing which stage matters for debugging.
Compile-time errors — caught by javac before anything runs:
- ›Syntax errors (missing semicolons, mismatched braces)
- ›Type errors (assigning a
Stringto anint) - ›Undefined variables or methods
1// This causes a compile-time error — missing semicolon
2System.out.println("Hello")
3// javac: error: ';' expectedRuntime errors — happen while the JVM is executing:
- ›Dividing by zero (
ArithmeticException) - ›Accessing a null object (
NullPointerException) - ›Array index out of bounds (
ArrayIndexOutOfBoundsException)
1// This compiles fine — but crashes at runtime
2int[] numbers = new int[3];
3System.out.println(numbers[10]); // ArrayIndexOutOfBoundsException💡 Debugging tip: If javac gives you an error, fix your source code. If the JVM throws an exception at runtime, the code compiled successfully — the problem is in the logic, not the syntax.
🔗 Full guide to handling runtime errors: → Exception Handling
Common Beginner Confusion
"What's the difference between javac and java?"
javac is the compiler — it reads your .java file and produces a .class file. You run it once per change. java is the launcher — it starts the JVM and runs your .class file. You run it every time you want to execute the program. Two different tools, two different jobs.
"If Bytecode isn't machine code, how does it run at all?"
The JVM translates it. The JVM itself is machine code — it's a native program installed on your OS. Your Bytecode tells the JVM what to do in a universal language; the JVM then does it using the machine's actual instructions. You never see that translation — it happens automatically.
"Does every .java file produce one .class file?"
Usually yes — one class, one .class file. But if your file contains inner classes or anonymous classes, the compiler produces multiple .class files from a single .java file. You'll see files like HelloWorld$1.class alongside HelloWorld.class.
The 3 Things to Remember From This Page
- ›Write → Compile → Run is the fixed three-stage journey every Java program takes — no shortcuts
- ›Bytecode is the middle layer — compiled once, runs anywhere via the JVM. This is what makes Java platform-independent
- ›The JIT Compiler makes Java fast — it detects hot code and compiles it to native machine instructions during runtime, eliminating the interpreter overhead for frequently-run sections
Summary
- ›Java execution has three stages: Write (
.java) → Compile (javac→.class) → Run (JVM executes) - ›The compiler (
javac) converts human-readable source code into Bytecode — a universal middle format - ›Bytecode is not machine code — it runs on any OS because every OS has its own JVM that translates it
- ›The JVM loads, verifies, and executes Bytecode through the Class Loader, Bytecode Verifier, and Interpreter/JIT
- ›The JIT Compiler identifies frequently-run code and compiles it to native machine code — making Java faster over time
- ›Compile-time errors are caught by
javac; runtime errors are caught by the JVM during execution - ›This entire architecture is what makes Write Once, Run Anywhere possible
What to Read Next
| Topic | Link |
|---|---|
| JDK, JRE, and JVM — what each one is | JDK, JRE & JVM → |
| Install Java and run your first program | Install Java & Setup → |
| Write and understand your first Java program | First Java Program → |
| JVM architecture in depth | JVM Architecture → |
| What the JIT compiler does in detail | JIT Compiler → |
| How Java handles errors at runtime | Exception Handling → |