← Debugging Guides

阅读中文版本

Java GC overhead limit exceeded — Find the Allocation Pressure

Diagnose GC overhead limit errors with jstat, heap histograms, and GC logs, then fix leaks and allocation hotspots before increasing the heap.

advanceddebuggingjavagc

Symptoms

  • java.lang.OutOfMemoryError: GC overhead limit exceeded
  • The application becomes unresponsive before the error appears
  • jstat shows nearly full old generation and high FGC count
  • CPU is saturated by GC threads while throughput collapses

Possible causes

  • The live set is close to the heap size, so collections recover almost nothing
  • A cache or collection grows without a bound and retains objects
  • The service allocates large temporary objects per request, raising collection frequency
  • The heap was sized for average load but not for peak concurrency

Diagnosis steps

Watch garbage collection in real time

jstat -gcutil 12345 1000 10

Interpretation

O and M columns near 100 percent with a rising FGC count mean the old generation is full. YGC rising fast with low survivor usage indicates high allocation rate.

Inspect the heap composition

jcmd 12345 GC.heap_info && jmap -histo:live 12345 | head -25

Interpretation

The histogram shows which classes dominate live memory. A huge HashMap, byte array, or domain object count points to a cache or accumulation bug rather than a heap that is merely too small.

Enable GC logging for the next run

java -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags -jar app.jar

Interpretation

The log shows pause times, heap before and after collection, and whether humongous allocations or full GCs dominate. Compare live memory after full GC with -Xmx.

Confirm the JVM's ergonomics

java -XX:+PrintFlagsFinal -version | grep -E 'MaxHeapSize|G1HeapRegionSize|UseG1GC'

Interpretation

In a container, the JVM should respect the container limit. A MaxHeapSize larger than the container memory leads to the OS killing the process instead of a clean OOM.

Root cause

The JVM throws GC overhead limit exceeded when it spends most of its time collecting but recovers very little memory, typically because the live set nearly fills the heap. It is a symptom of either a leak, an oversized cache, or a workload whose working set exceeds the configured heap, not a signal that GC settings alone are wrong.

Fix

Find and remove the retention or allocation source first: bound caches, stream large result sets, avoid per-request copies, and fix static collections that grow forever. Then size the heap from measured live-set data, enable GC logging, and tune pause goals only after the leak is gone. For very large heaps and low-latency requirements, evaluate a generational collector supported by your JDK.

Risk note

Increasing -Xmx delays the failure and can turn a clean OOM into a container kill or long full GC pauses. Disabling the GC overhead check with -XX:-UseGCOverheadLimit does not free memory. Heap dumps can be large enough to fill the disk; set -XX:HeapDumpPath explicitly.

Verify the fix

After the fix, run the same load and confirm old generation usage drops after each collection, full GC count stays near zero, and GC time is a small fraction of wall time. Check that the service sustains peak traffic without OOM and that heap after a forced full GC is well below -Xmx.

The JVM is collecting but not reclaiming

GC overhead limit exceeded is not the same as a simple heap exhaustion. The JVM has detected that it spends most of its time in GC and recovers almost nothing, so it aborts rather than thrash forever.

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

Measure before resizing

jstat -gcutil 12345 1000 10
jcmd 12345 GC.heap_info
jmap -histo:live 12345 | head -25

If old generation usage stays near 100 percent across collections, the live set is too large. The histogram identifies what is retained. A domain class count that grows with uptime is a leak; a huge byte array count may be response buffering.

Reduce retention, then size

  • Bound caches with maximum size and expiry.
  • Stream or paginate large queries instead of loading all rows.
  • Remove static maps that accumulate per tenant or request.
  • Fix per-request copies of large payloads.
  • Size -Xmx from live-set measurements, not from total machine memory.
java -Xms4g -Xmx4g -Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20m -jar app.jar

Tune pause goals only after the leak is gone. A larger heap with the same leak just lengthens the time between failures.

Verify

Run the production load and watch jstat: old generation should drop after each mixed or full collection, FGC should stay near zero, and GC time should be a small fraction of wall time. Force a full GC in a test environment and confirm live memory is comfortably below -Xmx. If the container limit is lower than the heap, fix that first so the process fails cleanly instead of being killed.