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:
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:
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:
# Intended: evict at 100MB
# Actual: evicts at 100 entries (not bytes)Common Causes
- Wrong unit in maxSize:
maxSize="100"withoutunitdefaults to entries, not bytes - Missing sizing agent: Ehcache requires a
SizeOfEngineto measure object sizes for byte-based eviction - Off-heap not bounded:
offheapstorage configured withoutmaxSizegrows 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:
heapandoffheaphave 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:
$ 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)oroffheap(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