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.
