DEV Community

Cover image for How the JVM Executes Your Code: From Bytecode to Native Code
Felipe Azevedo
Felipe Azevedo

Posted on

How the JVM Executes Your Code: From Bytecode to Native Code

If you program in Java, you've probably heard that "Java is slow." But is it really? The JVM is a true optimization machine, and if you understand how it works, you can make the most of its power. In this article, we'll explore how your code goes from something readable by humans to a whirlwind of binary instructions running at full speed on your CPU.

1. From Source to Bytecode: The First Transformation

It all starts with your beautiful, well-indented code (or maybe not so much). When you compile a .java file with javac, it doesn't turn into a binary ready to run on your operating system. Instead, the compiler generates bytecode, a set of intermediate instructions that the JVM understands.

This means your code can run on any system with a JVM installed. It's like a universal pass to run Java anywhere.

Basic example of Java code:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, JVM!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Compiling:

javac HelloWorld.java
Enter fullscreen mode Exit fullscreen mode

This generates a HelloWorld.class file, which contains bytecode.

2. Bytecode Interpretation by the JVM

The JVM Interpreter takes that bytecode and executes it instruction by instruction. It works like a simultaneous translator between your code and the CPU. This ensures that Java runs on any platform, but it can be a bit... slow.

To do its job, the JVM uses an operand stack, where it pushes and pops values as needed. Each method call gets a stack frame, ensuring everything is isolated and organized. This stack is essential for correct program execution, storing local variables, temporary values, and references to objects.

In addition, the JVM has several memory areas that help with execution, like:

  • Heap: Where dynamically created objects are stored.
  • Metaspace: Where information about classes is stored.
  • Stack: Where method calls and local variables are stored.

Why Pure Interpretation Can Be Inefficient?

Interpreting line by line is flexible, but it can be slow because each instruction has to be analyzed and executed individually. Imagine you needed to add two numbers millions of times; doing that through the interpreter would take much longer than executing raw machine code.

That's why the JVM doesn't stick to pure interpretation. It has a trick up its sleeve called the Just-In-Time (JIT) Compiler, which we'll look at next.

3. Just-In-Time (JIT) Compiler

Instead of interpreting each line every time the program runs, the JVM realizes that certain parts of the code are used all the time. These "hotspots" are then compiled just in time into pure machine code, eliminating the need for interpretation.

This is the magic of the JIT Compiler. It dynamically converts bytecode into native code, vastly improving performance.

JIT Strategies

The JVM has different strategies for Just-In-Time compilation, each optimized for a specific scenario:

Client (C1):

C1 focuses on fast compilation, ideal for short-lived applications. It excels at reducing initial latency and delivering acceptable performance quickly without doing complex optimizations. Its main feature is the ability to compile methods efficiently without compromising runtime. This model is perfect for scenarios where response speed is more important than long-term performance, like in desktop applications where quick startup is critical.

Server (C2):

C2 is designed for aggressive optimizations, aiming to improve long-term performance. Its main advantage is generating highly optimized code after analyzing the program's execution patterns. The C2 compilation process involves gathering execution statistics, which allows for more efficient code generation. This type of compilation is ideal for servers and applications that run continuously, where the benefits of advanced optimizations can be fully leveraged over time, maximizing performance for long-running systems.

Graal JIT:

The Graal JIT stands out for its high performance and flexibility, supporting multiple languages. Its modern architecture outperforms C2 in some situations, and because it's written in Java, it's highly adaptable to different contexts. It uses advanced optimization and code generation techniques to maximize performance, offering significant gains, especially in more demanding scenarios. The Graal JIT is ideal for applications that need the best possible performance, such as machine learning workloads, microservices, and cloud-native applications, where efficiency and scalability are crucial.

Tiered Compilation: The Best of Both Worlds

The JVM uses a scheme called "Tiered Compilation", which combines the benefits of the fast C1 compiler and the optimized C2 compiler.

  • Level 0: Code is 100% interpreted, with no compilation.
  • Level 1 (C1 without optimizations): Code starts being compiled quickly to improve initial performance.
  • Level 2 (C1 with execution profiles): The JVM begins collecting data about how the code is being used.
  • Level 3 (C2 optimized): Critical parts of the code are recompiled with aggressive optimizations for maximum efficiency.

With this scheme, the JVM can give a quick response time without sacrificing long-term performance. This is especially useful for applications that need to respond quickly but also run for a long time.

4. Advanced JVM Optimizations

The JVM doesn't just compile code in real-time; it also performs optimizations such as:

  • Inlining: If a method is small and called often, the JVM can copy it directly to the place where it's invoked, eliminating the cost of the method call.
  • Escape Analysis: If the JVM sees that an object never leaves a method's scope, it can allocate it on the stack instead of the heap, improving performance and reducing the Garbage Collector's workload.
  • Dead Code Elimination: If a piece of code is never executed, the JVM simply removes it from the compiled version.
  • Loop Unrolling: Instead of running a loop multiple times, the JVM can transform a short loop into a sequence of direct operations, reducing the number of condition checks.
  • Branch Prediction Optimization: The JVM reorganizes code to better align with the processor's flow prediction, reducing penalties for branch prediction failures.

These optimizations make the JVM seem like magic, turning readable code into something highly efficient without the programmer needing to lift a finger. Pretty impressive, right?

Conclusion

As you can see, the JVM is much more than just a runtime environment for Java. It has an extremely sophisticated optimization process. From interpreting bytecode to Just-In-Time compilation and advanced optimizations, the JVM is constantly working to ensure that your code runs as efficiently as possible.

So, the next time someone says "Java is slow," you can just smile and reply, "The JVM is a performance machine, and those who understand how it works know how to make the most of it." 🚀

Top comments (0)