Skip to content

Memory Leak in Cache.computeIfAbsent When Lambda Throws Exception #7975

@piyush-harness

Description

@piyush-harness

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) ...

Image

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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions