🔥 JIT Compilation in Java & Code Caching: Enhancing Performance for Faster Execution 🔥
Published: 2023-06-10 17:47:17

**Just-In-Time (JIT) compilation** is a game-changer in the world of Java programming, unleashing the power of optimization during runtime. 🚀 Imagine code that not only gets translated into bytecode but also dynamically compiles into native machine code for lightning-fast execution. This is where JIT compilation comes into play, revolutionizing the performance of our Java applications. 🏃‍♂️

In Java, the execution process involves two crucial steps: the translation of code into bytecode and the interpretation of bytecode by the Java Virtual Machine (JVM). However, the JIT compiler takes things to the next level. It continuously analyzes the bytecode and identifies frequently executed portions, known as hotspots. 🎯

Once these hotspots are detected, the JIT compiler swings into action. It optimizes the code by compiling it into highly efficient native machine code, which can be executed directly by the CPU. This dynamic adaptation of the JVM allows for real-time performance enhancements based on the actual runtime conditions of our program. 📈

To grasp the magic of JIT compilation, let's dive into a practical example. 🧪 Consider the following Java code snippet:

```java
/**
 * The PrimeNumberGenerator class generates a specified number of prime numbers.
 */
public class PrimeNumberGenerator {

    /**
     * The main method of the PrimeNumberGenerator class.
     * It takes a command-line argument specifying the total number of prime numbers to generate.
     *
     * @param args The command-line arguments.
     */
    public static void main(String[] args) {
        int numbersToGenerate = Integer.parseInt(args[0]);
        generateNumbers(numbersToGenerate);
    }

    /**
     * Generates the specified number of prime numbers.
     *
     * @param totalNumbers The total number of prime numbers to generate.
     */
    private static void generateNumbers(int totalNumbers) {
        int totalPrimesGenerated = 1;
        int next = 2;
        while (totalPrimesGenerated <= totalNumbers) {
            next = getNextPrimeAbove(next);
            System.out.println("Next Prime Number: "+ next);
            totalPrimesGenerated++;
        }
    }

    /**
     * Finds the next prime number greater than the given previous number.
     *
     * @param previous The previous number.
     * @return The next prime number.
     */
    private static int getNextPrimeAbove(int previous) {
        Integer testNumber = previous + 1;
        while (!isPrime(testNumber)) {
            testNumber++;
        }
        return testNumber;
    }

    /**
     * Checks if the given number is prime.
     *
     * @param testNumber The number to test.
     * @return True if the number is prime, false otherwise.
     */
    private static boolean isPrime(Integer testNumber) {
        for (int i = 2; i < testNumber; i++) {
            if (testNumber % i == 0) return false;
        }
        return true;
    }
}
```

This code generates a specified number of prime numbers. But what's interesting is that the execution count of the `isPrime` method directly affects the generation process. So, let's run the code and see the results unfold! 💫


![PrimeNumberGenerator Program Execution](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uruceql0wnic6tdmp7xw.png)

As we can see from the execution results, the program runs successfully. But let's take things up a notch and peek behind the scenes. We can print the **JIT Compiler Compilation logs** to see which methods are being natively compiled. To achieve this, we'll follow these steps:

1. Compile the Java program using the command `javac PrimeNumberGenerator.java`.
2. Run the program with the JVM instructed to print the compilation logs on the console using the command `java -XX:+PrintCompilation PrimeNumberGenerator 15`. Alternatively, we can generate a JIT compilation log file by running the program with the command `java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation PrimeNumberGenerator 15`.

After running the program, A log file named hotspot_pid<pid>.log is generated, revealing the secrets of JIT compilation. 😲

![Program Execution With Generating Log File](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vxi9ac4jfs47il4xrs2c.png)

Analyzing the log file, we notice that the `isPrime` method, our target for optimization, did not undergo any compilation. But don't fret! This is due to the limited execution count since we generated only 15 prime numbers. 🧮

Let's level up our analysis and generate 5000 prime numbers this time using the command `java -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation PrimeNumberGenerator 5000`.

Behold! In the image below, we'll witness the mighty JIT compiler optimizing the isPrime method through various levels of compilation. 🌟


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xjctgkvd4o4jakax27hf.png)


## Leveled Compilation in JIT Compiler: A Performance Marvel

When it comes to JIT compilation, it's all about levels and tiers of optimization. The JVM is not content with a one-size-fits-all approach. Instead, it employs leveled compilation to gradually boost performance and deliver optimal results. 📈

Here's the deal: The initial tier, known as **Tier 1**, focuses on swiftly generating compiled code while keeping compilation times low. This level of compilation includes nifty optimizations like inlining frequently called methods or removing redundant checks. But the journey doesn't end there! 😎

As our program continues to execute and hotspots persist, the JIT compiler cranks up the heat and elevates the hotspots to higher tiers like **Tier 2, Tier 3, and Tier 4**. These tiers introduce more advanced and time-consuming optimization techniques, such as loop unrolling, advanced inlining, and specialized code transformations. Gradually, the performance of our code scales new heights without overwhelming the initial compilation phase. It's an optimization marvel in action! 💪

However, there's a twist in the tale. When a Java bytecode is compiled to Tier 4, the code finds its home in the mighty Code Cache, a sacred space with a size limit. But what happens if this cache overflows? It will show up a message like this:

**VM warning: CodeCache is full. Compiler has been disabled.**

This foreboding warning indicates that the code could run even better if more parts of it could be compared to native machine code. Alas, there's no room in the code cache for further optimization. But fear not! Increasing the size of the code cache can unlock new realms of application performance. 🚀

We can gauge the size of the code cache using the JVM flag -XX:+PrintCodeCache, which yields intriguing insights into its dimensions. 📊

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fb8mwkn76zk4987ju9ph.png)


To customize the code cache size, we have three essential flags at o urdisposal:

**1. InitialCodeCacheSize:** This flag sets the initial size of the code cache when the JVM starts. We can tweak it using the `-XX:InitialCodeCacheSize` VM option. The default value varies depending on the JVM implementation and platform.

**2. ReservedCodeCacheSize:** It determines the maximum size the code cache can grow up to. Use the `-XX:ReservedCodeCacheSize={sizeInMb}m` VM option to adjust this value. The default value also depends on the JVM implementation and platform.

**3. CodeCacheExpansionSize:** This flag governs the amount by which the code cache can expand when it reaches its limits. We can set it using the `-XX:CodeCacheExpansionSize` VM option. The default value is typically a fraction of the reserved code cache size.

Code Snippets Are Available At: [https://github.com/belwalpb/jvm-code-cache-optimization](https://github.com/belwalpb/jvm-code-cache-optimization)


## Conclusion: Unleashing the Full Potential of Java

In this captivating journey, we've unraveled the incredible power of JIT compilation in Java. The ability to optimize program execution dynamically opens doors to unprecedented performance levels. JIT compilation, with its leveled approach, propels our code to greatness by gradually refining it through multiple tiers of optimization. And let's not forget the sacred Code Cache, a sanctuary for native machine code that can supercharge our application's speed. 💥

Remember, the size of the Code Cache plays a pivotal role in unlocking further performance enhancements. If we find ourself restricted by a full Code Cache, fear not! By tweaking the cache's size, we can unleash the true potential of our Java application and watch it soar to new heights. 🚀✨

Now, armed with this knowledge, go forth and conquer the world of Java optimization. Embrace JIT compilation, harness the power of Code Caching, and witness the remarkable transformation of our applications. Happy coding! 😄👩‍💻👨‍💻