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的数据删除机制主要分为两种类型:被动删除和主动删除,它们各自有不同的触发条件和执行方式。
- 被动删除:
- 基于大小的删除:当缓存中的元素数量达到预设的最大容量时,会按照LRU(最近最少使用)算法自动移除最久未使用的条目
- 基于时间的删除:
- 访问过期(expireAfterAccess):条目在指定时间内未被访问就会被自动移除
- 写入过期(expireAfterWrite):条目在创建或更新后超过指定时间就会被自动移除
- 基于引用的删除:当使用弱引用(WeakReference)或软引用(SoftReference)包装值时,在内存不足时会被垃圾回收器自动回收
- 主动删除:
- 显式调用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(弱引用回收)或覆盖写入触发,监听时机理解有偏差 | 打印 RemovalNotification 的 cause,区分 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案例 详解
🔗 大数据模块直达链接