Engineering research / java

Diagnose JVM Slowdowns When the Code Cache Stops Compilation

A full JVM code cache can stop compilation and leave new hot methods interpreted. A measured JDK 23 reproduction, code heap sizing, and jcmd diagnostics show how to investigate the slowdown. The measured run did not recover; that does not establish permanent compiler shutdown.

By Sandeep Belgavi · Published 2026-11-03

Core engineering · Practical technical guide · 3 November 2026

A full JVM code cache can stop compilation and leave new hot methods interpreted. A measured JDK 23 reproduction, code heap sizing, and jcmd diagnostics show how to investigate the slowdown. The measured run did not recover; that does not establish permanent compiler shutdown.

A service that got slower and stayed slow

The catalogue service at Belgavi's Book Store is a large Java application: a web framework, a template engine, several JSON libraries and hundreds of generated mappers. After a release that added a reporting module, latency doubled a few minutes after each start and never recovered. Heap, garbage collection and CPU limits all looked normal. The one clue was a line near the top of the log: CodeCache is full. Compiler has been disabled.

The code cache is the native memory where the JIT compilers put compiled methods. When it has no room, HotSpot stops compiling. Methods that were compiled stay compiled; everything else, including code paths that become hot later, keeps running in the interpreter.

How the code cache is laid out

The java command documentation gives the default maximum code cache size as 240 MB, or 48 MB with tiered compilation disabled, and caps the flag at 2 GB. JEP 197 split the cache into three code heaps, and the documentation says segmentation is on by default when tiered compilation is enabled and the reserved size is at least 240 MB. The heaps are: non-method code such as the interpreter and compiler buffers, profiled code from the C1 tier with a short expected lifetime, and fully optimised non-profiled code from C2. The segments are not resized at runtime.

On my machine, JDK 23.0.2's defaults are a 240 MB reservation (251,658,240 bytes) with 8,192,000 bytes for non-method code and about 116 MB each for profiled and non-profiled code, as printed by -XX:+PrintFlagsFinal. Setting ReservedCodeCacheSize=200m turned SegmentedCodeCache off, which matches the 240 MB rule: smaller caches are one heap.

A reproduction

To see the failure without a large application, I generated a Java class with 4,000 small static methods called in a hot loop, so that each becomes worth compiling, and ran it for 20,000 rounds on JDK 23.0.2 on one Windows 11 machine. Each configuration ran once; treat the times as an illustration of the effect, not a benchmark.

SettingCompiled methods at exitTotal timeLast tenth of the runCompiler state at exit
Default (240 MB, segmented)6,32811.7 s449 msenabled
ReservedCodeCacheSize=240m6,56212.3 s391 msenabled
ReservedCodeCacheSize=8m (one heap)5,69316.4 s447 msenabled
ReservedCodeCacheSize=4m (one heap)32159.9 s6,224 msdisabled, stopped_count=1, restarted_count=0

With 4 MB, the warning appeared 2.5 seconds in, the compiler was disabled with the reason "not enough contiguous free space left", and it was never restarted during the remaining 57 seconds. The last tenth of the run took about 16 times as long as with the default. With 8 MB the cache did not fill, but the run took about a third longer than with 240 MB: most slices were slower and the last tenth took 447 ms against 391 ms. I did not investigate why, and one run is not enough to attribute it. The workload's compiled code peaked at roughly 9 MB in the default runs, so 4 MB was simply too small.

Output excerpt from the measured run; nothing else is edited.

$ java -XX:ReservedCodeCacheSize=4m -XX:+PrintCodeCache Hot 20000
[2.467s][warning][codecache] CodeCache is full. Compiler has been disabled.
[2.467s][warning][codecache] Try increasing the code cache size using -XX:ReservedCodeCacheSize=
...
CodeCache: size=4096Kb used=3300Kb max_used=3656Kb free=795Kb
 total_blobs=704, nmethods=321, adapters=286, full_count=1
Compilation: disabled (not enough contiguous free space left), stopped_count=1, restarted_count=0

Why a full cache is worse than it sounds

  • Recovery was not observed in this run. The compiler did not restart during the measured run; this does not establish permanent shutdown in other workloads. The java documentation describes UseCodeCacheFlushing, on by default, as flushing the code cache before shutting down the compiler, which did not prevent the shutdown here.
  • It hits the newest code. Methods compiled early, often startup code, keep their space. Code that becomes hot later, such as a new endpoint after a traffic shift, stays interpreted.
  • It looks like a CPU problem. Interpreted code burns more CPU per request, so autoscalers add instances, each of which fills its own cache the same way.
  • Fragmentation matters. The reason printed was contiguous space, not total space: 795 KB were free, but not in a block large enough for the next method.

What fills it in real applications

Large frameworks, generated code (serialisation mappers, proxies, lambdas), dynamic languages on the JVM and heavy reflection all add methods that get hot. Tiered compilation keeps both a profiled C1 version and an optimised C2 version of hot methods for a while, which is why the profiled heap exists at all. Deoptimisation and recompilation after class loading changes consume more. Disabling tiered compilation shrinks the default cache to 48 MB according to the documentation, so a flag added for startup reasons elsewhere can lower the ceiling.

Seeing it in production

Three sources, from cheapest to most detailed. The log line above, at warning level, which is worth an alert on its own. The memory pools: through java.lang.management, JDK 23 reports non-heap pools named CodeHeap 'non-nmethods', CodeHeap 'profiled nmethods' and CodeHeap 'non-profiled nmethods' when segmented, or one pool named CodeCache otherwise; I listed them with a ten-line program. Most metrics agents export these pools, so used versus max per pool can be graphed and alerted at 80 or 90 percent. And jcmd <pid> Compiler.codecache, which the jcmd documentation describes as printing the code cache layout and bounds, with Compiler.CodeHeap_Analytics for a deeper look at free blocks and method ages.

Sizing it

  1. Run the service under realistic traffic for long enough to reach steady state, including the code paths that only run hourly or daily.
  2. Read the peak used size of each code heap, or of the single cache.
  3. Set ReservedCodeCacheSize to two to three times that peak, but keep at least 240 MB if you want segmentation; the reservation is virtual address space, and pages are committed as they are used.
  4. Count the memory against the container limit anyway: committed code cache is native memory outside the heap, and Native Memory Tracking reports it under Code.
  5. Re-check after each large dependency upgrade or new module, because the peak moves with the code base.

Failure modes and checks

  • A small value copied from an old guide. Startup-tuning articles sometimes lower the cache. Search the launch flags for ReservedCodeCacheSize and TieredCompilation.
  • Alerting on heap only. The heap is fine in this failure. Alert on the log line and on code heap usage.
  • One full heap among three. With segmentation, the profiled heap can fill while the others have room. Look at each pool, not the sum.
  • Assuming a restart fixes it. It does, until the same code paths become hot again.

Sources and scope

Checked against these sources on 3 November 2026. Measured on 3 November 2026: JDK 23.0.2 on one Windows 11 machine, a generated 4,000-method class, one run per setting. Illustrative timings, not a benchmark.

A full code cache can disable compilation and leave new hot methods interpreted. In this experiment the compiler did not restart during the observed run; recovery in other workloads was not measured. Alert on the warning and on per-heap code cache usage, size ReservedCodeCacheSize from the measured peak, and keep 240 MB or more for segmentation.

Views are my own and do not represent any employer, past or present.