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.
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?
Hmm… javac? It turns my code into bytecode.
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.
We can actually see bytecode?
With one JDK command: javap -c. Let's go.
How does a Java program run?
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.
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
iload, imul or return. Each takes one byte.The journey at a glance
| You write | Main.java, plain text |
|---|---|
| Compile | javac Main.java: lexical analysis → parsing → semantic checks (types, names) → bytecode generation |
| Output | Main.class: magic number CAFEBABE, version, constant pool, fields, methods (bytecode) |
| Start | java Main starts the JVM |
| Load and link | Class loader: loading → linking (verification, preparation, resolution) → initialization |
| Execute | Interpreter runs bytecode; the JIT compiles hot methods to machine code; the GC cleans memory |
| See the bytecode | javap -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.
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.
2. Parsing
Checks the grammar and builds a tree of the program. A missing ; or } fails here.
3. Semantic analysis
Checks meaning: does every variable exist, do the types match? int x = "hi"; fails here.
4. Bytecode generation
Writes the .class file. It even adds code you didn't write, such as a default constructor Main().
Inside the JVM: load, link, initialise, execute
| Phase | What happens | When |
|---|---|---|
| Loading | Class loader reads the .class bytes and creates the class in the method area | When the class is first needed |
| Verification | Bytecode verifier checks the code is well-formed and type-safe | During linking |
| Preparation | Memory for static variables is created, set to default values (0, null, false) | During linking |
| Resolution | Symbolic names in the constant pool (like java/lang/System) are turned into direct references | During linking, often lazily |
| Initialization | static blocks run and static variables get their real values | On first active use of the class |
| Execution | Interpreter runs bytecode; JIT compiles hot methods | While 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).
Standardjava Main.java
Since Java 11, single-file programs can be run directly. Java compiles in memory, without writing a .class file.
Quick practicejava -jar app.jar
Many .class files packed into one .jar with a manifest naming the main class. This is how most applications ship.
Deploymentjshell
An interactive prompt (Java 9+). Type one line, see the result immediately. Great for trying things out.
ExperimentFollow the chai bill through every stage
Write the source
On disk this is just characters. Nothing has been checked yet.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 60Compile it
javac runs its four phases: tokens → syntax tree → type and name checks → bytecode. Silence means success, andjavac Main.javaMain.class(861 bytes with JDK 21) appears.Look at the bytecode
Each line is one instruction.javap -c Mainpublic 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: returnbipush 20pushes 20 onto the operand stack,istore_1stores it in local variable slot 1 (price),imulmultiplies the top two values. The numbers on the left are byte offsets. javap also shows aMain()constructor that javac added for you.Run it
The JVM starts and loads hundreds of core classes (about 540 for this program on JDK 21) beforejava MainTotal: Rs 60Mainitself. You can watch them withjava -Xlog:class+load Main. Then Main is linked, initialised andmainis interpreted.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 withjava -XX:+PrintCompilation Main.
From keyboard to CPU: both views
Step through each stage. Left: what Riya sees. Right: what javac and the JVM are doing.
- Riya writes the chai bill program and saves it as
Main.java. - She runs
javac Main.java. No message appears, which means it compiled. javap -c Mainshows the bytecode: simple instructions likebipush,istoreandimul.- She runs
java Main. For a moment nothing visible happens. - Total: Rs 60 is printed.
- If the same code runs crores of times, later rounds get much faster even though nothing in the code changed.
- The saved file is just a sequence of characters. Neither the CPU nor the JVM can run it yet.
- javac splits it into tokens, builds a syntax tree, checks names and types, and generates bytecode.
- The .class file has a fixed layout: the CAFEBABE magic number, the version (65 = Java 21), the constant pool and the methods' bytecode.
- The JVM loads core classes and then Main, links it (verify, prepare, resolve) and initialises it.
- The interpreter executes each opcode using the method's local variables and operand stack: push 20, push 3, multiply, store 60.
- Code that runs often is JIT-compiled, first by C1 and then by C2, into real machine code that is cached and reused.
Compile-time errors vs run-time errors
| Compile-time error | Run-time error (exception) | |
|---|---|---|
| Found by | javac | The JVM, while running |
| Examples | Missing ;, unknown variable, wrong type | Dividing by zero, array index out of bounds, null reference |
| Does a .class file get created? | No | Yes, the program compiled fine |
| What you see | Main.java:5: error: … | Exception in thread "main" … |
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”.
- First call: the JVM's interpreter runs the bytecode of
gst()instruction by instruction. It starts quickly but runs slowly. - Counting: the JVM counts how often each method runs. After thousands of calls,
gst()is marked as a hot method. - JIT: the Just-In-Time compiler translates the bytecode of
gst()into native machine code for this CPU, in the background. - From now on: calls go straight to the machine code. Each call is many times faster than interpreting.
- Result: servers often run slower in the first minutes (warm-up) and then speed up. Numbers here are illustrative; real thresholds depend on the JVM.
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.
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.
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.
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.
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.
Examples are simplified to show where each stage fits. Real setups vary by company.
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);
}
}int price = 20;Bytecodebipush 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 toinvokedynamic, letting the JVM choose the fastest way at run time.System.out.println(…)getstaticfetchesSystem.out, theninvokevirtualcallsprintln.
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.
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.
140 chai × 12, 60 samosa × 15, cost 1100
Income: Rs 2580 Profit: Rs 1480
Approach
- Compute the income from each item.
- Add them and subtract the cost.
- 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.
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.
prices 500 and 1200
Bag: Rs 590 Shoes: Rs 1416
Approach
- Write
static int withGst(int price)that returns price × 118 / 100. - Call it from main for each price.
- 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.
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.
37.5 °C
37.5 C = 99.5 F
Approach
- Store the Celsius value as a
double. - Apply the formula; Java follows the usual order: × and / before +.
- 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.
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.
Swap the order
Print "Step 1", "Step 2", "Step 3" on separate lines.
(none)
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");
}
}How it works: statements in main run from top to bottom, one at a time.
Update a variable
Start with 10 points, add 5, then double it, then print.
10
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);
}
}How it works: each line uses the value left by the line before: 10 → 15 → 30.
Rectangle area
Print the area of a 12 × 8 room.
12, 8
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");
}
}How it works: multiplication happens before joining with text.
Call a method twice
Write static int square(int n) and print the squares of 6 and 9.
6, 9
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));
}
}How it works: each call gets its own frame with its own n.
Precedence puzzle
Print the values of 2 + 3 * 4 and (2 + 3) * 4.
(none)
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);
}
}How it works: * runs before +, unless brackets say otherwise.
Petrol cost of a trip
A car gives 15 km per litre. Petrol costs Rs 105. Print the cost of a 360 km trip.
360 km, 15 km/l, Rs 105/l
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);
}
}How it works: dividing by 15.0 keeps decimals. (Illustrative price.)
Simple interest
Print the simple interest on Rs 25,000 at 8% for 2 years (P × R × T / 100).
25000, 8, 2
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);
}
}How it works: several variables of the same type can be declared on one line.
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.
54 students
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);
}
}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.
“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.”
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.
- Check whether the dependency (here, Jackson) is actually inside the deployed jar or on the server's classpath.
jar tf app.jarlists the jar's contents. - Check the build: was the dependency marked
providedortestscope in Maven, so it wasn't packaged? - 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.
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.
“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.