What Really Happens When You Run a Java Program? A Deep Dive from ".java" to JVM Execution
When we write a Java program, it looks deceptively simple:
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
We save the file, run:
javac Main.java
java Main
and get:
Hello, World!
But what actually happens between these two commands?
Java doesn't directly execute the ".java" file.
Instead, the program goes through several stages involving the Java compiler, bytecode, class loader, JVM, interpreter, JIT compiler, and runtime memory.
Let's break the entire process down.
- Writing the Java Source Code
First, we write a Java source file:
public class Main {
public static void main(String[] args) {
int a = 10;
int b = 20;
System.out.println(a + b);
}
}
The file is saved as:
Main.java
At this stage, it is simply human-readable source code.
The computer cannot execute this source code directly.
- The Java Compiler Enters the Picture
When we run:
javac Main.java
the Java compiler ("javac") analyzes our source code.
It checks things such as:
- Syntax
- Data types
- Variable declarations
- Method calls
- Class structure
- Access modifiers
- Other compile-time rules
If everything is correct, the compiler generates:
Main.class
This is where Java becomes interesting.
- Java Doesn't Compile Directly to Machine Code
Languages such as C and C++ commonly compile source code into native machine code for a particular architecture.
Java takes a different approach.
The Java compiler converts:
Java Source Code
↓
javac
↓
Bytecode
↓
Main.class
The ".class" file contains Java bytecode.
For example, conceptually, our Java code might contain bytecode instructions such as:
iload
iadd
invokevirtual
return
Bytecode is not native machine code.
It is an intermediate instruction set designed to be executed by the Java Virtual Machine (JVM).
- Why Does Java Use Bytecode?
This is one of the most important ideas behind Java's portability.
Suppose you compile a Java program on Windows.
The resulting ".class" file can potentially run on:
- Windows
- Linux
- macOS
- Other platforms with a compatible JVM
The basic idea is:
Java Source
↓
javac
↓
Bytecode
↓
┌──────────┼──────────┐
↓ ↓ ↓
JVM JVM JVM
Windows Linux macOS
The JVM handles the platform-specific execution.
This is the foundation behind Java's famous:
«Write Once, Run Anywhere»
- The JVM Takes Over
When we execute:
java Main
the JVM starts.
But the JVM doesn't simply open the ".class" file and immediately execute everything.
Several components become involved.
A simplified pipeline looks like this:
Main.class
↓
Class Loader
↓
Bytecode Verification
↓
Runtime Data Areas
↓
Execution Engine
↓
Interpreter / JIT Compiler
↓
Machine Code
↓
CPU
Let's understand each part.
- Class Loader
The Class Loader is responsible for loading class information into JVM memory.
When the JVM needs the "Main" class, the Class Loader finds and loads it.
Java's class-loading process is commonly discussed in three major phases:
Loading
The JVM finds the class and loads its binary representation.
Linking
Linking includes:
- Verification
- Preparation
- Resolution
Initialization
Static fields and static initialization blocks are initialized when required.
For example:
class Example {
static int number = 100;
static {
System.out.println("Class initialized");
}
}
The JVM performs class initialization according to Java's initialization rules.
- Bytecode Verification
Before executing bytecode, the JVM performs verification.
The purpose is to ensure that the bytecode follows JVM constraints.
This helps prevent invalid operations and contributes to Java's runtime safety model.
Conceptually:
.class file
↓
Bytecode Verifier
↓
Valid?
↙ ↘
Yes No
↓ ↓
Execute Error
This is one reason Java's execution environment is more controlled than simply executing arbitrary machine instructions.
- JVM Runtime Memory
The JVM manages several runtime data areas.
Some of the most important ones are:
- Heap
- Java Virtual Machine Stacks
- Method Area
- PC Register
- Native Method Stacks
Let's focus on the most commonly discussed ones.
Heap
Objects are generally allocated on the heap.
For example:
Student student = new Student();
The object created by:
new Student()
is allocated in heap memory.
The variable:
student
holds a reference to that object.
Conceptually:
Stack
student ──────────────┐
↓
Heap
┌───────────┐
│ Student │
│ object │
└───────────┘
The JVM's garbage collector can later reclaim memory occupied by objects that are no longer reachable.
- Stack Memory
Each thread has its own JVM stack.
When a method is called, a new stack frame is created.
Consider:
public static void main(String[] args) {
calculate();
}
static void calculate() {
int x = 10;
int y = 20;
int result = x + y;
}
Conceptually:
JVM Stack
┌──────────────────┐
│ calculate() │
│ x = 10 │
│ y = 20 │
│ result = 30 │
├──────────────────┤
│ main() │
│ args │
└──────────────────┘
When "calculate()" finishes, its stack frame is removed.
This is one reason method calls have a structured lifetime.
- The Execution Engine
After the classes are loaded and verified, the JVM needs to execute the bytecode.
This is handled by the Execution Engine.
Two important mechanisms are:
Interpreter
The interpreter executes bytecode instructions.
Conceptually:
Bytecode
↓
Interpreter
↓
Instruction
↓
Instruction
↓
Instruction
This allows code to start executing without compiling the entire application into native machine code first.
But there is a performance problem.
Interpreting the same instructions repeatedly can be expensive.
That's where JIT enters.
- JIT Compilation
JIT stands for:
Just-In-Time compilation
The JVM can identify frequently executed code and compile it into optimized native machine code.
For example:
for (int i = 0; i < 1_000_000; i++) {
calculate();
}
If certain code becomes "hot", the JVM can optimize it.
Conceptually:
Bytecode
↓
Interpreter
↓
Frequently executed code
↓
JIT Compiler
↓
Native Machine Code
↓
CPU
This is one of the major reasons modern Java applications can achieve strong runtime performance.
- What Is "Hot Code"?
The JVM doesn't necessarily treat every method equally.
Some code executes once.
Other code may execute millions of times.
The JVM can monitor execution and identify frequently executed portions of the application.
These are often referred to as hot spots.
The JIT compiler can then apply optimizations to those frequently executed paths.
Possible optimizations include techniques such as:
- Method inlining
- Dead-code elimination
- Loop optimizations
- Escape analysis
- Other runtime-specific optimizations
The exact behavior depends on the JVM implementation and runtime conditions.
- Where Do JDK and JRE Fit?
Many Java developers memorize:
JDK
JRE
JVM
but don't understand their relationship.
A simplified model is:
JDK
└── Development Tools
├── javac
├── java
├── javadoc
└── other tools
+
Java Runtime Environment
└── JVM
Historically, the JRE was commonly distributed as a separate concept.
Modern Java distributions don't necessarily provide a standalone JRE installation in the same way older Java versions did.
The important conceptual distinction remains:
JDK: development environment
JVM: engine that executes Java bytecode
- Why Doesn't Java Just Compile Everything Before Running?
A natural question is:
«If native machine code is faster, why doesn't Java simply compile everything before execution?»
Because runtime information can be extremely valuable.
The JVM can observe the actual behavior of the running application.
For example, it may discover:
Method A → rarely executed
Method B → executed millions of times
Method C → usually receives integers of a particular type
The runtime can use this information to make optimization decisions.
This is one of the powerful ideas behind modern managed runtimes.
- What Happens to "System.out.println()"?
Consider:
System.out.println("Hello");
This looks like one simple operation.
But internally, several things happen:
System
↓
out
↓
PrintStream
↓
println()
↓
Output mechanism
↓
Operating system
↓
Terminal
The important lesson is that high-level Java statements often represent multiple layers of runtime behavior.
- The Complete Journey
We can now visualize the entire process:
Main.java
│
▼
Java Compiler
javac
│
▼
Main.class
Java Bytecode
│
▼
Class Loader
│
▼
Bytecode Verification
│
▼
JVM Runtime Areas
│
▼
Execution Engine
/ \
/ \
Interpreter JIT
\ /
\ /
▼ ▼
Native Machine Code
│
▼
CPU
This is the simplified mental model every Java developer should understand.
- Why Understanding This Matters
You might be thinking:
«"Why should I care? Java already works."»
Understanding the JVM becomes extremely useful when you start dealing with:
- Performance optimization
- Memory leaks
- Garbage collection
- Stack overflow
- "OutOfMemoryError"
- Multithreading
- Application profiling
- Backend development
- JVM tuning
- Production debugging
For example, when you encounter:
java.lang.StackOverflowError
you should immediately think about stack frames and potentially excessive recursion.
When you see:
java.lang.OutOfMemoryError: Java heap space
you should start thinking about heap allocation and object retention.
Understanding the JVM turns these errors from mysterious messages into useful diagnostic signals.
Final Thoughts
Java is often described simply as:
«"A programming language."»
But Java is more than the syntax we write.
A Java application involves an entire runtime ecosystem:
Source Code
↓
Compiler
↓
Bytecode
↓
Class Loader
↓
JVM
↓
Runtime Memory
↓
Interpreter / JIT
↓
Native Code
↓
CPU
Once you understand this pipeline, many advanced Java concepts become easier to reason about.
The next time you run:
java Main
remember that you're not simply "running a Java file."
You're starting a runtime system that loads bytecode, manages memory, interprets and compiles code, performs runtime optimizations, and ultimately turns Java instructions into work performed by your CPU.
And that's where Java becomes much more interesting than just writing:
System.out.println("Hello, World!");
What should you learn next?
If you're going deeper into Java, a useful progression is:
- Java OOP
- Collections Framework
- Exception Handling
- Multithreading
- JVM Architecture
- Garbage Collection
- Java Memory Model
- JVM Performance Tuning
- Spring Boot
- Building production-grade backend systems
Understanding the JVM is a strong foundation for that journey.
Top comments (0)