Java-185 Guava Cache 实战:删除策略、过期机制与常见坑全梳理

TL;DR

  • 场景:在 Java 项目里用 Guava Cache 做本地缓存,想搞清楚过期与删除的真实行为。
  • 结论:Guava 采用"懒清理 + LRU+FIFO"策略,被动删除和主动删除需要配合使用。
  • 产出:一套基于 33.4.8-jre 的完整示例代码、删除机制说明与常见问题排查清单。

版本矩阵

Guava 版本 已验证说明
33.4.8-jre ✅文中 POM 与所有示例代码实际跑通,行为与文中描述一致
32.x-jre 系列 ⚠️未在文中实测,按官方兼容性预期行为一致,建议自行回归测试
31.x 及以下 jre ⚠️API 大体相同,但可能存在细节差异或弃用接口,需结合自身版本验证

Guava Cache

POM

xml 复制代码
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.4.8-jre</version>
</dependency>

测试代码

java 复制代码
package icu.wzk;

import com.google.common.cache.*;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class GuavaDemo01 {

    public static void main(String[] args) throws Exception {
        LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
                .maximumSize(3)
                .expireAfterWrite(3, TimeUnit.SECONDS)
                .recordStats()
                .removalListener((RemovalListener<String, Object>)
                        notification -> System.out.println(notification.getKey() + ": " + notification.getCause())
                )
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return Constants.CACHE.get(key);
                    }
                });

        // 初始化CACHE数据
        for (int i = 1; i <= 5; i ++) {
            Constants.CACHE.put(String.valueOf(i), i);
        }
        // 初始化 Guava Cache
        for (int i = 1; i <= 3; i++) {
            cache.get(String.valueOf(i));
        }
        // 打印所有的数据
        for (Map.Entry<String, Object> stringObjectEntry : cache.asMap().entrySet()) {
            System.out.println(stringObjectEntry.toString());
        }
        // 延迟
        Thread.sleep(1000);
        System.out.println("Sleep 1000: " + cache.size());

        Thread.sleep(1000);
        System.out.println("Sleep 1000: " + cache.size());

        Thread.sleep(1000);
        System.out.println("Sleep 1000: " + cache.size());

        // 发现竟然没有过期?因为是懒清除,需要调用函数
        cache.cleanUp();
        System.out.println("CleanUp: " + cache.size());
    }
}

class Constants {
    public static final Map<String, Object> CACHE = new HashMap<>();
}

执行结果如下所示:

数据删除

Guava Cache的数据删除机制主要分为两种类型:被动删除和主动删除,它们各自有不同的触发条件和执行方式。

  1. 被动删除:
  • 基于大小的删除:当缓存中的元素数量达到预设的最大容量时,会按照LRU(最近最少使用)算法自动移除最久未使用的条目
  • 基于时间的删除:
    • 访问过期(expireAfterAccess):条目在指定时间内未被访问就会被自动移除
    • 写入过期(expireAfterWrite):条目在创建或更新后超过指定时间就会被自动移除
  • 基于引用的删除:当使用弱引用(WeakReference)或软引用(SoftReference)包装值时,在内存不足时会被垃圾回收器自动回收
  1. 主动删除:
  • 显式调用invalidate方法删除单个键
  • 调用invalidateAll方法批量删除所有键或指定键集合
  • 通过CacheBuilder的removalListener可以监听删除事件,在条目被删除时执行自定义逻辑
  • 定期维护时调用cleanUp方法主动清理过期条目

应用场景示例:

  • 电商平台商品缓存:使用expireAfterWrite确保价格信息定期更新
  • 用户会话管理:使用expireAfterAccess自动清理长时间不活跃的用户会话
  • 热点数据缓存:使用基于大小的删除防止缓存占用过多内存

被动删除

基于数据大小

规则:LRU+FIFO,访问次数一样少的情况下,FIFO。

java 复制代码
package icu.wzk;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.Map;

public class GuavaDemo03 {

    public static void main(String[] args) throws Exception {
        // 默认是 LRU+FIFO
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(3)
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        return "get: " + key;
                    }
                });
        cache.put("1", "1");
        cache.put("2", "2");
        cache.put("3", "3");
        // 按照 LRU+FIFO 原则,会淘汰1
        cache.put("4", "4");
        // 打印所有的数据
        for (Map.Entry<String, String> stringObjectEntry : cache.asMap().entrySet()) {
            System.out.println(stringObjectEntry.toString());
        }

        System.out.println("---");
        // 使用一次
        cache.get("2");
        // 按照 LRU+FIFO 原则,会淘汰3
        cache.put("5", "5");
        // 打印所有的数据
        for (Map.Entry<String, String> stringObjectEntry : cache.asMap().entrySet()) {
            System.out.println(stringObjectEntry.toString());
        }
    }

}

执行结果如下所示:

基于过期时间

间隔多长时间没有访问过的key被删除

java 复制代码
package icu.wzk;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.Map;
import java.util.concurrent.TimeUnit;

public class GuavaDemo04 {

