What does the JIT compiler do in the JVM, and when does it run?
The JIT compiler translates hot bytecode into native machine code, and it does it at run time rather than ahead of time, which is why the JVM can specialise the code for the machine it is actually running on and for the workload it is actually seeing.
The trigger is a counter. Each method has invocation and loop counters, and when a method crosses a threshold the compiler is queued to compile it. Until then it is interpreted, so a method that is called once costs almost nothing to compile. Once compiled, the compiled version replaces the interpreter for that method.
The compiler does considerably more than translate. It profiles the method as it executes — recording which branches are taken, which types flow into the parameters, and whether a call site always receives the same implementation. It then specialises: an inlined method that is always called with the same type gets that exact type baked in, a branch that is never taken is pruned, and a loop gets unrolled. This is why the JVM often outperforms a static ahead-of-time compiler on long-running workloads, and why it can be slower on short-lived ones.
Because the specialisation is based on observed behaviour, it can become wrong. If a call site that only ever received a String suddenly receives an Integer, the compiled code is invalid and the JVM throws it away, returning to the interpreter and recompiling. That is a deoptimisation, and it is the mechanism behind the occasional surprise that a long-running application suddenly gets slower for a moment.
Here is the same thing as runnable code:
public class Main {
public static void main(String[] args) {
// The JIT compiler kicks in once a method is called often enough.
long start = System.nanoTime();
long total = 0;
for (int i = 0; i < 1; i++) {
total += work(i);
}
long interpretedMs = (System.nanoTime() - start) / 1_000_000;
start = System.nanoTime();
for (int i = 0; i < 100_000; i++) {
total += work(i);
}
long compiledMs = (System.nanoTime() - start) / 1_000_000;
System.out.println("total : " + total);
System.out.println("first call ms: " + interpretedMs);
System.out.println("hot loop ms : " + compiledMs);
System.out.println("the numbers vary by machine and JIT state");
}
static long work(int i) {
return i * 3L + 1;
}
}