Question
Java GC Overhead Limit Exceeded: Causes and Fixes
Question
While running JUnit tests, Java throws the following error:
java.lang.OutOfMemoryError: GC overhead limit exceeded
I understand that an OutOfMemoryError means the application has run out of memory, but what does GC overhead limit exceeded specifically mean? How can I diagnose and fix it?
Short Answer
You will learn how Java garbage collection works, why the JVM throws GC overhead limit exceeded, how it differs from a regular heap-space error, and how to investigate the underlying memory problem—especially in JUnit test runs.
Concept
Java stores most objects in an area of memory called the heap. The garbage collector (GC) periodically finds objects that are no longer reachable and reclaims their heap space.
java.lang.OutOfMemoryError: GC overhead limit exceeded means the JVM is spending almost all of its time running garbage collection but is recovering very little memory. Rather than letting the application appear frozen while GC repeatedly runs, the JVM stops it with this error.
A common JVM threshold is:
- More than about 98% of elapsed time is spent in garbage collection.
- Less than about 2% of the heap is recovered in each collection cycle.
The exact behavior can vary by JVM version and garbage collector, but the important meaning is the same: the program needs memory faster than the collector can free it.
This is usually a symptom, not the root cause. Typical causes include:
- A collection, cache, or map grows without a limit.
- A loop keeps creating objects.
- Objects are unintentionally retained by references, listeners, static fields, or test fixtures.
- Tests load very large datasets or create too many objects at once.
- The heap is genuinely too small for a valid workload.
Mental Model
Imagine the heap as a storage room and the garbage collector as a cleaner.
Normally, the cleaner occasionally removes discarded boxes, leaving room for new ones. With this error, the storage room is nearly full, and the cleaner is working almost constantly—but nearly every box is still needed or still referenced. The cleaner removes only a few small items, then must start cleaning again immediately.
The JVM eventually concludes that cleaning cannot solve the problem and reports GC overhead limit exceeded. The useful question is not only “How do I get a larger room?” but also “Why are so many boxes being kept?”
Syntax and Examples
The error is usually observed in output rather than caused by a particular line of Java syntax:
java.lang.OutOfMemoryError: GC overhead limit exceeded
Here is an example that continually retains objects in a list:
import java.util.ArrayList;
import java.util.List;
public class MemoryGrowthExample {
public static void main(String[] args) {
List<byte[]> data = new ArrayList<>();
while (true) {
data.add(new byte[1024 * 1024]); // Keep another 1 MB array
}
}
}
data keeps references to every array added to it. Because those arrays remain reachable, the garbage collector cannot reclaim them. Eventually, the heap fills and Java throws an OutOfMemoryError, possibly with the GC-overhead message.
A bounded version avoids retaining unlimited data:
import java.util.ArrayDeque;
import java.util.Deque;
{
{
Deque<[]> recentData = <>();
( ; i < ; i++) {
recentData.addLast( []);
(recentData.size() > ) {
recentData.removeFirst();
}
}
}
}
Step by Step Execution
Consider this small example:
List<String> messages = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
messages.add("Message number " + i);
}
Step by step:
messagescreates anArrayListthat can hold references to strings.- Each loop iteration creates a new string such as
"Message number 42". messages.add(...)stores a reference to that string.- Because
messagesis still reachable, every stored string is also reachable. - As the list grows, the JVM needs more heap memory.
- The garbage collector runs when memory becomes tight, but it cannot remove the strings because the list still references them.
- If this continues until the heap is nearly full, the JVM may spend most of its time trying unsuccessfully to reclaim memory and throw
GC overhead limit exceeded.
If the list is only needed temporarily, process items in batches or remove entries after use instead of retaining all of them.
Real World Use Cases
This issue can appear in many real applications:
- JUnit tests: A parameterized test creates a large object graph for every case, or a
statictest fixture retains data between tests. - API services: An in-memory cache stores every response forever instead of using expiration or a maximum size.
- File processing: Code reads an entire large file into memory when it could stream lines or records.
- Data imports: A batch job accumulates every parsed database row before writing any results.
- Event systems: Listeners are registered but never removed, so old screens, sessions, or objects remain referenced.
- Logging and diagnostics: Code builds huge strings, stack traces, or debug collections in a loop.
The correct solution depends on whether the retained objects are unnecessary (a leak or design issue) or necessary (a workload that needs more memory).
Real Codebase Usage
In production code, developers prevent this problem by controlling object lifetime and limiting memory growth.
Validate input before allocating large structures
void importRecords(List<String> records) {
if (records.size() > 100_000) {
throw new IllegalArgumentException("Too many records in one import");
}
// Safe to continue with a known limit.
}
Process data as a stream instead of keeping all results
try (var lines = java.nio.file.Files.lines(java.nio.file.Path.of("input.txt"))) {
lines.forEach(line -> processLine(line));
}
This avoids calling Files.readAllLines(...) for a very large file when full in-memory storage is unnecessary.
Use bounded caches
A cache should usually have a maximum size, expiration policy, or both. Unbounded maps are a common memory-growth source.
Keep tests isolated
JUnit tests should avoid mutable static collections unless they are cleared deliberately. Tests that create costly resources should close them and release references when finished.
@org.junit.jupiter.api.AfterEach
{
sharedResults.clear();
}
Common Mistakes
Treating the error message as the root cause
Increasing the heap may delay the failure, but it does not fix an unbounded collection or object leak.
// Problem: this map grows for the lifetime of the application.
private static final Map<String, byte[]> cache = new HashMap<>();
Use a bounded cache, remove entries when they are no longer useful, or store data externally.
Calling System.gc() to fix memory
System.gc(); // Usually not a solution
Garbage collection can reclaim only unreachable objects. If a list, map, static field, or listener still references an object, calling GC will not free it.
Disabling the overhead-limit check
The JVM can be configured with -XX:-UseGCOverheadLimit in some JVMs. This only suppresses this particular early-failure condition. The application can still run out of memory and may spend a long time in repeated GC pauses. Do not use it as a normal fix.
Assuming every JUnit failure is a production memory leak
A test may allocate unusually large fixtures, run too many cases in one JVM, or accidentally keep shared test data. Investigate the test setup and the application code separately.
Forgetting resource cleanup
Memory is not the only resource. Always close streams, database connections, and files using try-with-resources. Although unclosed native resources do not always cause this exact error, they can contribute to unstable test runs.
Comparisons
| Situation | Meaning | Typical response |
|---|---|---|
OutOfMemoryError: Java heap space | Java could not allocate more heap memory. | Check retained objects, allocation volume, and heap size. |
OutOfMemoryError: GC overhead limit exceeded | GC is consuming nearly all execution time while freeing very little memory. | Investigate memory retention first; then consider heap sizing. |
OutOfMemoryError: Metaspace | Class metadata storage is exhausted, often due to excessive class loading. | Investigate class loaders, generated classes, or metaspace configuration. |
StackOverflowError | Too many nested method calls consumed a thread stack. | Find unintended/infinite recursion; this is not a heap-GC issue. |
| Approach | When it helps |
|---|
Cheat Sheet
-
Meaning: The JVM is spending almost all of its time in garbage collection and recovering very little heap memory.
-
Usually indicates: An object-retention problem, unbounded data growth, excessive allocation, or an undersized heap.
-
First checks: Look for growing
List,Map, cache, queue, static field, listener list, or test fixture. -
Useful run options:
-Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heap-dumps -
Good fixes: Bound caches, process in batches, stream files, remove stale references, close resources, reduce test fixture size.
-
Not a real fix: Repeated
System.gc(), blindly increasing heap size, or disablingUseGCOverheadLimit. -
JUnit clue: Check
staticstate, parameterized-test data,@BeforeAllfixtures, and resources that survive multiple tests.
FAQ
What does GC overhead limit exceeded mean in Java?
It means the JVM is spending nearly all available time running garbage collection but recovering too little memory to continue efficiently. It is a form of OutOfMemoryError.
Is this the same as Java heap space?
Both indicate heap-memory pressure. GC overhead limit exceeded specifically says the JVM detected repeated, mostly ineffective garbage collection before or while memory is exhausted.
Will increasing -Xmx solve the error?
It can solve the issue when the program legitimately needs more memory and enough system RAM is available. It will only postpone failure if the program retains data without a limit.
How do I find the object causing the memory problem?
Enable heap dumps with -XX:+HeapDumpOnOutOfMemoryError, reproduce the error, and inspect the dump using a memory-analysis tool. Look for the largest retained object groups and the references keeping them alive.
Why does this happen only during JUnit tests?
Tests can create large fixtures, run many cases in one JVM, retain static data between test methods, or fail to close resources. The test runner may also use a smaller heap configuration than your normal application run.
Should I call System.gc() in my code?
Usually no. It cannot reclaim objects that are still referenced, and it can hurt performance. Fix the references or memory-growth behavior instead.
Can I disable the GC overhead limit?
Mini Project
Description
Build a small batch processor that handles a large sequence of records without retaining every processed result. This demonstrates a practical way to avoid memory growth in import jobs and test data generation.
Goal
Process records in fixed-size batches while keeping only the current batch in memory.
Requirements
Create 10,000 sample record strings. Process records in batches of 500. Print the number of records in each processed batch. Clear each batch after it is processed. Do not store all processed batches in another collection.
Keep learning
Related questions
Add External JAR Files to an IntelliJ IDEA Java Project
Learn how to add external JAR dependencies to an IntelliJ IDEA Java project using module libraries, and when to use Maven or Gradle instead.
Avoiding Java Code in JSP with JSP 2: EL and JSTL Explained
Learn how to avoid Java scriptlets in JSP 2 using Expression Language and JSTL, with examples, best practices, and common mistakes.
Call a Method After a Delay in Android Java
Learn how to run Java code after a delay in Android using Handler.postDelayed, manage the main thread, and cancel callbacks safely.