JIT compiler and performance

Java starts by interpreting your bytecode, then quietly compiles the parts you use most into fast machine code while the program runs. That's why the same loop can be seven times faster in its fifth round than in its first. You'll see how the JVM finds hot code, what C1 and C2 do, what inlining and escape analysis mean, how to read -XX:+PrintCompilation, why naive timing benchmarks lie, and then practise with 7 problems.

Module 8 · JVM Memory⏱ 22 minIntermediate7 practice problems
1 The story

The benchmark that proved the wrong thing

Riya wants to show her team that her new invoice calculator is fast. She times one call with System.nanoTime(): 3 milliseconds. Kabir times it in a loop and gets 0.4 milliseconds per call. Same code, same laptop, a seven-fold difference. They argue until Arjun Sir walks by.

Same work, six rounds (time per round) 31862169958921437432 123456 microseconds Interpretertier 0 C1tier 3, quick C2tier 4, best The JVM compiles hot code while your program keeps running. Round 1 measures the warm-up, not your code's real speed. myphatshala.in
Riya

Sir, who is right? Is my method 3 ms or 0.4 ms?

Arjun Sir

Both measurements are real, but they measure different things. The first time, your method ran in the interpreter. As it kept running, the JIT compiler turned it into optimised machine code, so later rounds were far faster.

Riya

So Java changes its own speed while it runs?

Arjun Sir

Exactly. Today you'll see how the JVM decides what to compile, what the compilers do, and how to measure performance without fooling yourself.

2 Definition

What is the JIT compiler?

Formal definition (write this in exams)

A Just-In-Time (JIT) compiler translates frequently executed bytecode into native machine code at run time. In HotSpot, methods start in the interpreter; per-method invocation and loop (back-edge) counters detect hot code, which is compiled in the background. With tiered compilation (the default since Java 8), hot methods are first compiled by C1, a fast compiler that also collects profiling data, and the hottest are recompiled by C2, an optimising compiler that uses the profile for optimisations such as inlining, escape analysis and dead code elimination. If an assumption behind optimised code becomes invalid, the code is deoptimised and execution falls back to the interpreter.

In simple words

A translator reads a speech line by line for the first few minutes. Once she notices the same phrases coming up again and again, she writes polished translations of those phrases and just reads them out. The speech gets faster the longer it goes on.

Words you'll meet in this lesson

Hot codeMethods and loops that run often enough to be worth compiling.
Tiered compilationInterpreter (tier 0) → C1 (tiers 1–3) → C2 (tier 4).
InliningCopying a called method's body into the caller, removing the call.
Escape analysisFinding objects that never leave a method, so they need not be heap objects.
Warm-upThe early phase when code is still interpreted or being compiled.
DeoptimisationThrowing away compiled code whose assumptions no longer hold.

JIT facts at a glance

Starts withThe interpreter. No method is compiled before it has been used
Hot detectionCounters per method for calls and loop back-edges. Defaults (JDK 21): about 200 calls for C1 (Tier3InvocationThreshold), about 5,000 for C2 (Tier4InvocationThreshold), adjusted for loops and load
Tiers0 interpreter; 1–3 C1 (3 = with full profiling); 4 C2. Tiered compilation is on by default since Java 8
Key C2 optimisationsInlining (small methods under 35 bytes of bytecode, hot ones up to 325), escape analysis with scalar replacement, dead code elimination, loop optimisations
See itjava -XX:+PrintCompilation Main; compare -Xint (interpreter only)
Measure properlyWarm up, repeat, use the results, and prefer JMH (Java Microbenchmark Harness)

Exam tip: javac does almost no optimisation: it turns source into bytecode. The big optimisations happen at run time, in the JIT. So “Java is interpreted” and “Java is compiled” are both half-truths: it is compiled to bytecode, interpreted at first, and JIT-compiled where it matters.

3 Types and ways to use it

Two compilers, three big optimisations

C1 (client compiler)

Compiles quickly with basic optimisations, and in tier 3 adds counters that record which types and branches actually occur. Gets code faster early.

tiers 1–3

C2 (server compiler)

