Introduction
Ehcache 3 stores data in heap and optionally off-heap memory, but without proper size limits, the cache grows until it causes OutOfMemoryError. The default cache configuration may have no entry limit or an expiry time that is too long, causing stale data to accumulate. Eviction policies (LRU, LFU) determine which entries are removed when the cache is full, and incorrect eviction configuration means the least useful entries are kept while frequently accessed ones are discarded. Proper cache sizing requires understanding the trade-off between hit rate and memory usage.
Symptoms
java.lang.OutOfMemoryError: Java heap space
at org.ehcache.core.Ehcache.put(Ehcache.java:345)Or:
org.ehcache.spi.memory.OutOfOffHeapMemoryException: Offheap store is full
at org.ehcache.impl.internal.store.offheap.OffHeapStore.putOr degraded performance:
# Cache hit rate drops from 95% to 30%
# Because cache is evicting entries too aggressivelyCommon Causes
- No max entries configured: Cache grows without bound
- Heap size too large: Cache consumes all available heap memory
- Expiry not set: Entries never expire, consuming memory
- Eviction policy wrong: LRU evicts frequently used entries
- Off-heap not configured: All data on heap, competing with application
- Cache not monitored: No visibility into hit rate or size
Step-by-Step Fix
Step 1: Configure cache with proper limits
```java import org.ehcache.Cache; import org.ehcache.CacheManager; import org.ehcache.config.builders.*; import org.ehcache.expiry.Duration; import org.ehcache.expiry.Expirations; import java.util.concurrent.TimeUnit;
public class CacheConfig {
public Cache<String, Object> createCache(CacheManager cacheManager) { return cacheManager.createCache("productCache", CacheConfigurationBuilder.newCacheConfigurationBuilder( String.class, Object.class, ResourcePoolsBuilder.newResourcePoolsBuilder() .heap(10000, EntryUnit.ENTRIES) // 10K entries on heap .offheap(500, MemoryUnit.MB) // 500MB off-heap ) .withExpiry(Expirations.timeToLiveExpiration( Duration.of(30, TimeUnit.MINUTES))) // TTL: 30 minutes .withEvictionAdvisor((key, value) -> { // Custom eviction logic return false; // Let LRU handle eviction }) .build() ); } } ```
Step 2: Spring Boot Ehcache configuration
```xml <!-- ehcache.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="productCache"> <key-type>java.lang.String</key-type> <value-type>com.example.Product</value-type>
<resources> <heap unit="entries">10000</heap> <offheap unit="MB">500</offheap> </resources>
<expiry> <ttl unit="minutes">30</ttl> </expiry>
<eviction-advisor>org.ehcache.config.EvictionAdvisor</eviction-advisor> </cache>
<cache alias="sessionCache"> <key-type>java.lang.String</key-type> <value-type>com.example.Session</value-type> <resources> <heap unit="entries">5000</heap> </resources> <expiry> <ttl unit="minutes">60</ttl> </expiry> </cache> </config> ```
Step 3: Monitor cache performance
```java import org.ehcache.statistics.CacheStatistics;
@Component public class CacheMonitor {
private final Cache<String, Object> cache;
public CacheMonitor(CacheManager cacheManager) { this.cache = cacheManager.getCache("productCache"); }
@Scheduled(fixedRate = 60000) public void logCacheStats() { CacheStatistics stats = cache.getRuntimeConfiguration() .getStatisticsService();
long hits = stats.getCacheHits(); long misses = stats.getCacheMisses(); long size = stats.getMappings(); double hitRate = hits + misses > 0 ? (double) hits / (hits + misses) * 100 : 0;
log.info("Cache stats - Size: {}, Hits: {}, Misses: {}, Hit Rate: {:.1f}%", size, hits, misses, hitRate); } } ```
Prevention
- Always set heap and off-heap limits based on available memory
- Configure TTL (time-to-live) to expire stale entries
- Use off-heap storage for large caches to avoid GC pressure
- Monitor cache hit rates and adjust sizing accordingly
- Add JMX or Micrometer metrics for cache statistics
- Test cache behavior with production data volume in staging
- Use separate caches for different data types with different sizing needs