Back to JDK, JRE & JVM
Q2Question 2 of 4

What happens end to end when a Java program runs?

Key Concepts
  • javac compiles .java source into platform-neutral .class bytecode (not native code)
  • The class loader loads .class files into memory lazily, the first time each class is used
  • The bytecode verifier checks every loaded class is safe before any code runs
  • JVM sets up memory areas: heap (objects), metaspace (class metadata), thread stacks (method calls)
  • The interpreter runs bytecode first, then the JIT compiler turns hot methods into native machine code
  • The JVM is a hybrid: it interprets cold code and compiles hot code, which is why long-running servers get faster over time

Definition

Java source becomes bytecode once, then the JVM loads, verifies, interprets, and JIT-compiles the hot paths into native code. It interprets first, compiles the busy parts.

When a Java program runs, it goes through these stages:

  • javac compiles your .java source into .class files containing bytecode.
  • The JVM starts and the class loader loads those .class files into memory.
  • The bytecode verifier checks the loaded code is safe and well-formed.
  • The JVM allocates memory areas: heap, metaspace, and per-thread stacks.
  • The interpreter begins running bytecode one instruction at a time.
  • The JIT compiler detects frequently called methods and compiles them into native machine code for speed.

This is why the JVM is not a simple interpreter. It is a hybrid system that interprets cold code and compiles hot code.

Think of a bank teller processing loan applications. On her first day, she reads the procedure manual line by line for every application (interpretation). After processing hundreds of applications, she memorizes the common steps and does them from muscle memory without checking the manual (JIT compilation). She still checks the manual for rare edge cases. The JVM works the same way: slow and careful at first, fast on the paths it uses most.

Understanding the Concepts

What does "end to end" mean here?

When an interviewer asks this question, they want you to trace the full journey of your code from the moment you write it to the moment the CPU executes the instructions. Most developers know Java is "compiled and then run," but the actual journey has several distinct stages, and understanding each one helps you debug class loading errors, tune JIT performance, and reason about memory usage.

What happens at compile time?

Compile time is the phase before your program runs. You use javac, which is a JDK tool, to turn your source code into bytecode.

  • You write LoanProcessor.java in your editor.
  • You run javac LoanProcessor.java.
  • javac reads the source, checks the syntax and types, and produces LoanProcessor.class.
  • The .class file contains bytecode, which is a set of instructions for the JVM, not for any specific CPU.
  • This is why the same .class file runs on Windows, Linux, and Mac without recompiling.
JAVA
// A simple loan processor
public class LoanProcessor {
    public double calculateEMI(double principal, double annualRate, int months) {
        double monthlyRate = annualRate / 12 / 100;
        double emi = principal * monthlyRate
                     * Math.pow(1 + monthlyRate, months)
                     / (Math.pow(1 + monthlyRate, months) - 1);
        return Math.round(emi * 100.0) / 100.0;
    }
}
BASH
# Compile: this produces LoanProcessor.class
javac LoanProcessor.java

# You can inspect the bytecode with javap
javap -c LoanProcessor

The output of javap shows instructions like dload, dmul, invokestatic instead of x86 or ARM instructions. These are JVM bytecode instructions.

What happens when the JVM starts?

When you run java LoanProcessor, the operating system starts the JVM process. The JVM then performs several setup steps before your main method runs.

1. Class loading

The class loader reads .class files from disk and loads them into memory. It uses a delegation chain:

  • Bootstrap ClassLoader loads core Java classes like java.lang.String and java.lang.Object.
  • Platform ClassLoader loads standard library extensions like java.sql.
  • Application ClassLoader loads your classes from the classpath.

Classes are loaded lazily. The JVM does not load every class at startup. It loads a class only when your code references it for the first time. If LoanProcessor uses BigDecimal, that class is loaded the first time the code reaches a line that mentions BigDecimal.

2. Bytecode verification

Before any loaded class can run, the bytecode verifier checks it:

  • Does each method have a valid stack map?
  • Are all variable types consistent?
  • Does the code attempt to forge object references or jump to invalid positions?

This is a real security guarantee. Even if someone manually edits a .class file to bypass a balance check, the verifier catches the invalid bytecode and refuses to run it.

3. Memory allocation

The JVM sets up its runtime memory areas:

  • Heap is the shared memory where all objects live. Every Account object, every String, and every BigDecimal is allocated on the heap.
  • Metaspace (called PermGen before Java 8) stores class metadata, method bytecode, and the constant pool.
  • Thread stacks are private to each thread. Each method call creates a stack frame containing local variables and the return address. When the method returns, the frame is popped.
  • Program counter tracks which bytecode instruction each thread is currently executing.

How does execution work?

Once a class is loaded, verified, and memory is ready, execution begins.

The interpreter

The interpreter reads bytecode instructions one at a time and executes each one. This is simple and starts quickly, but it is slow for repeated operations because it re-reads the same instructions every time.

The profiler

While interpreting, the JVM continuously counts how often each method and loop runs. When a method's invocation count crosses a threshold (typically around 10,000 in HotSpot), the JVM marks it as "hot."

The JIT compiler

The Just-In-Time compiler takes hot methods and compiles them directly into native machine code for the CPU. This compiled code is stored in a memory region called the code cache.

HotSpot uses two JIT compilers working together (tiered compilation):

  • C1 (client compiler) compiles quickly with basic optimizations. Used for warm-up.
  • C2 (server compiler) compiles slowly but produces highly optimized native code. Used for hot methods.

A method might start interpreted, get compiled by C1, and then recompiled by C2 as it gets hotter.

Why is this better than pure interpretation?