Slower to compile, much better code. Uses C1's profile to make bold assumptions, such as “this call always goes to the same class”.

tier 4

Inlining, escape analysis, dead code

Inline small hot methods; avoid allocating objects that never escape; delete code whose result is never used. Most other optimisations build on inlining.

C2 at work

Seeing the JIT work: -XX:+PrintCompilation

This program times the same method six times (your numbers will differ):

public class Main {
    static long invoiceTotal(int items) {
        long total = 0;
        for (int i = 1; i <= items; i++) {
            total += i * 118L / 100;          // each item's price with 18% GST
        }
        return total;
    }

    public static void main(String[] args) {
        for (int round = 1; round <= 6; round++) {
            long start = System.nanoTime();
            long t = invoiceTotal(1_000_000);
            long micros = (System.nanoTime() - start) / 1_000;
            System.out.println("Round " + round + ": " + micros + " us (total " + t + ")");
        }
    }
}
Round 1: 3186 us (total 590000100000) Round 2: 2169 us (total 590000100000) Round 3: 958 us (total 590000100000) Round 4: 921 us (total 590000100000) Round 5: 437 us (total 590000100000) Round 6: 432 us (total 590000100000)

The same run with compilation logging, keeping only our method's lines (hundreds of JDK methods are compiled too):

java -XX:+PrintCompilation Main
27 9 % 3 Main::invoiceTotal @ 4 (30 bytes) 27 10 3 Main::invoiceTotal (30 bytes) 27 11 % 4 Main::invoiceTotal @ 4 (30 bytes) 28 9 % 3 Main::invoiceTotal @ 4 (30 bytes) made not entrant 29 11 % 4 Main::invoiceTotal @ 4 (30 bytes) made not entrant Round 1: 3186 us (total 590000100000) 46 120 % 4 Main::invoiceTotal @ 4 (30 bytes) 47 121 4 Main::invoiceTotal (30 bytes) Round 2: 2169 us (total 590000100000) Round 3: 958 us (total 590000100000) 48 10 3 Main::invoiceTotal (30 bytes) made not entrant Round 4: 921 us (total 590000100000) Round 5: 437 us (total 590000100000) Round 6: 432 us (total 590000100000)
ColumnExampleMeaning
Time47Milliseconds since the JVM started
Compile id121A running number for each compilation task
Flags%% = on-stack replacement (a hot loop compiled while running); others: s synchronized, ! has exception handlers, n native
Tier3 / 43 = C1 with profiling, 4 = C2
Method and sizeMain::invoiceTotal @ 4 (30 bytes)The method, the loop's bytecode index for OSR, and the bytecode size
Statusmade not entrantThis compiled version is retired: replaced by a better tier, or deoptimised

Naive benchmarks lie. Timing one call measures the interpreter and class loading. Timing a loop whose result is never used may measure nothing, because C2 can delete the work. GC pauses, CPU frequency changes and other processes add noise. For real measurements use JMH (Java Microbenchmark Harness), which handles warm-up, forks fresh JVMs, and consumes results so they can't be optimised away.

4 Step by step

The journey of one hot method

  1. Interpret and count

    total += withGst(price);   // called again and again
    The interpreter executes bytecode one instruction at a time. Each call to withGst increments its invocation counter; each jump back to the top of a loop increments a back-edge counter.
  2. Hot: queue it for C1

    After about 200 calls (default tiered thresholds, adjusted by the JVM), the method is queued for compilation.

    Compilation happens on separate compiler threads, so your code keeps running in the interpreter meanwhile. A long loop inside a single call can be compiled on the spot and switched to mid-loop: on-stack replacement, shown as %.
  3. C1 code runs and gathers a profile

    The tier 3 code is already much faster than the interpreter, and it records what really happens.

    The profile notes which classes appear at each call site, which branches are taken and how often loops run. C2 relies on this to make speculative optimisations.
  4. Very hot: C2 optimises

    // after inlining, the loop effectively becomes:
    total += price * 118 / 100;
    C2 inlines small hot methods, keeps values in CPU registers, removes unneeded bounds checks and allocations (escape analysis), unrolls loops and deletes dead code. The new tier 4 code replaces the tier 3 version, which is “made not entrant”.
  5. When assumptions break: deoptimise

    Suppose C2 assumed a call always reached one class, and a new subclass shows up.

    The JVM discards that compiled code and continues in the interpreter, then recompiles later with the new information. Your program's results never change; only its speed does.
