DEV Community

Dhairya Gemini
Dhairya Gemini

Posted on

What Really Happens When You Run a Java Program? A Deep Dive from .java to JVM Execution ❓️❓️

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.


  1. 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);
}
Enter fullscreen mode Exit fullscreen mode

}

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.


  1. 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.


  1. 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).


  1. 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
Enter fullscreen mode Exit fullscreen mode

Windows Linux macOS

The JVM handles the platform-specific execution.

This is the foundation behind Java's famous:

«Write Once, Run Anywhere»


  1. 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.


  1. 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");
}
Enter fullscreen mode Exit fullscreen mode

}

The JVM performs class initialization according to Java's initialization rules.


  1. 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.


  1. 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.


  1. 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.


  1. 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.


  1. 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.


  1. 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.


  1. 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
Enter fullscreen mode Exit fullscreen mode

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


  1. 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.


  1. 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.


  1. 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
     /        \
    /          \
Enter fullscreen mode Exit fullscreen mode

Interpreter JIT
\ /
\ /
▼ ▼
Native Machine Code


CPU

This is the simplified mental model every Java developer should understand.


  1. 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:

  1. Java OOP
  2. Collections Framework
  3. Exception Handling
  4. Multithreading
  5. JVM Architecture
  6. Garbage Collection
  7. Java Memory Model
  8. JVM Performance Tuning
  9. Spring Boot
  10. Building production-grade backend systems

Understanding the JVM is a strong foundation for that journey.

Top comments (0)