Learnitweb

Understanding the JVM Code Cache

When studying the JVM and its Just-In-Time (JIT) compilation process, one important component to understand is the code cache.
This cache stores the optimized native machine code generated by the JIT compilers.
Knowing how the code cache works, how to inspect it, and how to tune its size can help improve application performance, especially in large or long-running Java applications.

Modern JVMs have evolved, and the structure and reporting format of the code cache have changed.
This tutorial brings both the theoretical foundation and the modern JVM output together to give a complete understanding.


1. What the Code Cache Is and Why It Matters

As a Java program runs, the JVM identifies frequently executed methods and compiles them into native machine code.
This is done through tiered compilation:

  • Tier 1–3: compiled by the C1 compiler
  • Tier 4: compiled by the C2 compiler (the highest optimization level)

Once a method reaches a high enough tier (often tier 4), the JVM may choose to store the compiled machine code inside the code cache.

The benefits of being in the code cache include:

  • Faster execution
  • Reduced interpretation overhead
  • Improved application throughput

However, the code cache has a finite size. When it fills up:

  • The JVM cannot store more compiled code
  • It may disable further compilation
  • Performance may degrade because unoptimized bytecode must be interpreted

A full code cache frequently results in the warning:

CodeCache is full. Compiler has been disabled.

This does not stop the application but prevents it from reaching optimal performance.


2. How the JVM Organizes the Code Cache Today

Older tutorials and older JVMs show a single block of memory called “CodeCache.”
Modern JVMs (Java 8u40+, Java 11+, Java 17+, Java 21+) introduced segmented code heaps, dividing the code cache into three separate regions.

When you run:

-XX:+PrintCodeCache

Your output may look like this:

CodeHeap 'non-profiled nmethods': size=120000Kb used=74Kb max_used=74Kb free=119925Kb
CodeHeap 'profiled nmethods': size=120000Kb used=228Kb max_used=228Kb free=119772Kb
CodeHeap 'non-nmethods': size=5760Kb used=1299Kb max_used=1304Kb free=4460Kb
total_blobs=639 nmethods=227 adapters=317
compilation: enabled

This reflects the modern internal structure of the code cache.


3. Understanding Each CodeHeap Region

The JVM splits its compiled code across three heaps.
Each heap has a specific purpose.

CodeHeap ‘non-profiled nmethods’

Stores:

  • Early-stage compiled code
  • C1 tier 1 and tier 2 methods
  • Methods compiled without profiling information

These help the JVM warm up quickly.

CodeHeap ‘profiled nmethods’

Stores:

  • Tier 3 (C1 profiled) methods
  • Tier 4 (C2 fully optimized) methods

This is the region that has the biggest impact on performance.
A fully optimized method, such as a hot numerical routine or string operation, ends up here.

CodeHeap ‘non-nmethods’

Stores:

  • Interpreter stubs
  • Adapter handlers
  • Runtime helper code
  • On-stack replacement (OSR) stubs

These are not compiled Java methods but essential runtime components.


4. Why Your Output Looks Different from Older Tutorials

Older tutorials show output like:

CodeCache: 20480Kb used=1024Kb free=19456Kb

Modern JVMs replaced this single code cache with separate CodeHeaps for improved efficiency, lower fragmentation, and more predictable warm-up behavior.

The meaning is the same, but the structure is more detailed.

The total code cache is simply the sum of all code heaps.


5. Monitoring Code Cache Usage with -XX:+PrintCodeCache

You can inspect the code cache usage using:

java -XX:+PrintCodeCache Main 5000

This gives:

  • Total size of each code heap
  • How much space is used
  • How much free space remains
  • Whether the JIT compiler is enabled

If you notice that the “used” portion of any heap approaches the “size,” the code cache may be filling up.

This is especially common in:

  • High-throughput servers
  • Big data applications
  • Systems with many dynamically generated methods
  • Large enterprise applications with many hot code paths

6. When the Code Cache Becomes a Problem

If the code cache becomes full, you may see:

CodeCache is full. Compiler has been disabled.

This means:

  • The JVM cannot compile new hot methods
  • Existing compiled methods may be evicted
  • Overall performance declines
  • The JVM switches back to interpreting bytecode for new hotspots

This problem is more common in applications that run for long periods and experience varying workloads.


7. Adjusting the Code Cache Size with JVM Flags

Three important JVM flags allow you to tune the size and growth of the code cache.

InitialCodeCacheSize

Sets the starting size of the code cache at JVM startup.

Example:

-XX:InitialCodeCacheSize=1M

ReservedCodeCacheSize

Defines the maximum size the code cache can grow to.

Example:

-XX:ReservedCodeCacheSize=28M

CodeCacheExpansionSize

Defines how much the cache grows when more space is needed.

Example:

-XX:CodeCacheExpansionSize=64K

You can specify values in:

  • bytes
  • kilobytes (K or k)
  • megabytes (M or m)
  • gigabytes (G or g)

8. Example: Increasing the Code Cache to 28 MB

Suppose we want a 28 MB code cache.
Run the application like this:

java -XX:ReservedCodeCacheSize=28M -XX:+PrintCodeCache Main 5000

The output will now reflect the increased size:

CodeHeap 'profiled nmethods': size=28672Kb used=230Kb ...

This confirms the code cache has been expanded from its default to 28 MB.

This tuning can significantly improve performance in applications that:

  • Contain many hot code paths
  • Use frameworks with heavy runtime generation
  • Run for long periods
  • Rely on repeated numerical or functional operations

9. Textual Diagram: The Code Cache and Modern CodeHeaps

To help visualize the structure:

+---------------------------------------------------+
|                   JVM Code Cache                  |
+------------------------+--------------------------+
|   non-profiled         |     profiled             |
|     nmethods           |     nmethods             |
| (C1 tiers 1–2 code)    | (C1 tier 3 and C2 code)  |
+------------------------+--------------------------+
|                 non-nmethods heap                 |
|     stubs, adapters, runtime helpers, OSR code    |
+---------------------------------------------------+

All three together make up the full code cache that the JVM manages.