5 See it happen

Interpreter to C1 to C2: both views

Left: the program and real -XX:+PrintCompilation lines from the run above. Right: the tier a method is running in, and what the JVM is doing.

How invoiceTotal gets fast 6 steps
What you see long t = invoiceTotal(1_000_000); // a compiled version is retired // six rounds of the same work // round 1 starts in the interpreter // 1,000,000 loop jumps to count 27 9 % 3 Main::invoiceTotal @ 4 (30 bytes) // hot loop: OSR-compiled by C1 27 10 3 Main::invoiceTotal (30 bytes) Round 1: 3186 us 47 121 4 Main::invoiceTotal (30 bytes) Round 2: 2169 us Round 3: 958 us 48 10 3 Main::invoiceTotal (30 bytes) made not entrant // tier 3 copy no longer needed Round 1: 3186 us Round 2: 2169 us Round 3: 958 us Round 4: 921 us Round 5: 437 us Round 6: 432 us Slow start: bytecode is interpreted. The loop got hot inside the first call. The whole method is compiled by C1 too. C2 (tier 4) takes over: rounds speed up. Older code is retired, not your results. Same work, about 7 times faster by round 6. myphatshala.in
Behind the screen Where the code runs Tier 0 · Interpreter one bytecode at a time Tier 3 · C1 fast compile + profiling Tier 4 · C2 optimised with the profile Countinginvocations: 1loop back-edges: rising fastthreshold reached → compile Compiler thread (C1)compiles the hot loopin the backgroundOSR: jump from theinterpreted loop intocompiled code mid-call C1 code + profilerecords how the codereally behaves:• branches taken• loop trip counts• types at call sites C2 optimisations• inline small methods• values in registers• escape analysis• dead code removed• loops unrolled Made not entrantold tier 3 code retired:new calls use tier 4(the same happens ondeoptimisation, when anassumption breaks) time per round Counters decide what is worth compiling. Compiling happens alongside your program. Quick code now, data for better code later. C2 bets on the profile and wins big. Code versions come and go; results never change. Warm-up rounds are slow; measure steady state. myphatshala.in
6 Real-world use

The JIT in real systems

Most of the time the JIT just works. These are the situations where developers need to understand it.

Watch it with an example: a bank's month-end interest job speeding up

A batch job calls addInterest() for every account. Watch the JVM spot the hot method, compile it twice, and briefly step back when a new account type appears.

Hot code gets compiled, and recompiled 6 steps
Animated example for (Account a : accounts) a.addInterest(); // call 200: addInterest is hot // C1 code runs and profiles // call 5,000: C2 compiles // first FixedDeposit account! // recompiled for both types calls to addInterest: 150 200 1,800 5,000 lakhs lakhs Interpreter C1 (tier 3) C2 (tier 4) queued for C1 on a compiler thread profile: receiver is always Savings inlined addInterest + getBalance assumption broken: deoptimise both types inlined behind a type check speed interest credited: same on every account, every tier "made not entrant" in -XX:+PrintCompilation myphatshala.in
coldwarm

1. Warming up before taking traffic

A freshly started instance is slower for its first seconds or minutes. Teams send synthetic requests to new instances, or use gradual traffic ramps, before they serve real users at full load.

New instance→warm-up requests→Join load balancer
@Benchmark@Warmup(5)Blackhole bh

2. Honest micro-benchmarks

Before replacing one algorithm or library with another, teams measure with JMH. It runs warm-up iterations, forks fresh JVMs, reports errors, and protects results from dead code elimination.

Two implementations→JMH benchmark→Reliable comparison
getter

3. Clean code without a speed penalty

Getters, small helper methods and interfaces with one implementation are typically inlined by C2. Write readable code first; optimise only what a profiler shows is slow.

Small methods→inlined by C2→No call overhead
allocation rate

4. Allocation-heavy hot paths

