To truly understand Java, don't stop at:
.java → .class → JVM
The complete flow involves source code, compilation, bytecode, class loading, linking, initialization, execution, memory, JIT compilation, and garbage collection.
Java Source Code
.java
│
▼
Java Compiler
javac
│
▼
Bytecode
.class
│
▼
Class Loader
│
▼
Loading → Linking
│
┌───────┴────────┐
▼ ▼
Verification Preparation
│
Resolution
│
▼
Class Initialization
│
▼
JVM Runtime
│
┌──────────┴──────────┐
▼ ▼
Interpreter JIT
│ │
└──────────┬──────────┘
▼
Native Execution
│
▼
CPU
│
▼
Output
Let's understand each stage.
You write a Java program in a .java file.
Example:
public class Hello {
public static void main(String[] args) {
int a = 10;
int b = 20;
int sum = a + b;
System.out.println(sum);
}
}The source file is:
Hello.java
At this point, it is simply text written according to Java's language rules.
We compile it using:
javac Hello.java
The Java compiler performs several checks.
For example:
int x = ;is invalid Java syntax.
For example:
int age = "Hello";is invalid because a String cannot be assigned to an int.
The compiler also checks whether variables, methods, classes, etc. are used consistently.
If compilation succeeds:
Hello.java
↓
javac
↓
Hello.class
The .class file contains JVM bytecode.
This is extremely important.
The compiler does not normally produce one specific operating system's machine code.
Instead:
Java Source
↓
Java Compiler
↓
JVM Bytecode
That bytecode can be executed by an appropriate JVM implementation.
This is the foundation of Java's portability.
We normally launch a standalone Java program with:
java Hello
The Java launcher starts an appropriate JVM/runtime and requests execution of the Hello class.
The JVM needs to find the class.
That leads to class loading.
The JVM uses a Class Loader to load class definitions.
Conceptually:
Hello.class
↓
Class Loader
↓
JVM memory
The class loader doesn't necessarily load every class in the application immediately.
Classes can be loaded when they are needed.
This is important because Java applications may depend on hundreds or thousands of classes.
After loading, the JVM performs the linking process.
Linking is commonly discussed in three major parts:
Loading
↓
Verification
↓
Preparation
↓
Resolution
Let's break those down.
The JVM checks that the bytecode is structurally valid and follows JVM rules.
Conceptually:
Bytecode
↓
Verifier
↓
Is it valid?
This helps prevent malformed bytecode from being executed improperly.
The JVM prepares memory for class-level data and establishes required runtime structures.
For example, static fields receive appropriate initial default values during this stage.
Symbolic references in the class may be resolved to concrete runtime references.
For example, bytecode might refer to another class or method symbolically.
The JVM can resolve those references as required.
Resolution can be performed lazily depending on the JVM implementation and circumstances.
After loading/linking, a class may need to be initialized.
Consider:
class Test {
static int x = 100;
}The JVM initializes the class when its initialization conditions are met.
Static initialization may involve:
static int x = 100;
static {
System.out.println("Class initialized");
}The important distinction is:
Loading
≠
Initialization
A class can be loaded before its initialization occurs.
For a traditional standalone Java application, the runtime looks for:
public static void main(String[] args)For example:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello");
}
}The main() method becomes the starting point for application execution.
Once execution begins, the JVM manages several important runtime memory areas.
A simplified model is:
JVM Runtime
│
├── Heap
├── Java Stacks
├── Method Area
├── PC Registers
└── Native Method Stacks
Let's understand them.
The heap is where Java objects are generally allocated.
Example:
Student s = new Student();The new Student() creates an object whose storage is associated with the heap.
Conceptually:
Stack
│
│ s
▼
Heap
│
└── Student object
The heap is also the main area managed by the garbage collector.
Each thread has its own Java stack.
When a method is called, a stack frame is created.
For example:
main()
↓
calculate()
↓
add()Conceptually:
Stack
┌─────────────┐
│ add() frame │
├─────────────┤
│calculate() │
├─────────────┤
│main() frame │
└─────────────┘
When add() finishes, its frame is removed.
This is why method calls follow a Last In, First Out (LIFO) structure.
This is one of the most important beginner concepts.
Consider:
Student s = new Student();A simplified mental model is:
Stack Heap
────── ────
s ───────────────────→ Student object
The variable s is a reference.
The object created with new is associated with heap memory.
This is a simplified model; the JVM/JIT may optimize actual storage and object behavior in ways that differ from this conceptual picture.
Now the JVM needs to execute the bytecode.
There are two major concepts to understand:
The JVM can interpret bytecode instructions.
Bytecode
↓
Interpreter
↓
Execution
Frequently executed code can be compiled into optimized native code.
Bytecode
↓
JVM observes execution
↓
Hot code
↓
JIT
↓
Native machine code
Modern JVMs use sophisticated combinations of interpretation and JIT compilation.
Suppose your program has:
for (int i = 0; i < 1_000_000; i++) {
calculate(i);
}The method calculate() may execute an enormous number of times.
The JVM can recognize that this code is frequently executed.
That makes it a candidate for optimization.
Rarely executed code
↓
Interpretation may be enough
Frequently executed code
↓
JIT optimization
↓
Native machine code
This is called adaptive optimization.
People sometimes think:
"Java is slow because it runs inside a virtual machine."
That is an outdated oversimplification.
Modern JVMs perform sophisticated runtime optimization.
They can use:
- JIT compilation
- method inlining
- escape analysis
- speculative optimization
- optimized garbage collectors
- profile-guided runtime decisions
So the JVM isn't simply "interpreting everything."
Suppose:
Student s = new Student();Later:
s = null;If there are no other reachable references to that object, it may become eligible for garbage collection.
Conceptually:
Student object
↓
No longer reachable
↓
Eligible for GC
↓
Garbage Collector
↓
Memory reclaimed
Important:
Becoming unreachable does not mean the object is immediately destroyed.
The garbage collector decides when and how memory is reclaimed.
Java automatically manages ordinary object memory through garbage collection.
But developers still need to manage resources such as:
- files
- sockets
- database connections
- external resources
For example:
try (FileInputStream file =
new FileInputStream("data.txt")) {
// use file
}The try-with-resources mechanism helps ensure the resource is closed.
So:
Garbage collection manages memory; it is not a replacement for resource management.
Suppose we execute:
System.out.println(10 + 20);The result is:
30
The overall journey was:
Source
↓
Compiler
↓
Bytecode
↓
Class Loader
↓
Verification
↓
Initialization
↓
JVM execution
↓
Interpreter/JIT
↓
CPU
↓
Output
Let's follow one tiny program.
public class Demo {
public static void main(String[] args) {
int a = 10;
int b = 20;
int result = a + b;
System.out.println(result);
}
}File:
Demo.java
Compile:
javac Demo.java
Generated:
Demo.class
Run:
java Demo
JVM loads Demo.
JVM verifies and links the class.
Class initialization occurs as required.
main() executes.
The JVM executes the bytecode.
The result is printed:
30
.java
↓
javac
↓
.class
↓
JVM
↓
Output
Source
↓
Compilation
↓
Bytecode
↓
Class Loading
↓
Verification
↓
Execution
↓
Output
Source
↓
javac
↓
Bytecode
↓
Class Loader
↓
Loading
↓
Linking
├── Verification
├── Preparation
└── Resolution
↓
Initialization
↓
Runtime Data Areas
↓
Interpreter + JIT
↓
Native Machine Code
↓
CPU
↓
Output
Do not memorize this as:
Java → JVM → CPU
Understand the layers:
┌─────────────────────────────┐
│ Java Source │
│ .java │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ javac Compiler │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ JVM Bytecode │
│ .class │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ Class Loader │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ Verification / Linking │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ JVM Runtime │
│ │
│ Heap | Stack | GC | JIT │
└─────────────┬───────────────┘
↓
┌─────────────────────────────┐
│ Native Machine Code │
└─────────────┬───────────────┘
↓
CPU
Remember the 8-step flow:
Write → Compile → Bytecode → Load → Link → Initialize → Execute → Output
Or the short version:
.java → javac → .class → Class Loader → JVM → JIT → CPU → Output
Once you understand this flow, concepts like JDK, JVM, bytecode, class loading, heap, stack, garbage collection, and JIT stop being separate topics—they become different parts of the same Java execution process.