Guava Cache: A High-Performance JVM-Level In-Memory Caching Library

Guava Cache is a robust, thread-safe in-memory caching library provided by Google's Guava toolkit. Built atop principles similar to ConcurrentHashMap, it extends core map functionality with rich cache-specific features—such as expiration, size constraints, automatic loading, and fine-grained concurrency control—while remaining lightweight and zero-dependency.

Configurable Expiration Policies

Guava Cache supports two distinct time-based eviction strategies:

  • Write-based expiration: Entries expire after a fixed duration from insertion, regardless of access frequency.
  • Access-based expiration: Entries expire only after a period of inactivity, resetting the timer on every read or write.

Both are configured at build time:

// Expires 30 minutes after insertion
Cache<String, UserProfile> writeExpiringCache = CacheBuilder.newBuilder()
    .expireAfterWrite(30, TimeUnit.MINUTES)
    .build();

// Expires 30 minutes after last access
Cache<String, UserProfile> accessExpiringCache = CacheBuilder.newBuilder()
    .expireAfterAccess(30, TimeUnit.MINUTES)
    .build();

Capacity Management with Flexible Sizing

To prevent unbounded memory growth, Guava Cache allows capacity limits based on either entry count or custom weight:

  • Fixed entry count:

    Cache<String, UserProfile> boundedByCount = CacheBuilder.newBuilder()
        .maximumSize(5_000)
        .build();
    
  • Weighted capacity, where each value contributes dynamically to the total budget:

    Cache<String, UserProfile> boundedByWeight = CacheBuilder.newBuilder()
        .maximumWeight(10_000_000) // ~10 MB
        .weigher((key, profile) -> (int) Math.ceil(
            calculateSerializedSize(profile) / 1024.0))
        .build();
    

Weighting enables precise memory pressure control—especially valuable when cached objects vary significantly in memory footprint.

Eviction Triggers: Passive and Explicit

Eviction occurs through multiple mechanisms:

  • Passive triggers: expiration, size limitss, or weak/soft references.
  • Explicit removal: via invalidate(key), invalidateAll(keys), or invalidateAll().

Weak reference support includes:

  • weakKeys(): keys are garbage-collected when no strong references remain.
  • weakValues() or softValues(): values are reclaimed under memory pressure (soft values follow LRU-like global ordering).

Note: Weak/soft references bypass equals() comparison—equality is determined by identity (==).

Automatic Loading and Load Coordination

Guava Cache implements loading cache semantics: missing entries are loaded on-demand using user-provided logic. Two primary approaches exist:

Callable-Based Loading

User user = cache.get("u123", () -> {
    log.info("Loading user u123 from DB...");
    return database.fetchUser("u123");
});

CacheLoader-Based Loading (Preferred for Consistency)

LoadingCache<String, User> loadingCache = CacheBuilder.newBuilder()
    .build(new CacheLoader<String, User>() {
        @Override
        public User load(String key) throws Exception {
            return database.fetchUser(key);
        }
    });

User user = loadingCache.get("u123"); // Auto-loads if absent

When both Callable and CacheLoader are available (e.g., in LoadingCache.get(key, callable)), the Callable takes precedence.

Concurrent Load Protection

Under high concurrency, Guava avoids thundering herd by ensuring only one thread loads a missing key; others block until the result is ready—or receive stale values during refresh (see below).

Refresh vs. Expire: Distinct Lifecycle Behaviors

  • expireAfterWrite() and expireAfterAccess() define lifetimes: expired entries are eventually evicted and no longer serve reads unless reloaded.
  • refreshAfterWrite() defines staleness tolerance: entries remain valid but trigger background reloads after the interval—readers continue receiving the old value until the new one arrives.

Key distinctions:

Behavior expire refresh
Read blocking Yes (all threads wait for load) No (stale value returned immediately)
Memory retension May be purged on cleanup Always retained until explicit eviction
Consistency Strong (all see latest post-load) Eventual (mix of old/new during transition)

Use expire when correctness outweighs latency; use refresh when responsiveness matters most—and combine both when data must age out and stay fresh within its lifetime.

Segment-Based Concurrency Model

Like ConcurrentHashMap, Guava Cache shards internal state into segments to reduce lock contention. The concurrencyLevel parameter guides segment count estimation (power-of-two, capped at 65,536). Actual concurrency depends on hash distribution—uneven key patterns may concentrate load in few segments.

For optimal throughput, set concurrencyLevel near 2 × CPU cores.

Read operations avoid locking entirely unless cleanup is triggered. Cleanup itself uses non-blocking tryLock() to minimize reader impact:

void postReadCleanup() {
    if ((readCount.incrementAndGet() & 0x3F) == 0) { // Every 64th read
        cleanUp(); // tryLock() inside
    }
}

Built-in Monitoring and Diagnostics

Enable statistics collection with .recordStats():

LoadingCache<String, User> cache = CacheBuilder.newBuilder()
    .recordStats()
    .build(loader);

// Later...
CacheStats stats = cache.stats();
System.out.println("Hit rate: " + stats.hitRate());
// Output: Hit rate: 0.25

Key metrics include hitCount, missCount, loadSuccessCount, loadExceptionCount, totalLoadTime, and evictionCount.

Practical Usage Guidelines

  • Best for: Read-heavy workloads with modest update frequency, low consistency requirements, or ultra-low-latency needs.
  • Avoid for: Distributed environments requiring strong cache coherence—local caches cannot synchronize across nodes.
  • Prefer over raw maps: When you need TTL, size bounds, loading, or stats—CacheBuilder.newBuilder().build() replaces ConcurrentHashMap with minimal overhead.

Dependency and Initialization

Add Maven dependency:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.2.1-jre</version>
</dependency>

Construct a production-ready cache:

LoadingCache<String, User> userCache = CacheBuilder.newBuilder()
    .initialCapacity(512)
    .maximumSize(10_000)
    .expireAfterWrite(20, TimeUnit.MINUTES)
    .refreshAfterWrite(2, TimeUnit.MINUTES)
    .concurrencyLevel(16)
    .recordStats()
    .build(new CacheLoader<String, User>() {
        public User load(String id) throws Exception {
            return fetchFromDataSource(id);
        }
    });

Core API Summary

Method Description
get(key, callable) Load-on-miss with fallback logic
getIfPresent(key) Non-loading loookup (returns null if absent)
getAllPresent(keys) Bulk non-loading lookup
put(key, value) Insert or replace
invalidate(key) Remove single entry
invalidateAll() Clear all entries
stats() Retrieve usage metrics
asMap() Expose underlying ConcurrentMap view
cleanUp() Force immediate cleanup of expired entries

Encapsulate cache access behind interfaces or Spring @Cacheable abstractions to decouple business logic from implementation details.

Tags: guava Caching java Concurrency Performance

Posted on Tue, 12 May 2026 13:51:32 +0000 by waterox