How Java code runs: source → bytecode → JVM

You type javac, then java, and text appears. Between those two commands, your program is checked, turned into bytecode, loaded, verified, interpreted and finally compiled to machine code while it runs. This lesson follows one small program through every stage, using the real bytecode that javac produces.

Module 1 · Introduction to Java⏱ 18 minBeginnerNo install needed8 practice problems
1 The story

What happens to ₹20 × 3?

At the college canteen, Riya buys three cups of chai at ₹20 each and writes a tiny Java program to print the bill. It prints Total: Rs 60, as expected. Then Arjun Sir asks her a question she's never thought about.

Main.java text you typed price * cups javac check + translate Main.class bipush 20 imul JVM 1. load + verify 2. interpret 3. JIT → machine code CPU Total: Rs 60 appears only after every one of these stages. myphatshala.in
Arjun Sir

Riya, your CPU understands only machine code: tiny instructions like “multiply these two registers”. It has never heard of int or println. So who turned price * cups into something the CPU could do?

Riya

Hmm… javac? It turns my code into bytecode.

Arjun Sir

That's half the answer. Bytecode isn't machine code either. Two more translators work behind the screen: the JVM's interpreter, and its JIT compiler. Today we'll look at the real bytecode of your chai bill, instruction by instruction.

Riya

We can actually see bytecode?

Arjun Sir

With one JDK command: javap -c. Let's go.

2 Definition

How does a Java program run?

Formal definition (write this in exams)

A Java program runs in two stages. At compile time, the Java compiler (javac) checks the source code (.java) and translates it into platform-independent bytecode stored in .class files. At run time, the JVM's class loader loads the class files, links them (verification, preparation, resolution) and initialises them, and the execution engine runs the bytecode by interpreting it and JIT-compiling frequently used parts into native machine code.

In simple words

Step 1: javac checks your code and writes it in a simpler universal language, bytecode. Step 2: java starts the JVM, which loads the bytecode, checks it's safe, and runs it. At first it reads instructions one by one (interpreting). If some code runs again and again, the JVM translates it into your CPU's own machine code so it runs much faster (JIT).

Words you'll meet in this lesson

Compile timeEverything javac does, before the program runs. Errors here are compile-time errors.
Run timeEverything that happens while the JVM is executing the program. Errors here are exceptions.
OpcodeOne bytecode instruction, such as iload, imul or return. Each takes one byte.
Operand stackA small scratch stack inside each method's frame, where bytecode pushes and pops values while calculating.
Constant poolA table inside every .class file holding names, strings and numbers the bytecode refers to.
JIT compilerJust-In-Time compiler: turns frequently run bytecode into machine code while the program runs.

The journey at a glance

You writeMain.java, plain text
Compilejavac Main.java: lexical analysis → parsing → semantic checks (types, names) → bytecode generation
OutputMain.class: magic number CAFEBABE, version, constant pool, fields, methods (bytecode)
Startjava Main starts the JVM
Load and linkClass loader: loading → linking (verification, preparation, resolution) → initialization
ExecuteInterpreter runs bytecode; the JIT compiles hot methods to machine code; the GC cleans memory
See the bytecodejavap -c Main (a JDK tool)

Exam tip: “Java is both compiled and interpreted.” Write it with the two stages: compiled by javac into bytecode, then interpreted and JIT-compiled by the JVM. Also list the three class-loading phases in order: loading, linking, initialization, and the three parts of linking: verify, prepare, resolve.

3 Types and ways to use it

The stages, and the different ways to run Java

Inside javac: four compile phases

1. Lexical analysis

Splits the text into tokens: int, price, =, 20, ;. A stray character such as # fails here.

Words

2. Parsing

Checks the grammar and builds a tree of the program. A missing ; or } fails here.

Grammar

3. Semantic analysis

Checks meaning: does every variable exist, do the types match? int x = "hi"; fails here.

Meaning

4. Bytecode generation

Writes the .class file. It even adds code you didn't write, such as a default constructor Main().