Escape analysis removes some temporary objects, but not all (for example, objects stored in fields or passed to methods that aren't inlined). In very hot paths, teams reduce allocation, which also lowers GC pressure (Lesson 8.3).

Profiler shows hot path→fewer temporary objects→Less GC
start-upmatters

5. Short-lived programs

Command-line tools and serverless functions may finish before the JIT has warmed up. For them, start-up techniques such as class data sharing, or ahead-of-time compiled native images (for example with GraalVM), can matter more than peak speed.

Runs for 1 second→CDS / native image→Fast start

Examples are simplified. JIT behaviour varies between JVM versions and hardware; always confirm a performance idea with measurements on your own system.

7 Run the code

Warm-up you can see

The same program as above. Its timings are machine-dependent: your numbers will differ, but the pattern (slow first rounds, fast later ones) should appear.

public class Main {
    static long invoiceTotal(int items) {
        long total = 0;
        for (int i = 1; i <= items; i++) {
            total += i * 118L / 100;          // each item's price with 18% GST
        }
        return total;
    }

    public static void main(String[] args) {
        for (int round = 1; round <= 6; round++) {
            long start = System.nanoTime();
            long t = invoiceTotal(1_000_000);
            long micros = (System.nanoTime() - start) / 1_000;
            System.out.println("Round " + round + ": " + micros + " us (total " + t + ")");
        }
    }
}
Round 1: 2851 us (total 590000100000) Round 2: 2437 us (total 590000100000) Round 3: 929 us (total 590000100000) Round 4: 927 us (total 590000100000) Round 5: 449 us (total 590000100000) Round 6: 451 us (total 590000100000)
  • System.nanoTime()A high-resolution timer for measuring durations (not the time of day).
  • i * 118L / 100The L makes the multiplication use long, so it can't overflow int.
  • (total " + t + ")"Printing the result keeps the work alive; an unused result could be optimised away.
  • Round 1 vs Round 6Interpreter and C1 first, C2 later. Locally, try java -Xint Main to see every round stay slow.
8 Line by line

Work whose result nobody uses

Recorded from the real JVM, which runs in the interpreter while you step. Every call happens here; the notes explain which ones C2 would delete once the loop is hot.

Worked examples: real problems, solved and traced in memory

The JIT compiler is invisible in a debugger: while you step through code, the JVM runs it in the interpreter. These traces show the program exactly as the interpreter runs it, and the notes explain what the JIT would change once the same code becomes hot: which calls get inlined, and which objects never need to reach the heap.

Example 1E-commerce · pricing

A small method that will become hot

Problem statement

A cart adds 18% GST to each price (in paise, to avoid decimals) using a small helper withGst. A static counter shows how often the helper runs. In production it runs lakhs of times a minute. Trace one cart.

Input
cart = (49900, 12000, 7500) paise
Expected output
Total: 81892 paise, withGst called 3 times

Approach

  1. Keep money in long paise: integer maths, no rounding surprises.
  2. Call the helper once per item. Each call pushes and pops a frame in the interpreter.
  3. The JVM counts calls per method. After a few hundred calls, C1 compiles it; after thousands, C2 compiles an optimised version, usually inlined into the loop.

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 · cart total

Getters: calls in the interpreter, gone after inlining

Problem statement

Good design hides fields behind getters. A cart total calls getPrice() and getQty() for each item. Does all this calling cost anything in production? Trace it and read the notes on inlining.

Input
items: (Rs 250 × 2), (Rs 99 × 3)
Expected output
Cart total: Rs 797

Approach

  1. Store each item as an object with private fields and getters.
  2. Loop over the items and add price × quantity.
  3. In the trace every getter call pushes a frame. Once the loop is hot, the JIT inlines these tiny methods: it copies their body into the caller, so the compiled loop reads the fields directly.

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 3Payments · fees and ledger

Escape analysis: which object really needs the heap?

Problem statement

A payment service adds a Rs 5 fee (500 paise) using a temporary Money object, then records the charged amount in a ledger, which is a static list. Trace both methods and decide which Money object escapes its method.

Input
amount = 10000 paise, fee = 500 paise
Expected output
Charged 10500 paise, ledger size 1

Approach

  1. addFee creates a Money, reads one field, and returns a plain long: the object never leaves the method.
  2. record creates a Money and stores it in a static list: the object escapes and must live on the heap.
  3. C2's escape analysis can replace a non-escaping object with plain local values (scalar replacement), so in hot compiled code the first object may never be allocated at all.

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 8.5 in one screen

  • javac → bytecode; the JVM interprets first, then JIT-compiles hot code to machine code at run time.
  • Hot = counters for calls and loop back-edges cross thresholds (about 200 for C1, about 5,000 for C2 by default).
  • Tiers: 0 interpreter → 3 C1 with profiling → 4 C2 optimised. OSR compiles hot loops mid-call.
  • C2: inlining, escape analysis (scalar replacement), dead code elimination, loop optimisations.
  • Deoptimisation: compiled code is discarded when assumptions break (“made not entrant”).
  • Warm-up makes early runs slow; measure steady state, use results, prefer JMH.
  • Tools: -XX:+PrintCompilation, -Xint, -XX:TieredStopAtLevel=1.

Interview and viva questions

What is the JIT compiler and why does Java use it?

It compiles frequently executed bytecode into native machine code while the program runs. Starting in the interpreter gives fast start-up and keeps bytecode portable; compiling only hot code focuses effort where it pays, and run-time profiling lets the JIT make optimisations an ahead-of-time compiler can't safely make, such as optimising for the classes and branches actually seen.

Explain tiered compilation.

Code starts at tier 0 in the interpreter. When it becomes hot it is compiled by C1, usually at tier 3, which produces decent code quickly and collects profiling data. If it stays hot, C2 recompiles it at tier 4 using that profile for aggressive optimisations. Tiered compilation has been the default since Java 8.

What is escape analysis?

An analysis in C2 that checks whether an object created in a method can be seen outside it (returned, stored in a field or a collection, passed to code that isn't inlined). If it doesn't escape, the JIT can replace the object with its fields as local values (scalar replacement) and avoid the heap allocation, and it can also remove unneeded locking on that object.

Why can a micro-benchmark written with System.nanoTime() be wrong?

It may measure warm-up (interpretation, class loading, compilation) instead of steady state; the JIT may remove work whose results are unused; GC pauses and other processes add noise; and one run proves little. JMH handles warm-up, multiple forks and iterations, and prevents dead code elimination with Blackholes.

What is deoptimisation?

C2 makes speculative assumptions, for example that a call site always sees one class. If that stops being true (say a new subclass is loaded), the JVM invalidates the compiled code, continues in the interpreter and may recompile later. It affects speed, never correctness.

Practice problems

You can't print the JIT's decisions from inside a normal program, so these problems practise the skills around it: spotting hot code, reading -XX:+PrintCompilation output, and measuring honestly. The last three are business tasks.

P1Easy

Which methods are hot?

Count how many times each of three methods runs while processing 500 orders, and flag those called more than 200 times (roughly where C1 compilation starts with default settings).

Input
500 orders; every order is validated, every 4th gets a coupon, 1 report at the end
Expected output
validate: 500 calls  <- hot, will be compiled
coupon  : 125 calls
report  : 1 calls
Show solution and explanation
public class Main {
    static int validateCalls, couponCalls, reportCalls;

    static boolean validate(int amount) { validateCalls++; return amount > 0; }
    static int coupon(int amount) { couponCalls++; return amount / 10; }
    static void report() { reportCalls++; }

    static void show(String name, int calls) {
        System.out.println(name + ": " + calls + " calls" + (calls > 200 ? "  <- hot, will be compiled" : ""));
    }

    public static void main(String[] args) {
        for (int order = 1; order <= 500; order++) {
            if (validate(order * 100) && order % 4 == 0) {
                coupon(order * 100);
            }
        }
        report();
        show("validate", validateCalls);
        show("coupon  ", couponCalls);
        show("report  ", reportCalls);
    }
}
validate: 500 calls <- hot, will be compiled coupon : 125 calls report : 1 calls

How it works: the JVM keeps counters like these for every method (and for loop back-edges). Methods that cross the thresholds are compiled, first by C1, later by C2 if they stay hot. report(), called once, is simply interpreted: compiling it would cost more than it saves. The 200 here is the default Tier3InvocationThreshold; the real decision also considers loops and compiler load.

P2Easy

Read one PrintCompilation line

Split a line of -XX:+PrintCompilation output into its parts and explain the tier.

Input
"     36    9       4       Main::gst (6 bytes)"
Expected output
time:       36 ms after start
compile id: 9
tier:       4 = C2, fully optimised
method:     Main::gst
bytecode:   6 bytes
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        String line = "     36    9       4       Main::gst (6 bytes)";
        String[] p = line.trim().split("\\s+");
        String[] tierNames = {"interpreter", "C1, no profiling", "C1, light profiling",
                              "C1, full profiling", "C2, fully optimised"};
        int tier = Integer.parseInt(p[2]);
        System.out.println("time:       " + p[0] + " ms after start");
        System.out.println("compile id: " + p[1]);
        System.out.println("tier:       " + tier + " = " + tierNames[tier]);
        System.out.println("method:     " + p[3]);
        System.out.println("bytecode:   " + p[4].substring(1) + " bytes");
    }
}
time: 36 ms after start compile id: 9 tier: 4 = C2, fully optimised method: Main::gst bytecode: 6 bytes

