Even though there are only two basic compilers (+ interpreter) in Java, there are five levels of executions, because the client (C1) compiler has three different levels of compilation. The server compiler (C2) has only one.
Level 0 – interpreted code
Level 1 – simple C1 compiled code (with no profiling)
Level 2 – limited C1 compiled code (with light profiling)
Level 3 – full C1 compiled code (with full profiling)
Level 4 – C2 compiled code (uses profile data from the previous steps)
The usual path is 0 -> 3 -> 4, so the code is interpreted first, then when it gets hot enough, it’s compiled by the client compiler (C1) with full profiling enabled and finally, the server compiler (C2) compiles the code using profile data collected by C1.
But there are three exceptions:
Method is simple
If the method to be compiled is trivial, then it is compiled only at level 1 (C1 without profiling), because the server compiler wouldn’t make it faster – there is no point in doing extensive profiling to find out how the code is used if it can be made fast without it.
C2 is busy
If at some point the server compiler queue gets full, the method will be taken from the server queue and compiled at level 2 (with light profiling which makes the method compiled sooner). After some time, the code will be compiled at level 3 (with full profiling) and finally, when the server queue is less busy, it will be compiled by the server compiler (again, using profile data collected at level 3).
C1 is busy, but C2 is not
If C1 queue is full, but C2 queue is not, then the method can be profiled by the interpreter (level 0) and then it can go directly to C2 (so the client compiler is not involved at all).
It is worthwhile to mention that compiler queues are not standard FIFO, but the priority ones.