Output

Inside the JVM: load, link, initialise, execute

PhaseWhat happensWhen
LoadingClass loader reads the .class bytes and creates the class in the method areaWhen the class is first needed
VerificationBytecode verifier checks the code is well-formed and type-safeDuring linking
PreparationMemory for static variables is created, set to default values (0, null, false)During linking
ResolutionSymbolic names in the constant pool (like java/lang/System) are turned into direct referencesDuring linking, often lazily
Initializationstatic blocks run and static variables get their real valuesOn first active use of the class
ExecutionInterpreter runs bytecode; JIT compiles hot methodsWhile the program runs

Four ways to run Java code

javac + java

The classic two steps: compile to .class, then run. Used for real projects (via build tools like Maven and Gradle).

Standard

java Main.java

Since Java 11, single-file programs can be run directly. Java compiles in memory, without writing a .class file.

Quick practice

java -jar app.jar

Many .class files packed into one .jar with a manifest naming the main class. This is how most applications ship.

Deployment

jshell

An interactive prompt (Java 9+). Type one line, see the result immediately. Great for trying things out.

Experiment
4 Step by step

Follow the chai bill through every stage

  1. Write the source

    public class Main {
        public static void main(String[] args) {
            int price = 20;
            int cups = 3;
            int total = price * cups;
            System.out.println("Total: Rs " + total);
        }
    }
    Total: Rs 60
    On disk this is just characters. Nothing has been checked yet.
  2. Compile it

    javac Main.java
    javac runs its four phases: tokens → syntax tree → type and name checks → bytecode. Silence means success, and Main.class (861 bytes with JDK 21) appears.
  3. Look at the bytecode

    javap -c Main
    public static void main(java.lang.String[]); Code: 0: bipush 20 2: istore_1 3: iconst_3 4: istore_2 5: iload_1 6: iload_2 7: imul 8: istore_3 9: getstatic #7 // Field java/lang/System.out 12: iload_3 13: invokedynamic #13 // makeConcatWithConstants 18: invokevirtual #17 // Method java/io/PrintStream.println 21: return
    Each line is one instruction. bipush 20 pushes 20 onto the operand stack, istore_1 stores it in local variable slot 1 (price), imul multiplies the top two values. The numbers on the left are byte offsets. javap also shows a Main() constructor that javac added for you.
  4. Run it

    java Main
    Total: Rs 60
    The JVM starts and loads hundreds of core classes (about 540 for this program on JDK 21) before Main itself. You can watch them with java -Xlog:class+load Main. Then Main is linked, initialised and main is interpreted.
  5. Hot code gets compiled

    Our chai bill runs once, so it is simply interpreted. But if a method runs thousands of times, as in a server handling requests, the JIT steps in.

    HotSpot uses tiered compilation: a quick compiler (C1) compiles warm methods first, and an optimising compiler (C2) recompiles the hottest ones with deeper optimisations. You can watch it with java -XX:+PrintCompilation Main.
5 See it happen

From keyboard to CPU: both views

Step through each stage. Left: what Riya sees. Right: what javac and the JVM are doing.