How it works: the columns are a timestamp (ms since start), a compile id, optional flags (such as % for OSR), the tier (0 to 4), the method and its bytecode size. This real line says Main::gst, a 6-byte method, was compiled by C2 (tier 4) 36 ms after start-up. The regex \s+ splits on any run of spaces.

P3Medium

Classify compilation events

Given real lines from -XX:+PrintCompilation, count normal compilations, OSR compilations (marked %) and deoptimised code (“made not entrant”).

Input
7 lines (see the code)
Expected output
Normal compilations: 3
OSR compilations:    2
Old code discarded:  2
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        String[] log = {
            "35    8       3       Main::gst (6 bytes)",
            "36    9       4       Main::gst (6 bytes)",
            "36    8       3       Main::gst (6 bytes)   made not entrant",
            "38   10 %     3       Main::main @ 4 (36 bytes)",
            "39   11       3       Main::main (36 bytes)",
            "39   12 %     4       Main::main @ 4 (36 bytes)",
            "42   10 %     3       Main::main @ 4 (36 bytes)   made not entrant"
        };
        int normal = 0, osr = 0, discarded = 0;
        for (String line : log) {
            if (line.contains("made not entrant")) discarded++;
            else if (line.contains(" % ")) osr++;
            else normal++;
        }
        System.out.println("Normal compilations: " + normal);
        System.out.println("OSR compilations:    " + osr);
        System.out.println("Old code discarded:  " + discarded);
    }
}
Normal compilations: 3 OSR compilations: 2 Old code discarded: 2

