The labs on this site need JavaScript to run. The same content is written out below.
Learn › Java › JVM
JIT warmup
Why are the first requests right after a deployment slow?
Java code is compiled to bytecode, and the JVM runs it by interpreting it one bytecode instruction at a time. The JVM finds the methods that run often and compiles them again, this time to machine code (JIT), and from then on they run much faster.
Checked against real execution · Values actually measured on JDK 21
Concepts in this lab
- JIT compiler
- This JVM component compiles frequently executed bytecode to machine code while the program runs, and from then on runs that machine code directly. At first the interpreter interprets and runs the bytecode one instruction at a time; then the JVM finds the parts that run repeatedly and optimizes them. This is why a Java application is faster after it has run for a while than it is right after startup (warmup).
- Bytecode
- This is the intermediate form of code that the Java compiler (javac) produces from source code, and it is stored in .class files. It is not the machine code of a particular CPU or operating system but a set of instructions that the JVM understands, so it runs on the JVM of any platform.
- Java Virtual Machine (JVM)
- This is the program that reads and runs Java bytecode. Each operating system has its own version of it, so a Java program that is compiled once can run on several operating systems without modification. It loads classes, manages memory (garbage collection), and converts bytecode to machine code and runs it.
- Class loader
- This JVM component finds class files and reads them into memory. Classes are not all read when the program starts; each class is loaded at the point when it is first needed. After a class is loaded, the JVM makes it ready for use through linking (verification, preparation, and resolution of symbolic references) and initialization (running static initializers).
Try these
- Default settings — The first batch takes 5.8ms, and the batches after it take about 1.4ms.
- With JIT turned off — It never gets faster. About 29ms per batch.
- With a warmup in advance — It is fast from the first user request.
Running with the default settings
- The Java compiler (javac) turns source code into bytecode, not machine code. When the program starts, the interpreter of the JVM interprets and runs this bytecode one instruction at a time. The bytecode runs on any CPU, but this is much slower than running machine code directly.
- Batch #1: 5.8ms. At first the code is run by the interpreter. The JVM counts how many times the method has been called and how many times the loop has run, and when the counts pass a threshold, it compiles this method to machine code. This batch ran as interpreted execution until the compiled code was ready (the compilation itself runs on another thread).
- Batch #2: 1.7ms. The code has started to run as compiled machine code. It is 3 to 4 times faster than the first batch. Using the information it gathered while running (such as which branches are taken often), the JVM optimizes the code once more, this time more heavily.
- Batch #3: 1.4ms.
- Batch #4: 1.7ms.
- Batch #5: 1.5ms.
- Batch #6: 1.4ms.
- Batch #7: 1.5ms.
- Batch #8: 1.4ms.
- Batch #9: 1.4ms.
- Batch #10: 1.4ms.
- Batch #11: 1.6ms.
- Batch #12: 1.7ms.
Result of this run
- The code is the same, but its speed at the start and later is different. The JVM does not compile all the code in advance. It picks only the parts that actually run often and compiles them while the program is running (just-in-time). Because it optimizes based on information from the actual execution, once the code is fully warmed up it runs at a speed close to that of a language compiled in advance.
- Right after a deployment, the code has not been JIT-compiled yet, and on top of that, classes are still being loaded, the connection pool is still being filled, and the caches are empty, so the first requests are especially slow. This is also why traffic is moved to a new server a little at a time and not all at once. When you measure performance, too, discard the first period and look at the values after the code is fully warmed up.
Code
static long work(int n) { // the method that was measured
long s = 0;
for (int i = 0; i < n; i++) s += (i * 31L) ^ (s >>> 3);
return s;
}
for (int batch = 1; batch <= 12; batch++) {
long t = System.nanoTime();
for (int k = 0; k < 2000; k++) work(1000); // one batch = 2,000 calls
print((System.nanoTime() - t) / 1000 + " microseconds");
}
// Run: java JitWarmup.java
Assumptions of this model
The numbers are values measured by actually running the code above on JDK 21 on this PC (the first of two runs with the default settings, and one run with -Xint). The values of "Warmed up in advance" are the default-settings measurement with the first batch removed and the rest moved up, and the last value (1.4ms) is not a measurement but a similar value added at the end. The numbers vary with the CPU and with when the code is run, but the shape is the same: the first period is slow, and the code gets faster after that. A JVM with the default settings applies a light compilation (C1) and a strongly optimizing compilation (C2) in stages.
Previous lab: GC and memory leaks · Next lab: Class loading and initialization