-
Notifications
You must be signed in to change notification settings - Fork 11.1k
Description
Guava Version
33.4.8-jre
Description
Hi All,
When using Cache.asMap().computeIfAbsent with a lambda that throws an exception, Guava’s LocalCache retains a reference to the failed computation indefinitely. This results in memory leaks that ultimately cause the JVM to run out of heap space.
The issue appears to be that the temporary ComputingValueReference created during computeIfAbsent is not released when the computation fails. This ComputingValueReference retains a SettableFuture, which holds the thrown RuntimeException. Because stack traces are large objects, repeated failures quickly exhaust the heap.
Steps to Reproduce:
Run the following program with -Xmx128m (or similarly small heap) to trigger the leak quickly:
PS: We did implement the exception handling ourselves in loader function to avoid this issue.
Example
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class GuavaLeakReproducer {
public static void main(String[] args) {
System.out.println("Starting reproducer for OOM with a size-bounded cache and large exceptions...");
System.out.println("Run with -Xmx128m to trigger the OutOfMemoryError quickly.");
// We are now simulating the production cache with a fixed size.
final int MAX_CACHE_SIZE = 50000;
Cache<String, Boolean> leakingCache =
CacheBuilder.newBuilder()
.maximumSize(MAX_CACHE_SIZE)
.expireAfterAccess(4, TimeUnit.HOURS)
.build();
long counter = 0;
while (true) {
String uniqueKey = UUID.randomUUID().toString() + "-" + counter;
try {
leakingCache.asMap().computeIfAbsent(uniqueKey, cacheKey -> {
throw new RuntimeException("Failed to load value for key: " + cacheKey);
});
} catch (Exception ignored) {
// The heavy exception object is now stored in the cache.
}
counter++;
// Report progress and heap usage.
if (counter % 500 == 0) {
long heapUsed = (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024 / 1024;
long heapMax = Runtime.getRuntime().maxMemory()/1024/1024;
System.out.printf("Processed %,d keys. Used Heap: %,d MB , Max Heap: %,d MB. Cache will OOM before eviction can help.%n",
counter, heapUsed,heapMax);
}
// The OOM will likely happen before or right as the cache hits its max size.
if (counter >= MAX_CACHE_SIZE * 1.5) {
System.out.println("Cache did not OOM as expected. The heap may be too large for this test.");
break;
}
}
}
}
Expected Behavior
Cache should keep evicting elements based on maxSize and GC should keep collecting those objects.
Actual Behavior
Heap usage continuously grows.
OutOfMemoryError occurs before cache eviction can help.
Heap dump shows many retained ComputingValueReference objects holding SettableFuture instances with RuntimeException stack traces.
Processed 27,000 keys. Used Heap: 31 MB , Max Heap: 32 MB. java.lang.OutOfMemoryError: Java heap space at com.google.common.util.concurrent.AbstractFuture.setException(AbstractFuture.java:514) at com.google.common.util.concurrent.SettableFuture.setException(SettableFuture.java:54) at com.google.common.cache.LocalCache$LoadingValueReference.setException(LocalCache.java:3525) ...

Packages
No response
Platforms
No response
Checklist
-
I agree to follow the code of conduct.
-
I can reproduce the bug with the latest version of Guava available.