Introduction

Ehcache eviction by memory threshold is designed to remove the least-recently-used entries when the cache exceeds a configured size limit. When eviction does not trigger as expected, the cache grows unbounded, consuming heap memory until the application hits OutOfMemoryError. This commonly happens when the eviction configuration uses the wrong unit (entries vs bytes), when the cache manager is not properly configured with a sizing agent, or when off-heap storage is not bounded. The result is a slow memory leak that only manifests under production load.

Symptoms

Heap usage grows until OOM:

bash
java.lang.OutOfMemoryError: Java heap space
    at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1011)
    at org.ehcache.core.Ehcache.put(Ehcache.java:291)

Cache statistics show no evictions despite large size:

java
CacheRuntimeStatistics stats = cache.getRuntimeStatistics();
System.out.println("Mappings: " + stats.getMappings());
System.out.println("Evictions: " + stats.getEvictions());
// Mappings: 2500000, Evictions: 0  <-- Should have evicted!

Or eviction happens but at the wrong threshold:

yaml
# Intended: evict at 100MB
# Actual: evicts at 100 entries (not bytes)

Common Causes

  • Wrong unit in maxSize: maxSize="100" without unit defaults to entries, not bytes
  • Missing sizing agent: Ehcache requires a SizeOfEngine to measure object sizes for byte-based eviction
  • Off-heap not bounded: offheap storage configured without maxSize grows indefinitely
  • Cache manager not closed: Resources not released, causing memory accumulation across restarts
  • Statistics disabled: Cannot monitor eviction behavior without statistics enabled
  • Heap store vs off-heap confusion: heap and offheap have independent size limits

Step-by-Step Fix

Step 1: Configure eviction with correct unit

```java @Configuration @EnableCaching public class CacheConfig {

@Bean public CacheManager cacheManager() { ResourcePoolsBuilder pools = ResourcePoolsBuilder.newResourcePools() .heap(500, EntryUnit.ENTRIES) // 500 entries in heap .offheap(100, MemoryUnit.MB); // 100MB off-heap

CacheConfigurationBuilder<String, Object> config = CacheConfigurationBuilder.newCacheConfigurationBuilder( String.class, Object.class, pools) .withExpiry(Expirations.timeToLiveExpiration(Duration.ofMinutes(30))) .withEvictionAdvisor((key, value) -> true);

return JCacheCacheManager(CacheManagerBuilder.newCacheManagerBuilder() .withCache("myCache", config) .build(true)); } } ```

Step 2: Use XML configuration for clarity

```xml <config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns='http://www.ehcache.org/v3' xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">

<cache alias="myCache"> <key-type>java.lang.String</key-type> <value-type>java.lang.Object</value-type>

<resources> <heap unit="entries">500</heap> <offheap unit="MB">100</offheap> </resources>

<expiry> <ttl unit="minutes">30</ttl> </expiry> </cache> </config> ```

Step 3: Enable and monitor statistics

```yaml spring: cache: jcache: config: classpath:ehcache.xml type: jcache

management: endpoints: web: exposure: include: health,info,caches ```

Monitor via actuator:

bash
$ curl http://localhost:8080/actuator/caches/myCache
{
  "cache": "myCache",
  "cacheManager": "cacheManager",
  "statistics": {
    "getHits": 1523,
    "getMisses": 47,
    "puts": 1570,
    "removals": 0,
    "evictions": 1070   <-- Should increase when heap is full
  }
}

Prevention

  • Always specify the unit explicitly: heap(500, EntryUnit.ENTRIES) or offheap(100, MemoryUnit.MB)
  • Enable cache statistics in all environments to monitor eviction behavior
  • Set heap limits conservatively -- entries count is easier to predict than bytes
  • Use off-heap storage for large cached objects that would pressure the GC
  • Add a health indicator that alerts when cache utilization exceeds 80%
  • Test cache behavior with realistic data volumes in staging before production deployment