The life of the chai bill program 6 steps
What you see Main.java int price = 20; int cups = 3; int total = price * cups; System.out.println("Total: Rs " + total); Riya types the program and saves it. > javac Main.java > Nothing printed = compiled fine. > javap -c Main 0: bipush 20 2: istore_1 3: iconst_3 4: istore_2 5: iload_1 6: iload_2 7: imul 8: istore_3 9: getstatic … System.out … println, return ← price ← cups ← price * cups ← total > java Main A fraction of a second where nothing seems to happen… but the JVM is busy. > java Main Total: Rs 60 The bill is printed. Same bill, 5 rounds of 1 crore bills Round 1 Round 2 Round 3 Round 4 Round 5 Same code, but later rounds are much faster. (illustration; real timings vary by machine) myphatshala.in
Behind the screen On disk: characters, not a program i n t ␣ p r … The CPU can't run this. The JVM can't either. It must be checked and translated first. 1 Lexer → tokens [int] [total] [=] [price] [*] [cups] [;] 2 Parser → syntax tree = total * 3 Semantic check price, cups declared? both int? int * int = int ✓ 4 Generate bytecode iload_1 iload_2 imul istore_3 Inside Main.class (861 bytes) CA FE BA BEmagic number major version 65= Java 21 Constant pool #7 Fieldref java/lang/System.out#17 Methodref PrintStream.println this: Main · super: java/lang/Object MethodsMain() · main(String[]): 22 bytes of code JVM start-up: load → link → initialise Loadingread Main.classinto method area Linkingverify ✓prepare staticsresolve names Initialiserun staticblocks Before Main, ~540 core classes are loaded: java.lang.Object, java.lang.String,java.lang.System, java.io.PrintStream… Then Main, and finally main() is called. Interpreter: one instruction at a time Local variables 0 args 1 price = 20 2 cups = 3 3 total = 60 Operand stack iload_1 → [20] iload_2 → [20, 3] imul → [60] istore_3 → [ ] The interpreter reads each opcode and does it. println sends "Total: Rs 60" to the console. The JIT compiler watches and counts Interpretercounts callsand loops C1 (quick)warm method →fast machine code C2 (deep)hottest code →optimised code Bytecode for imul becomes a real CPU instruction: imul eax, ebx (x86-64 example) The machine code is cached, so later calls skip the interpreter completely. This is why Java “warms up”. myphatshala.in

Compile-time errors vs run-time errors

Compile-time errorRun-time error (exception)
Found byjavacThe JVM, while running
ExamplesMissing ;, unknown variable, wrong typeDividing by zero, array index out of bounds, null reference
Does a .class file get created?NoYes, the program compiled fine
What you seeMain.java:5: error: …Exception in thread "main" …
6 Real-world use

Where this pipeline matters at work

Knowing what happens between javac and the CPU helps you build, deploy and speed up real systems.

Watch it with an example: the JIT warms up a busy method

A billing server calls gst(price) again and again. Watch the JVM start by interpreting it, then compile it to machine code once it becomes “hot”.

Interpreter first, JIT later 5 steps
Animated example for (bill : bills) total += gst(bill); calls of gst(): 1 5,000 10,000 10,001 1,00,000 Interpreter reads bytecode one instruction at a time JIT compiler turns gst() into native machine code slow: ~ 40 units/call fast: ~ 2 units/call HOT method! Same program, now much faster myphatshala.in
.jar

1. Build tools: Maven and Gradle

In real projects nobody types javac for each file. Maven or Gradle compile hundreds of classes, run tests, and pack the .class files into a .jar. Underneath, they use the same compiler.

