缓存可以显著降低数据库或磁盘访问压力,但在 Java 里如果只考虑命中率而忽略引用强度,很容易把一个性能优化手段变成内存泄漏源头。常规做法是用 HashMap 或 ConcurrentHashMap 保存计算结果、查询结果、图片对象等。这些容器对 key 和 value 持有强引用,意味着只要缓存项没有被显式删除,对象就始终和 GC Roots 之间存在可达路径。垃圾回收器无法判断业务是否还会继续使用某个缓存项,只能认为这些对象仍然存活,因此不会回收。当缓存数量持续增多或缓存对象体积较大时,老年代会被逐渐填满,最终触发 OutOfMemoryError。要设计内存友好的缓存,就需要引入弱引用机制,让缓存不成为对象存活的唯一原因。

一、为什么普通 Map 缓存会阻止垃圾回收
JVM 判断一个对象是否可以回收,主要依据可达性分析。垃圾回收器会从一组 GC Roots 出发,扫描所有能够直接或间接到达的对象。GC Roots 包括方法栈帧中的局部变量、类的静态字段、活跃线程等。HashMap 作为缓存容器,如果它本身被某个静态字段引用,那么它内部的 table 数组就是可达的。数组中的每个 Entry 又持有 key 和 value 的强引用。于是所有放进缓存的键值对象都可以从 GC Roots 到达,即使业务上已经不再使用,它们也不会被标记为垃圾。
下面的代码是一个典型的强引用缓存。它没有设置容量上限,也没有淘汰策略。程序运行过程中不断向里面写入大字节数组,即使之前写入的数据已经没有业务用途,堆内存仍然会持续增长。
import java.util.HashMap;
import java.util.Map;
public class StrongReferenceCache {
private final Map<String, byte[]> cache = new HashMap<>();
public void put(String key, byte[] data) {
cache.put(key, data);
}
public byte[] get(String key) {
return cache.get(key);
}
}
如果每个 byte 数组大小为 10MB,同时存在 20 个这样的缓存对象,就需要大约 200MB 堆空间。在 -Xmx128m 的启动参数下,应用可能在短时间内抛出 OutOfMemoryError。需要注意的是,这个错误不是因为某一行代码写错了,而是因为缓存没有自动回收机制。只要缓存本身还被引用,它就会阻止垃圾回收器释放内部对象。
Java 的引用类型可以分成强引用、软引用、弱引用和虚引用。强引用是默认行为,只要存在就从可达性分析中存活。软引用在系统即将发生内存溢出前回收,弱引用则在下一次垃圾回收时只要对象没有其他强引用就会被回收。对于缓存场景,弱引用通常比软引用更积极,因此更适合那些可重新计算、可重新加载的数据。
二、WeakReference 与 WeakHashMap 的回收机制
WeakReference 是 Java 提供的一种引用包装类。它本身不阻止被引用对象被垃圾回收。当目标对象除了弱引用之外没有其他强引用时,垃圾回收器会在某个 GC 周期中回收该对象,同时 WeakReference 的 get 方法开始返回 null。下面的示例创建了一个 10MB 的对象,并把它只交给弱引用持有,之后清空强引用并触发 GC。
import java.lang.ref.WeakReference;
public class WeakReferenceDemo {
static class BigObject {
byte[] data = new byte[1024 * 1024 * 10];
}
public static void main(String[] args) {
BigObject obj = new BigObject();
WeakReference<BigObject> ref = new WeakReference<>(obj);
System.out.println("GC前: " + ref.get());
obj = null;
System.gc();
System.out.println("GC后: " + ref.get());
}
}
在这个示例中,obj 置空后 BigObject 实例只有 WeakReference 引用它。执行 System.gc 之后,ref.get 可能返回 null。之所以说可能,是因为 System.gc 只是一个建议,JVM 不保证一定立即执行完整的垃圾回收。真实应用中不能依赖 System.gc,但这个实验足以说明弱引用的回收语义。
WeakHashMap 则把这种机制应用到了 Map 的 key 上。WeakHashMap 内部会将每个 key 包装成弱引用。当一个 key 不再被其他地方强引用时,垃圾回收器会回收这个 key,WeakHashMap 会在后续访问时清理对应的 entry。它的 value 虽然仍由 entry 强引用,但 entry 一旦被清理,value 的引用也会消失。需要注意 value 不能强引用 key,否则 key 就无法只靠弱引用存活,最终导致清理失效。
WeakHashMap 最常见的坑是字符串常量池。字符串字面量会被 JVM 放在字符串常量池中,而常量池本身持有这些字符串的强引用。如果使用字面量直接作为 key,例如 cache.put("user:1", value),那么即使业务不再使用这个 key,WeakHashMap 中的引用也不会被回收,因为常量池还持有它。要利用弱引用特性,通常应使用 new String("user:1") 或自定义 key 对象,避免常量池的强引用干扰。
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapDemo {
public static void main(String[] args) throws Exception {
Map<String, byte[]> cache = new WeakHashMap<>();
String key = new String("big-key");
cache.put(key, new byte[1024 * 1024 * 20]);
System.out.println("GC前缓存大小: " + cache.size());
key = null;
System.gc();
Thread.sleep(1000L);
System.out.println("GC后缓存大小: " + cache.size());
}
}
三、实现一个基于弱引用的自定义缓存
WeakHashMap 的弱引用作用在 key 上,但很多缓存场景希望 key 保持稳定,value 是可以被回收的部分。例如用户 ID 作为 key,用户详情对象作为 value。此时需要把弱引用放在 value 侧,同时保证并发场景下的读写安全。Java 并发包中的 ConcurrentHashMap 搭配 WeakReference 就可以实现这样一个轻量级缓存。
下面是一个可复用的 WeakCache 实现。它的核心思路是:缓存中存储 WeakReference 而不是原始对象;获取时先检查引用是否仍然有效,如果已经被回收则删除旧项并重新加载;通过 putIfAbsent 和 replace 避免多线程重复加载同一个 key。
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
public class WeakCache<K, V> {
private final ConcurrentHashMap<K, WeakReference<V>> cache = new ConcurrentHashMap<>();
public V get(K key, ObjectLoader<K, V> loader) {
WeakReference<V> ref = cache.get(key);
if (ref != null) {
V value = ref.get();
if (value != null) {
return value;
}
cache.remove(key, ref);
}
V loaded = loader.load(key);
WeakReference<V> newRef = new WeakReference<>(loaded);
WeakReference<V> oldRef = cache.putIfAbsent(key, newRef);
if (oldRef != null) {
V oldValue = oldRef.get();
if (oldValue != null) {
return oldValue;
}
cache.replace(key, oldRef, newRef);
}
return loaded;
}
public void clear() {
cache.clear();
}
public interface ObjectLoader<K, V> {
V load(K key);
}
}
代码中 get 方法接收一个 loader,用于缓存未命中或弱引用失效时重新生成对象。取出 WeakReference 后先调用 get,如果得到 null 说明原对象已经被回收,此时 cache.remove(key, ref) 可以清理已经失效的 entry。加载新对象后使用 putIfAbsent 写入,如果其他线程已经抢先写入则优先返回已有值。replace 操作进一步校验旧引用是否仍然有效,避免把其他线程刚放入的新值覆盖掉。这个实现没有使用锁,依靠 ConcurrentHashMap 的原子操作保证基本并发安全,适合读多写多的缓存场景。
需要理解,弱引用缓存保存的是引用而不是对象副本。当调用方通过 get 获得返回值后,调用方局部变量会形成强引用,因此使用期间对象不会被回收。如果调用方只是短暂使用,用完后应尽快让引用离开作用域,这样下一次 GC 时对象才有机会被释放。缓存中出现的 null 并不表示数据不存在,只表示弱引用已经被回收,需要重新加载。
四、弱引用缓存的适用边界与避坑建议
弱引用缓存最适合缓存体积较大、可以重新构建或重新加载的对象。典型场景包括图片缩略图、从磁盘读取的文件元数据、数据库查询的临时结果、网络请求的短期响应等。这些数据通常有以下几个共同点:单个对象占内存较多、重新生成成本可以接受、业务允许短期内重新加载。使用弱引用缓存后,当内存压力上升、GC 发生时,这些对象可以被优先回收,从而降低 OutOfMemoryError 风险。
但弱引用缓存并不是所有缓存的默认选择。对于创建成本极高且无法重建的对象、需要长期保存的配置数据、需要严格过期时间和容量控制的缓存,直接使用 WeakReference 并不合适。弱引用的回收时机由 JVM 决定,无法精确控制。如果需要精确控制缓存大小,可以在弱引用缓存之上再叠加 LRU 或 TTL 策略,形成双层缓存。也可以使用 ReferenceQueue 在对象被回收后及时清理辅助数据结构。
另一个常见误区是把弱引用缓存当成主动淘汰策略。实际上弱引用不会主动删除 Map 中的 key,只是让 value 对象变成可回收。缓存 entry 本身可能仍然留在 Map 中,直到下一次查询或清理操作才会移除。因此在高并发、key 数量很大的场景中,还需要配合定期清理或访问时清理,避免 Map 中残留大量空引用,占用额外的内存和哈希表空间。
弱引用和软引用经常被拿来比较。SoftReference 在内存不足时回收,回收时机更晚,适合希望尽量保留数据的缓存;WeakReference 回收更积极,适合只要能回收就可以丢弃的数据。在服务端长时间运行的系统中,弱引用通常比软引用更容易预测行为,因为软引用何时回收受 JVM 实现和堆使用情况影响较大。最后,缓存设计本质上是在性能与内存之间做权衡,弱引用缓存提供了一个更安全的默认起点,但仍需要结合监控、GC 日志和实际缓存命中率不断调整。
Java弱引用WeakReference缓存内存溢出修改时间:2026-10-04 00:45:21