How it works: OSR (on-stack replacement) compiles a long-running loop while the method is still inside it, switching to compiled code mid-loop; @ 4 is the bytecode index of the loop. “Made not entrant” means older code is retired, usually because a better tier replaced it (tier 3 → 4) or an assumption broke (deoptimisation). These lines come from the GST example in this lesson.

P4Medium

Keep the result, or the JIT may drop the work

A benchmark computes discounts in a loop. Make sure the work can't be optimised away by combining every result into a checksum that is printed at the end.

Input
prices 1..1,00,000 (Rs), discount 10% above Rs 1000
Expected output
Checksum: 499910400
Show solution and explanation
public class Main {
    static int discount(int price) {
        return price > 1000 ? price / 10 : 0;
    }

    public static void main(String[] args) {
        long checksum = 0;
        for (int price = 1; price <= 100_000; price++) {
            checksum += discount(price);      // result is used
        }
        System.out.println("Checksum: " + checksum);
    }
}
Checksum: 499910400

How it works: if the loop just called discount(price) and ignored the result, C2 could inline it, see it has no side effects, and delete it: a timing would then measure an empty loop. Using every result (and printing the total) keeps the computation real. JMH does the same thing with its Blackhole class.

P5Business problem

Ignore the warm-up when reporting speed

A team timed 10 rounds of an invoice-generation job (microseconds, as measured on one machine). Report the naive average, the steady-state average after discarding 4 warm-up rounds, and how much slower the first round was.