A pure interpreter reads and decodes the same bytecode every time a method runs. The JIT compiler does the work once and then the CPU runs native instructions directly. For a banking batch job running millions of interest calculations, the JIT-compiled version can be 10 to 50 times faster than the interpreted version.

Can you show the JIT in action?

JAVA
public class EMIBenchmark {
    public static void main(String[] args) {
        LoanProcessor processor = new LoanProcessor();

        // Warm up: let the JIT compile the hot method
        for (int i = 0; i < 100_000; i++) {
            processor.calculateEMI(500_000, 8.5, 240);
        }

        // Now measure: the method is running as native code
        long start = System.nanoTime();
        double emi = 0;
        for (int i = 0; i < 1_000_000; i++) {
            emi = processor.calculateEMI(500_000, 8.5, 240);
        }
        long elapsed = (System.nanoTime() - start) / 1_000_000;

        System.out.println("EMI: " + emi);
        System.out.println("1M calculations in " + elapsed + " ms");
    }
}
BASH
# Run with JIT logging to see when methods get compiled
java -XX:+PrintCompilation EMIBenchmark

In the output, you will see lines like com.bank.LoanProcessor::calculateEMI (42 bytes) showing the JIT compiler compiling that method to native code.

What are deoptimization and speculative optimization?

The JIT compiler sometimes makes assumptions to generate faster code. For example, if it observes that a method is always called with the same argument type, it generates code optimized for that type. If a future call violates that assumption, the JVM "deoptimizes" the method back to interpreted mode, recompiles with updated assumptions, and continues.

This is why the JVM can sometimes match or beat statically compiled languages like C for long-running workloads. It optimizes based on actual runtime behavior, not just static analysis.

What are common mistakes developers make?

  • Benchmarking without warm-up. If you measure performance on the first few calls, you measure the interpreter, not the JIT. Always warm up the JVM before measuring.
  • Thinking javac optimizes code. It does almost nothing. The real optimization happens at runtime in the JIT compiler.
  • Assuming all code is JIT-compiled. Only hot methods get compiled. Cold methods (called rarely) stay interpreted, which is fine because they do not affect overall performance.
  • Ignoring class loading costs. The first time a class is used, it must be loaded from disk, verified, and initialized. This is why the first request to a web application is slower than subsequent ones.

What should a senior engineer know?

  • Tiered compilation is the default since Java 8. C1 and C2 work together. You can disable it with -XX:-TieredCompilation if you need to debug JIT behavior.
  • The code cache has a size limit. If it fills up, the JVM stops compiling. Monitor with -XX:+PrintCodeCache or JMX.
  • Ahead-of-time compilation (GraalVM native image) skips the interpreter and JIT entirely by compiling to native code at build time. This eliminates warm-up but loses runtime optimization opportunities.
  • The -XX:+PrintCompilation flag shows every method the JIT compiles. The -XX:+PrintInlining flag shows which methods are inlined into callers.
  • On-stack replacement (OSR) lets the JIT compile a long-running loop even while it is still executing, without waiting for the method to be called again.

Visual Overview

DIAGRAM
  LoanProcessor.java (your source code)
       │
       │  javac (JDK tool, compile time)
       ▼
  LoanProcessor.class (bytecode on disk)
       │
       │  java command starts JVM (runtime begins)
       ▼
  ┌──────────────────────────────────────────┐
  │  1. CLASS LOADER                          │
  │     Bootstrap → Platform → Application   │
  │     Loads .class lazily on first use      │
  ├──────────────────────────────────────────┤
  │  2. BYTECODE VERIFIER                     │
  │     Checks safety: types, stack, jumps   │
  ├──────────────────────────────────────────┤
  │  3. MEMORY SETUP                          │
  │     Heap (objects)                        │
  │     Metaspace (class metadata)            │
  │     Thread stacks (method frames)         │
  ├──────────────────────────────────────────┤
  │  4. EXECUTION ENGINE                      │
  │     Interpreter (cold code, slow)         │
  │         │ profiler counts invocations     │
  │         ▼                                 │
  │     JIT Compiler (hot code, fast)         │
  │         C1 → basic optimization           │
  │         C2 → aggressive optimization      │
  │         │                                 │
  │         ▼                                 │
  │     Native machine code (code cache)      │
  ├──────────────────────────────────────────┤
  │  5. GARBAGE COLLECTOR                     │
  │     Reclaims unreachable objects          │
  └──────────────────────────────────────────┘
       │
       ▼
  Output: EMI = 4364.07, 1M calcs in 12 ms

Key Takeaway

When you run a Java program, the journey has two phases: compile time and runtime.

At compile time, javac reads your .java source and produces .class bytecode. This bytecode is not native code for any CPU. It is a portable instruction set that any JVM can understand, which is why the same .class file runs on any operating system.

At runtime, the JVM takes over. The class loader reads your .class files into memory, but only when each class is first used. The bytecode verifier checks that the loaded code is safe. Then the JVM allocates memory: a shared heap for objects, metaspace for class metadata, and a private stack for each thread.

Execution starts in the interpreter, which reads bytecode one instruction at a time. This is slow but starts immediately. The JVM watches which methods run frequently and marks them as hot. The JIT compiler then compiles those hot methods into native machine code that runs directly on the CPU.

This is exactly why a banking batch job gets faster the longer it runs. The first thousand interest calculations are interpreted. Then the JIT compiles the calculation method, and the remaining million calculations run as native code at full hardware speed.

Remember: the JVM is not a simple interpreter. It is a hybrid system that interprets cold code and compiles hot code. That is the key insight interviewers are looking for.

Share
Use ← → arrow keys to navigate