    public static void main(String[] args) throws Exception {
        // 默认是 LRU+FIFO
        // 加入时间的淘汰原则
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(3)
                // 3秒内没有访问 就删除
                .expireAfterAccess(3, TimeUnit.SECONDS)
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        return "get: " + key;
                    }
                });
        cache.put("1", "1");
        cache.put("2", "2");
        cache.put("3", "3");

        // 打印所有的数据
        for (Map.Entry<String, String> stringObjectEntry : cache.asMap().entrySet()) {
            System.out.println(stringObjectEntry.toString());
        }

        // 延迟3秒
        Thread.sleep(3000);

        // 打印所有的数据
        for (Map.Entry<String, String> stringObjectEntry : cache.asMap().entrySet()) {
            System.out.println(stringObjectEntry.toString());
        }
    }

}

执行结果如下所示:

基于引用

可以通过 weakKeys 和 weakValues 方法指定 Cache 只保存对缓存记录 Key 和 Value 的弱引用,这样当没有其他强引入指向 Key 和 Value 的时候,Key 和 Value 对象就会被垃圾回收器回收。

java 复制代码
package icu.wzk;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.util.Map;


public class GuavaDemo05 {

    public static void main(String[] args) throws Exception {
        LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
                .maximumSize(3)
                // weak value
                .weakValues()
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public String load(String key) throws Exception {
                        return "get: " + key;
                    }
                });
        // 创建一个对象
        Object value = new Object();
        cache.put("1", value);
        // 和原来对象没有强引用了
        value = new Object();

        // 触发一下GC
        System.gc();
        // 打印所有的数据
        for (Map.Entry<String, Object> stringObjectEntry : cache.asMap().entrySet()) {
            System.out.println(stringObjectEntry.toString());
        }
    }
}

执行结果如下所示:

主动删除

单独删除

java 复制代码
cache.invalidate("1");

批量删除

java 复制代码
cache.invalidateAll(Arrays.asList("1", "2", "3"));

清空所有数据

java 复制代码
cache.invalidateAll();

错误速查

症状 根因定位 修复
超过 expireAfterWrite/expireAfterAccess 时间后,cache.size() 仍未下降 Guava 采用惰性删除,未触发访问/维护逻辑,或未调用 cleanUp 打日志观察访问路径,查看是否有 get/put 或定期维护线程;在定时任务或关键路径补充 cache.cleanUp(),或通过访问触发回收
maximumSize 已达上限,但看上去"没按预期淘汰" LRU+FIFO 策略与预期不一致,访问顺序改变了淘汰对象 打印 cache.asMap(),结合访问顺序重现,确认 1/2/3/4/5 的访问轨迹;根据真实访问场景调试用例,不要仅依赖"想象中的 LRU 次序"
JVM 内存占用持续升高,怀疑是 Guava Cache 内存泄漏 未限制 maximumSize 或未使用基于时间/引用的过期策略 使用 profiler 查看堆中缓存对象数量,排除其它大对象;设置合理的 maximumSize + 过期策略,必要时使用 weakValues/weakKeys
RemovalListener 未按预期频繁触发删除 多由 GC(弱引用回收)或覆盖写入触发,监听时机理解有偏差 打印 RemovalNotificationcause,区分 EXPIRED / SIZE / COLLECTED;调整监听逻辑,只在需要的 cause 类型上做业务操作
get() 调用偶发很慢,甚至出现局部雪崩 CacheLoader 执行耗时长或内部又访问远程服务,多个 miss 串联放大 统计 load() 耗时,关注高并发 miss 场景;限制 CacheLoader 内部逻辑(避免重 IO),必要时加降级或并发控制
通过 weakValues 配置后,缓存命中率异常偏低 Value 只保留弱引用,被 GC 过早回收,导致频繁重新加载 对比开启/关闭 weakValues 前后的 stats,查看 hit/miss 变化;仅在确实需要弱引用回收时使用 weakValues,否则改用 size/时间过期

其他系列

🚀 AI篇持续更新中(长期更新)

AI炼丹日志-29 - 字节跳动 DeerFlow 深度研究框斜体样式架 私有部署 测试上手 架构研究 ,持续打造实用AI工具指南!
AI研究-132 Java 生态前沿 2025:Spring、Quarkus、GraalVM、CRaC 与云原生落地
🔗 AI模块直达链接

💻 Java篇持续更新中(长期更新)

Java-180 Java 接入 FastDFS:自编译客户端与 Maven/Spring Boot 实战

MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务已完结,Dubbo已完结,MySQL已完结,MongoDB已完结,Neo4j已完结,FastDFS 已完结,OSS正在更新... 深入浅出助你打牢基础!
🔗 Java模块直达链接

📊 大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!
大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解
🔗 大数据模块直达链接

相关推荐
阿杰同学3 小时前
Java 网络协议面试题答案整理,最新面试题
java·开发语言·网络协议
CoderYanger3 小时前
动态规划算法-两个数组的dp(含字符串数组):41.最长公共子序列(模板)
java·算法·leetcode·动态规划·1024程序员节
爬山算法3 小时前
Redis(171)如何使用Redis实现分布式事务?
redis·分布式·junit
凌波粒3 小时前
Springboot基础教程(8)--Shiro
java·spring boot·后端
dzl843943 小时前
springboot脚手架备忘
java
feathered-feathered3 小时前
网络套接字——Socket网络编程(TCP编程详解)
java·网络·后端·网络协议·tcp/ip
路边草随风5 小时前
java实现发布spark yarn作业
java·spark·yarn
为爱停留6 小时前
Spring AI实现MCP(Model Context Protocol)详解与实践
java·人工智能·spring
汝生淮南吾在北8 小时前
SpringBoot+Vue饭店点餐管理系统
java·vue.js·spring boot·毕业设计·毕设