Input
rounds = {4551, 2228, 1217, 1198, 819, 557, 561, 549, 553, 560}
Expected output
Naive average:  1279.3 us
Steady average: 599.8 us
First round was 7.6x slower than steady state
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        long[] rounds = {4551, 2228, 1217, 1198, 819, 557, 561, 549, 553, 560};
        int warmup = 4;
        long all = 0, steady = 0;
        for (int i = 0; i < rounds.length; i++) {
            all += rounds[i];
            if (i >= warmup) steady += rounds[i];
        }
        double naive = (double) all / rounds.length;
        double steadyAvg = (double) steady / (rounds.length - warmup);
        System.out.printf("Naive average:  %.1f us%n", naive);
        System.out.printf("Steady average: %.1f us%n", steadyAvg);
        System.out.printf("First round was %.1fx slower than steady state%n", rounds[0] / steadyAvg);
    }
}
Naive average: 1279.3 us Steady average: 599.8 us First round was 7.6x slower than steady state

How it works:

  1. The first rounds ran in the interpreter and C1 while C2 was still compiling, so they are much slower. The first six numbers are real measurements from this lesson's warm-up demo.
  2. Averaging everything mixes two different situations. Report steady-state numbers (and, separately, start-up time if that matters to your users).
  3. Tools like JMH run warm-up iterations automatically and repeat measurements in fresh JVMs.
P6Business problem

Daily sales total: primitives in the hot loop

Sum 1,00,000 order amounts (in paise) twice: once with a long accumulator and once with a boxed Long. Show the totals agree, and explain why the hot loop should use the primitive.

Input
amount of order i = (i % 500 + 1) × 100 paise
Expected output
long total: Rs 25050000
Long total: Rs 25050000
Same? true
Show solution and explanation
public class Main {
    public static void main(String[] args) {
        long primitiveTotal = 0;
        Long boxedTotal = 0L;
        for (int i = 0; i < 100_000; i++) {
            long amount = (i % 500 + 1) * 100L;
            primitiveTotal += amount;
            boxedTotal += amount;          // unbox, add, box a new Long
        }
        System.out.println("long total: Rs " + primitiveTotal / 100);
        System.out.println("Long total: Rs " + boxedTotal / 100);
        System.out.println("Same? " + (primitiveTotal == boxedTotal));
    }
}
long total: Rs 25050000 Long total: Rs 25050000 Same? true

How it works:

  1. The results are identical, but boxedTotal += amount creates a new Long object on most iterations (only small values are cached).
  2. Escape analysis can sometimes remove such boxes in compiled code, but you can't rely on it; the primitive version never allocates.
  3. In hot paths (billing runs, analytics), prefer primitives and let the JIT do the rest.
P7Business problem

Latency of the first requests after a deploy

After a deploy, an API's first 10 requests and the next 10 were timed (ms). Compare the averages and the worst request of each group to decide whether the service needs a warm-up step before taking traffic.

Input
first = {180, 150, 96, 88, 61, 47, 40, 33, 30, 28}
later = {22, 21, 23, 20, 22, 21, 24, 22, 21, 23}
Expected output
First 10: average 75.3 ms, worst 180 ms
Next 10:  average 21.9 ms, worst 24 ms
Needs warm-up before traffic? yes
Show solution and explanation
public class Main {
    static void summary(String name, int[] ms) {
        int sum = 0, worst = 0;
        for (int t : ms) {
            sum += t;
            worst = Math.max(worst, t);
        }
        System.out.printf("%s average %.1f ms, worst %d ms%n", name, (double) sum / ms.length, worst);
    }

    public static void main(String[] args) {
        int[] first = {180, 150, 96, 88, 61, 47, 40, 33, 30, 28};
        int[] later = {22, 21, 23, 20, 22, 21, 24, 22, 21, 23};
        summary("First 10:", first);
        summary("Next 10: ", later);
        System.out.println("Needs warm-up before traffic? " + (first[0] > 3 * later[0] ? "yes" : "no"));
    }
}
First 10: average 75.3 ms, worst 180 ms Next 10: average 21.9 ms, worst 24 ms Needs warm-up before traffic? yes

