Learnitweb

Understanding -XX:+PrintCompilation in the JVM

When you run a Java application, the JVM does not immediately compile everything into optimized machine code. Instead, it dynamically profiles, interprets, and JIT-compiles parts of your program based on how frequently they are used.

To see exactly what the JVM is compiling, the JVM provides a powerful diagnostic flag:

-XX:+PrintCompilation

This option prints every method that the JIT compiler compiles during application execution.
It is one of the most useful tools for understanding JVM performance behavior.

1. What Does -XX:+PrintCompilation Do?

When enabled, the JVM prints a line to the console every time:

  • A method is selected for JIT compilation
  • A method finishes compiling
  • A previously compiled method is recompiled with a higher optimization level
  • A method is optimized by the C1 or C2 compiler
  • A method is installed into the JVM’s Code Cache

This shows you:

  • Which parts of your code are “hot”
  • When the JVM optimizes them
  • What compilation tier was used
  • Whether the method was inlined or synchronized
  • Whether it was finally JIT-compiled by the highest tier (C2)

It is an essential tool for:

  • Performance analysis
  • Warm-up behavior study
  • Understanding JVM internal behavior
  • Java benchmarking
  • Learning how JIT compiles your code

2. Basic Usage

You run your Java program with:

java -XX:+PrintCompilation Main

If your program requires arguments:

java -XX:+PrintCompilation Main 5000

This enables compilation logging for the entire session.

3. Example Program

We will use a small but computationally heavy example: generating prime numbers.

PrimeNumbers.java

public class PrimeNumbers {

    public static void generateNumbers(int count) {
        int number = 1;
        int found = 0;

        while (found < count) {
            number = getNextPrimeAbove(number);
            found++;
        }
    }

    public static int getNextPrimeAbove(int start) {
        int n = start + 1;
        while (!isPrime(n)) {
            n++;
        }
        return n;
    }

    public static boolean isPrime(int n) {
        for (int i = 2; i <= Math.sqrt(n); i++)
            if (n % i == 0)
                return false;
        return true;
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        int count = Integer.parseInt(args[0]);
        PrimeNumbers.generateNumbers(count);
    }
}

This code repeatedly calls loops and methods — ideal for triggering JIT compilations.

4. Run the Program With Compilation Logging

Run:

java -XX:+PrintCompilation Main 5000

The JVM begins profiling and compiling the hot methods.

You will see output similar to:

    92   1       java.lang.String::hashCode (28 bytes)
    95   2 %     PrimeNumbers::isPrime (39 bytes)
   110   3       PrimeNumbers::getNextPrimeAbove (31 bytes)
   115   4 %     java.lang.Math::sqrt (11 bytes)
   210   5 %     PrimeNumbers::isPrime (39 bytes)
   230   6       java.util.ArrayList::add (12 bytes)

Now let’s decode these lines.

5. Understanding the Output: Column-by-Column Explanation

Each line represents a compiled method.

Let’s examine a typical line:

95   2 %     PrimeNumbers::isPrime (39 bytes)

Breakdown:

|    | |     |--------------------------- Method name and bytecode size
|    | |----- Symbol indicating extra info (% = code cache)
|    |-------- Compilation ID (ordinal)
|------------- Time in milliseconds since JVM start

Let’s decode each piece.

Column 1: Timestamp (ms)

Example:

95

This means:

  • 95 milliseconds after the JVM started, this compilation was triggered.

This helps analyze warm-up behavior.

Column 2: Compilation Number (ID)

Example:

2

This is the sequential ID for this compilation task.
If numbers look out of order (e.g., 3 appears before 2), that is because:

  • JIT runs in multiple threads
  • Some compilations finish earlier than others

Column 3: Flags / Symbols

You may see symbols like:

SymbolMeaning
%Compiled by C2 (highest tier) and installed in Code Cache
sSynchronized method
!Contains exception handler
bBlocking compilation
nNative method (JNI)
*Method was inlined into another

Example:

%

This means isPrime was optimized and placed into the Code Cache, ready for direct machine-code execution.

Column 4: Compilation Level (0–4)

Not always displayed unless using -XX:+PrintTieredCompilation, but internally:

LevelMeaning
0Interpreted
1Simple compilation (C1)
2Limited optimizations (C1)
3Full C1 optimization
4Full optimization (C2 compiler)

If you see a % symbol, you can assume it reached level 4.

Column 5: Method & Size

Example:

PrimeNumbers::isPrime (39 bytes)

This shows:

  • Which method was compiled
  • The size of its bytecode

Smaller methods (under 50–60 bytes) are often candidates for inlining, which can dramatically improve performance.

6. Understanding the JIT Behavior in This Example

Let’s interpret the earlier output:

95   2 %     PrimeNumbers::isPrime (39 bytes)

What does it mean?

  • isPrime() became hot (called thousands of times)
  • JVM compiled it into native machine code
  • JVM installed it into the Code Cache
  • Future calls will run at maximum speed

Another line:

110   3       PrimeNumbers::getNextPrimeAbove (31 bytes)

Interpretation:

  • The method was compiled
  • No % symbol → not placed into code cache
  • May be a C1 compilation (lower tier)

Another example:

115   4 %     java.lang.Math::sqrt (11 bytes)

This means:

  • Math.sqrt() was compiled at the highest tier
  • Entirely optimized
  • Stored inside the code cache

7. Why Doesn’t Everything Get Compiled?

The JVM only compiles methods that:

  • Run frequently
  • Show performance benefit when compiled
  • Are not too large (huge methods do not inline or compile well)
  • Get executed enough times to justify compilation cost
  • Are not in trivial code paths

This is why, for small workloads:

  • Only core Java methods get JIT-compiled
  • Your code stays interpreted

But for heavier workloads:

  • Your methods become hot
  • JVM optimizes them

This selective compilation is key to JVM efficiency.

8. Textual Diagram: How JVM Decides to Compile

       +-----------------------------------------------+
       | JVM Interpreter executes bytecode             |
       +-----------------------------------------------+
                          |
                          v
           JVM Profiling Engine counts:
           - Method calls
           - Loop iterations
           - Branch frequency
                          |
                          v
             Is the method hot enough?
                    /        \
                  No          Yes
                  |            |
                  v            v
      Continue interpreting   Queue for JIT
                               (background thread)
                                   |
                                   v
                   Native Machine Code Generated
                                   |
                                   v
                        Store in Code Cache (optional)
                                   |
                                   v
                 JVM switches from interpreted → compiled

9. Advanced Usage: Combining with Other Diagnostic Flags

You can get even more detail using:

Print only hot methods:

-XX:+PrintInlining

Print bytecode as well:

-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly

Print compilation tiers:

-XX:+PrintTieredCompilation

Show reasons for compiling:

-XX:+LogCompilation

Together, these tools give you deep insight into JVM behavior.