src/*.java→mvn package (javac inside)→app.jar
warm-up

2. Warm-up before traffic

A freshly started Java service is slower for its first requests, because the code is still being interpreted. Teams often send some warm-up requests, or roll out new versions gradually, before full traffic arrives.

Deploy→Interpreted, then JIT-compiled→Full speed
invokevirtualgetfieldathrow

3. Reading bytecode to debug libraries

When a library behaves strangely and you don't have its source, javap or the IDE's decompiler shows what its bytecode really does. Developers also check bytecode versions when they see “class file version” errors.

Unknown .class→javap -c / decompiler→Understand the bug

4. Tools that rewrite bytecode

Frameworks such as Spring and Hibernate, and monitoring agents used by observability tools, generate or modify bytecode at run time, for example to add logging or timing around your methods. This works because bytecode is a well-defined format.

Your class→Agent adds timing bytecode→Dashboards

5. Fast start-up for cloud functions

Serverless functions start and stop often, so JIT warm-up matters a lot. Options include Class Data Sharing (pre-processed core classes, shown as “sharing” in java -version) and GraalVM native image, which compiles ahead of time.

Cold start→CDS / native image→Faster first response

Examples are simplified to show where each stage fits. Real setups vary by company.

7 Run the code

Experiment with the pipeline

The chai bill from the story, with notes. Then try the challenges, which show compile-time vs run-time errors, lazy class initialisation and JIT warm-up.

public class Main {
    public static void main(String[] args) {
        int price = 20;
        int cups = 3;
        int total = price * cups;
        System.out.println("Total: Rs " + total);
    }
}
Total: Rs 60
  • int price = 20;Bytecode bipush 20, istore_1: push 20, store it in local slot 1.
  • int total = price * cups;iload_1, iload_2, imul, istore_3: load both, multiply, store the result in slot 3.
  • "Total: Rs " + totalSince Java 9, javac compiles string joining to invokedynamic, letting the JVM choose the fastest way at run time.
  • System.out.println(…)getstatic fetches System.out, then invokevirtual calls println.
8 Line by line

The chai bill, one bytecode instruction at a time

This tracer goes one level deeper than before. For each source line, the note shows the bytecode the JVM executes, and the stack shows the method's local variable slots.

Worked examples: real problems, solved and traced in memory

Your .java file is compiled to bytecode once, and the JVM runs it statement by statement. In these three programs, watch the order: each line runs only after the one before it, and every method call gets its own frame on the stack.

Example 1Tea stall · sales

A tea stall's daily income

Problem statement

A tea stall sells 140 cups of chai at Rs 12 and 60 samosas at Rs 15. Its daily cost is Rs 1,100. Print the income, the cost and the profit.

Input
140 chai × 12, 60 samosa × 15, cost 1100
Expected output
Income: Rs 2580
Profit: Rs 1480

Approach

  1. Compute the income from each item.
  2. Add them and subtract the cost.
  3. Each statement uses values computed by the lines above it: order matters.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Retail · GST

A method call gets its own stack frame

Problem statement

A shop needs the price including 18% GST for two items. Put the calculation in a method withGst and call it twice. Watch a new frame appear on the stack for each call.

Input
prices 500 and 1200
Expected output
Bag: Rs 590
Shoes: Rs 1416

Approach

  1. Write static int withGst(int price) that returns price × 118 / 100.
  2. Call it from main for each price.
  3. The method's frame is pushed when called and popped when it returns.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Weather · conversion

Celsius to Fahrenheit, step by step

Problem statement

A weather display stores temperatures in Celsius but must also show Fahrenheit: F = C × 9 / 5 + 32. Convert 37.5 °C and print both.

Input
37.5 °C
Expected output
37.5 C = 99.5 F

Approach

  1. Store the Celsius value as a double.
  2. Apply the formula; Java follows the usual order: × and / before +.
  3. Print both values.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 1.4 in one screen

  • Compile time: javac: lexical analysis → parsing → semantic analysis → bytecode generation → .class.
  • .class file: CAFEBABE, version (65 = Java 21), constant pool, fields, methods. View with javap -c.
  • Run time: class loader loads → links (verify, prepare, resolve) → initialises (static blocks).
  • Execution: interpreter runs opcodes using local variable slots and the operand stack.
  • JIT: hot methods → C1 (quick) → C2 (optimised) machine code. That's warm-up.
  • Compile-time errors stop javac (no .class). Run-time errors are exceptions thrown by the JVM.
  • Ways to run: javac+java, java Main.java, java -jar, jshell.

Interview and viva questions

Explain the steps of executing a Java program.

Write source in a .java file → compile with javac into bytecode in a .class file → run with java: the class loader loads the class, the verifier checks it, statics are prepared and initialised → the execution engine interprets bytecode and JIT-compiles hot code into machine code → the garbage collector manages memory throughout.

What is the difference between the interpreter and the JIT compiler?

The interpreter executes bytecode one instruction at a time, which starts quickly but runs slower. The JIT compiler translates a whole frequently used method into native machine code once and caches it, so later calls run at near-native speed. HotSpot uses both: interpret first, then compile what's hot.

What is bytecode, and why not compile straight to machine code?

Bytecode is a compact instruction set for the JVM. Because it isn't tied to any CPU, the same .class file runs on every platform, and the JVM can optimise it for the actual machine at run time, using information (such as which code is hot) that isn't known at compile time.

What does the bytecode verifier check?

That the class file is well-formed, that instructions use values of the right types (no treating an int as an object reference), that the operand stack never overflows or underflows, that jumps go to valid instructions, and that access rules are respected. It runs during linking, before any code executes.

Can we run a Java program without compiling it with javac?

Since Java 11, java Main.java runs a single-file program directly, but it still compiles: the launcher compiles the source in memory first. Bytecode is always produced; it just isn't saved to disk.

Practice problems

Programs whose results depend on the order statements run in. Trace each one in your head before running it. The last three are business tasks.

P1Easy

Swap the order

Print "Step 1", "Step 2", "Step 3" on separate lines.

Input
(none)
Expected output
Step 1
Step 2
Step 3
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        System.out.println("Step 1");
        System.out.println("Step 2");
        System.out.println("Step 3");
    }
}
Step 1 Step 2 Step 3

How it works: statements in main run from top to bottom, one at a time.

P2Easy

Update a variable

Start with 10 points, add 5, then double it, then print.

Input
10
Expected output
30
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        int points = 10;
        points = points + 5;
        points = points * 2;
        System.out.println(points);
    }
}
30

How it works: each line uses the value left by the line before: 10 → 15 → 30.

P3Easy

Rectangle area

Print the area of a 12 × 8 room.

Input
12, 8
Expected output
Area: 96 sq ft
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        int length = 12;
        int width = 8;
        System.out.println("Area: " + length * width + " sq ft");
    }
}
Area: 96 sq ft

How it works: multiplication happens before joining with text.

P4Medium

Call a method twice

Write static int square(int n) and print the squares of 6 and 9.

Input
6, 9
Expected output
36
81
Show solution and explanation
public class Main {
    static int square(int n) {
        return n * n;
    }

    public static void main(String[] args) {
        System.out.println(square(6));
        System.out.println(square(9));
    }
}
36 81

How it works: each call gets its own frame with its own n.

P5Medium

Precedence puzzle

Print the values of 2 + 3 * 4 and (2 + 3) * 4.

Input
(none)
Expected output
14
20
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        System.out.println(2 + 3 * 4);
        System.out.println((2 + 3) * 4);
    }
}
14 20

How it works: * runs before +, unless brackets say otherwise.

P6Business problem

Petrol cost of a trip

A car gives 15 km per litre. Petrol costs Rs 105. Print the cost of a 360 km trip.

Input
360 km, 15 km/l, Rs 105/l
Expected output
Litres: 24.0
Cost: Rs 2520.0
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        double litres = 360 / 15.0;
        double cost = litres * 105;
        System.out.println("Litres: " + litres);
        System.out.println("Cost: Rs " + cost);
    }
}
Litres: 24.0 Cost: Rs 2520.0

How it works: dividing by 15.0 keeps decimals. (Illustrative price.)

P7Business problem

Simple interest

Print the simple interest on Rs 25,000 at 8% for 2 years (P × R × T / 100).

Input
25000, 8, 2
Expected output
Interest: Rs 4000
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        int p = 25000, r = 8, t = 2;
        System.out.println("Interest: Rs " + p * r * t / 100);
    }
}
Interest: Rs 4000

How it works: several variables of the same type can be declared on one line.

P8Business problem

Cost per student

A college trip costs Rs 48,600 for a bus and Rs 350 per student for food. Print the total for 54 students and the cost per student.

Input
54 students
Expected output
Total: Rs 67500
Per student: Rs 1250
Show solution and explanation
public class Main {
    static int total(int students) {
        return 48600 + students * 350;
    }

    public static void main(String[] args) {
        int t = total(54);
        System.out.println("Total: Rs " + t);
        System.out.println("Per student: Rs " + t / 54);
    }
}
Total: Rs 67500 Per student: Rs 1250

How it works: 48600 + 18900 = 67500; 67500 / 54 = 1250.

Real-world scenario questions (asked in company interviews)

Real problems where knowing the compile → load → run pipeline makes the difference. Decide your answer first, then open the model answer.

E-commerce · after deployment1–3 years

“The first few hundred requests are slow”

After every deployment of the checkout service, the first few minutes show high response times and a few timeouts, then everything becomes fast. Nothing in the code is slow. Your manager asks why, and what can be done.

The interviewer is checking: do you know about JVM warm-up (interpreter → JIT), and practical ways to handle it?

Show a strong answer

This is JVM warm-up. A new JVM starts by interpreting bytecode and loading classes on first use. Only after code has run many times does the JIT compile it to optimised machine code. Caches and connection pools are also empty at first.

  • Use readiness checks plus a warm-up step that calls the main endpoints before the instance receives real traffic.
  • Roll out gradually: shift traffic to new instances a little at a time.
  • Pre-fill connection pools and caches at start-up.
  • Consider Class Data Sharing (AppCDS) to speed up class loading, or a GraalVM native image if start-up time is critical.

Avoid saying: “Java is just slow at the start, nothing can be done.”

Any company · production bugFresher

NoClassDefFoundError in production, but it compiled fine

The project compiles and all tests pass on your laptop. On the server, one feature fails with java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/ObjectMapper. A teammate says, “Impossible, it compiled!”

The interviewer is checking: do you understand that compile time and run time are separate, and that classes are loaded lazily at run time?

Show a strong answer

Compiling only proves the class was available to javac. At run time, the JVM's class loader must find the class again on the runtime classpath, and it only tries when the code first needs it. That's why only one feature fails.

  1. Check whether the dependency (here, Jackson) is actually inside the deployed jar or on the server's classpath. jar tf app.jar lists the jar's contents.
  2. Check the build: was the dependency marked provided or test scope in Maven, so it wasn't packaged?
  3. Fix the packaging (for example build a “fat jar” with Spring Boot or the Maven Shade plugin), redeploy and retest that feature.

Avoid: confusing this with ClassNotFoundException, or copying random jar files onto the server until the error goes away.

Banking · code reviewFresher

A static block that slows down start-up

In a code review you see a class RateTable whose static { } block downloads the latest interest rates over the network. Sometimes the application hangs for a long time when a customer first opens the loans page. Why does it happen at that moment, and what would you suggest?

The interviewer is checking: do you know when class initialisation happens, and the risks of doing heavy work in static blocks?

Show a strong answer

A static block runs during class initialisation, which happens the first time the class is actively used, here when the loans page first touches RateTable. That request waits for the whole download. If the download throws an exception, the class fails to initialise and later uses fail with NoClassDefFoundError.

  • Move the network call out of the static block into a normal service method with a timeout and error handling.
  • Load the rates at application start-up (or on a schedule) and cache them, with a sensible fallback if the download fails.
  • Keep static blocks for simple, fast, failure-free setup only.

Avoid saying: “Static code runs when the program starts.” It runs when the class is first used.

Startup · performance testing1–3 years

“My benchmark says Java is slower than Python”

A teammate times a method once with System.currentTimeMillis() around a single call, finds it slow, and proposes rewriting the service in another language. How would you respond?

The interviewer is checking: do you understand that JIT warm-up makes naive timing misleading, and do you know how to measure properly?

Show a strong answer

A single cold call mostly measures class loading and interpretation, not steady-state performance. The JIT hasn't compiled anything yet, and the JIT may also remove code whose result is never used, which distorts naive loops in the other direction.

  • Use a proper benchmark harness such as JMH (Java Microbenchmark Harness), which runs warm-up iterations, repeats measurements and prevents dead-code removal.
  • Compare realistic workloads (for example requests per second under load), not one call.
  • Profile to find the real bottleneck before deciding anything.

Avoid: drawing conclusions from one timed run, in any language.