How it works:

  1. Right after start-up the code runs in the interpreter, classes are still loading and caches are cold, so the first requests are several times slower.
  2. Teams send synthetic warm-up requests before adding a new instance to the load balancer, or use readiness checks so real users never see the cold phase.
  3. The figures here are sample data for the exercise; measure your own service.

Real-world scenario questions (asked in company interviews)

Performance questions test whether you measure before you change things. Decide your answer first, then open the model answer.

Fintech · code reviewFresher

“Getters are slow, use public fields”

During a code review for a loan-EMI service, a senior asks you to replace getters with public fields “because method calls are slow in hot loops”. How do you respond?

The interviewer is checking: do you know about inlining, and do you ask for measurements?

Show a strong answer
  • Tiny methods like getters are among the first things C2 inlines once code is hot, so in steady state the call usually disappears and the field is read directly.
  • Public fields would weaken encapsulation (anyone could set an invalid interest rate) for a gain that is probably zero.
  • If there is a real concern, measure: a JMH benchmark or a profiler on the real service, not a guess.

Avoid: optimising by folklore.

E-commerce · deployment1–3 years

Timeouts for two minutes after every deploy

After each rolling deploy of a checkout service, the first one to two minutes on each new instance show many slow requests and some timeouts, then everything is normal. CPU on the new instances is very high during that time. What is happening and what can you do?

The interviewer is checking: can you connect cold-start symptoms to class loading and JIT warm-up?

Show a strong answer
  • A fresh JVM is loading classes and running code in the interpreter, while compiler threads compile hot paths. That burns CPU and makes early requests slow: classic warm-up.
  • Send warm-up traffic (exercise the main endpoints) before the instance is marked ready, and ramp real traffic gradually.
  • Make sure the container has enough CPU for compiler threads during start-up, and consider start-up features such as application class data sharing.
  • Measure: compare latency per instance age to confirm the cause.

Avoid: disabling the JIT tiers or adding flags without measuring.

Analytics · library choice1–3 years

The benchmark that said the new parser is 50 times faster

A teammate times two CSV parsers by running each once on a small file with System.nanoTime(), old parser first, and reports that the new one is 50 times faster. You need to decide which parser the team adopts. What is wrong with the test and how would you redo it?

The interviewer is checking: do you know the classic benchmarking traps?

Show a strong answer
  • The first parser paid for warm-up: class loading of shared libraries, interpretation, JIT compilation. The second benefited from the already-warm JVM. One run on a small file measures start-up, not parsing speed.
  • Redo it with JMH: warm-up iterations, several measurement iterations, separate forks per parser, and consume the parsed results with a Blackhole.
  • Use realistic input sizes (the real daily file), and also check memory allocation and correctness, not only time.

Avoid: trusting a single timing, or benchmarking both in the same JVM run in a fixed order.

Payments · performance tuning3+ years

Throughput dropped after adding a new payment type

A payment router calls processor.process(txn) in a hot loop. For months there was one Processor implementation. After adding a second and third (UPI and wallet), throughput of that loop dropped noticeably, although each implementation is simple. Explain a likely JIT-related reason and how you would confirm it.

The interviewer is checking: do you understand profile-based inlining and deoptimisation?

Show a strong answer
  • With one implementation, C2 saw a single receiver type at that call site and inlined process directly (monomorphic call). When new classes appeared, that assumption broke: the code was deoptimised and recompiled.
  • With two types C2 can still inline both behind a type check (bimorphic), but with three or more the site is usually megamorphic: a virtual call that isn't inlined, which also blocks optimisations such as escape analysis inside the loop.
  • Confirm with -XX:+PrintCompilation (look for “made not entrant” around the change) and diagnostic inlining output, or with a profiler; then decide whether it matters for the business SLA before redesigning (for example, grouping transactions by type).

Avoid: blaming the new classes' code without evidence; the change was in what the JIT can assume.