你确定你的“缓存”不是在跟 GC 赌球吗?WeakHashMap 真能让你少背锅? 你好欢迎来到我的博客我是【菜鸟不学编程】我是一个正在奋斗中的职场码农步入职场多年正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上我决定记录下自己的学习与成长过程也希望通过博客结识更多志同道合的朋友。️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等也会分享一些踩坑经历与面试复盘希望能为还在迷茫中的你提供一些参考。 我相信写作是一种思考的过程分享是一种进步的方式。如果你和我一样热爱技术、热爱成长欢迎关注我一起交流进步全文目录I. 引用类型Strong / Soft / Weak / Phantom先把“权力”讲清楚1Strong Reference强引用2Soft Reference软引用3Weak Reference弱引用4Phantom Reference虚引用II. WeakHashMap键是弱引用值不是很多人第一反应就理解错了经典误区请你务必避开它最适合的场景是什么III. ReferenceQueue引用清理通知“谁死了我知道”一个最小可用示例弱引用 ReferenceQueueIV. 使用场景缓存与内存敏感应用别一上来就 WeakHashMap先想清楚目标1“我想要稳定命中率的缓存”2“我不想因为缓存导致内存泄漏但命中率不重要”3“我想要内存紧张时自动让步”V. 与 GC 交互finalize 的替代别再指望 finalize 了VI. 示例图像缓存系统WeakHashMap ReferenceQueue LRU 兜底代码ImageCache带 LRU Soft 清理线程这个示例为什么更“工程化”最后一句“带点情绪的中立结论” 写在最后I. 引用类型Strong / Soft / Weak / Phantom先把“权力”讲清楚Java 里对象能不能活下去根本问题是GC Roots 到它的可达性。引用类型就是在可达性上做“不同强度的约定”。1Strong Reference强引用ObjectonewObject();只要强引用还在对象基本不会被回收。你平时写的 99% 都是强引用。最可靠但也最容易内存泄漏尤其是静态集合/单例缓存。2Soft Reference软引用SoftReferenceObjectrefnewSoftReference(newObject());设计初衷做“内存敏感缓存”。GC 在内存紧张时更倾向回收软引用指向对象并不是立刻回收。特点比 Weak “更能活”但也更不可控跟 JVM 策略、堆压力相关。3Weak Reference弱引用WeakReferenceObjectrefnewWeakReference(newObject());只要对象只剩弱引用下一次 GC 就可能回收基本是“看见就收”。适合做“对象生命周期跟随外部强引用”的映射关系。典型WeakHashMap的 key。4Phantom Reference虚引用PhantomReferenceObjectrefnewPhantomReference(obj,queue);get()永远返回null。用于对象即将被回收时的通知必须配合ReferenceQueue。常见用途管理堆外资源释放DirectByteBuffer 类似思路、清理器Cleaner方案等。你可以粗暴理解Strong护身符Soft缺钱才卖的保险Weak一阵风就散Phantom临终通知但你摸不到人II.WeakHashMap键是弱引用值不是很多人第一反应就理解错了WeakHashMapK,V的关键点是弱的是 key不是 value。也就是说如果某个 key 在外部不再被强引用持有那么这条 entry 会在 GC 后变成“可清理”随后被WeakHashMap移除通常在后续访问/操作时触发清理经典误区请你务必避开误区 A把字符串常量当 key 做 WeakHashMap 缓存比如map.put(img:001,image);字符串常量可能长期被强引用常量池、静态字段、其他缓存你会发现怎么也不回收。误区 B以为它能当“稳定缓存”WeakHashMap 的命中率不稳定因为回收时机不由你决定。它更像“跟随外部对象生命周期的附属数据结构”而不是你传统理解的缓存。它最适合的场景是什么为某些对象附加元数据metadata且元数据不应阻止对象被回收比如对BufferedImage或ClassLoader、Session做一些额外信息映射但不想造成泄漏。III.ReferenceQueue引用清理通知“谁死了我知道”如果你想知道“弱引用/软引用指向的对象被 GC 处理了”就需要ReferenceQueue。GC 在回收对象时会把对应的Reference入队你就能在后台线程里做清理、统计、释放资源等。一个最小可用示例弱引用 ReferenceQueueimportjava.lang.ref.*;importjava.util.concurrent.*;publicclassRefQueueDemo{staticclassKeyRefextendsWeakReferenceObject{finalStringid;KeyRef(Objectreferent,ReferenceQueueObjectq,Stringid){super(referent,q);this.idid;}}publicstaticvoidmain(String[]args)throwsException{ReferenceQueueObjectqnewReferenceQueue();ObjectkeynewObject();KeyRefrefnewKeyRef(key,q,k1);keynull;// 断开强引用System.gc();// 提示 GC不保证立刻但示例里通常会发生Reference?polledq.remove(2000);System.out.println(polled!null?collected: ((KeyRef)polled).id:not collected yet);}}现实项目里你不会System.gc()你会用一个后台清理线程一直remove()或poll()来处理队列。IV. 使用场景缓存与内存敏感应用别一上来就 WeakHashMap先想清楚目标我一般把需求拆成三类1“我想要稳定命中率的缓存”用 LRU/LFUCaffeine、Guava Cache或自己实现明确容量、过期策略、统计指标不要用 Weak/Soft 去赌 GC2“我不想因为缓存导致内存泄漏但命中率不重要”WeakReference/WeakHashMap 可以考虑典型附属数据、监听器注册表某些场景下3“我想要内存紧张时自动让步”SoftReference 可能适合但要注意不同 JVM/参数下行为差异大生产环境要监控命中率和回收行为别拍脑袋上我的中立建议是Weak/Soft 更像“最后一道保险”不是“缓存策略本身”。缓存策略必须可控保险才有意义。V. 与 GC 交互finalize的替代别再指望 finalize 了finalize()这东西在工程里基本属于“能不用就不用”的古董。原因很简单调用时机不确定会拖慢 GC还容易复活对象更阴间更现代、可控的替代路线显式释放资源try-with-resources最可靠CleanerJDK 提供的清理机制比 finalize 现代得多PhantomReference ReferenceQueue更底层、更灵活也更考验功力你要做图像缓存这种“纯堆内存对象”一般不需要 finalize但如果你缓存里带堆外资源比如 DirectByteBuffer 或 native handle那就必须认真对待“回收通知”和“资源释放”。VI. 示例图像缓存系统WeakHashMap ReferenceQueue LRU 兜底来重点来了我们做一个图像缓存系统目标很明确keyString imageId注意不能直接用 WeakHashMapString,… 作为核心因为 String 往往被强引用导致回收不如预期valueBufferedImage示例用byte[]模拟也行我们采用“两级策略”更符合工程现实强引用 LRU容量可控保证常用图片命中软引用缓存内存紧张自动让步作为二级缓冲用ReferenceQueue回收二级缓存里已被 GC 清掉的条目防止 map 里堆一堆空壳这样比“直接 WeakHashMap 一把梭”稳定很多也更像真实系统。下面示例用SoftReference做二级缓存因为图像通常内存占用大“内存紧张时自动让步”是合理诉求。你要改 Weak 也行但命中会更飘。代码ImageCache带 LRU Soft 清理线程importjava.lang.ref.ReferenceQueue;importjava.lang.ref.SoftReference;importjava.util.*;importjava.util.concurrent.ConcurrentHashMap;publicclassImageCache{// 一级强引用 LRU可控、稳定privatefinalMapString,byte[]lru;// 二级SoftReference内存紧张时自动释放privatefinalConcurrentHashMapString,ImageRefsoftCachenewConcurrentHashMap();privatefinalReferenceQueuebyte[]refQueuenewReferenceQueue();// 用于从 ReferenceQueue 反查 keyprivatestaticclassImageRefextendsSoftReferencebyte[]{finalStringkey;ImageRef(Stringkey,byte[]referent,ReferenceQueuebyte[]q){super(referent,q);this.keykey;}}publicImageCache(intlruMaxEntries){this.lruCollections.synchronizedMap(newLinkedHashMap(16,0.75f,true){OverrideprotectedbooleanremoveEldestEntry(Map.EntryString,byte[]eldest){booleanevictsize()lruMaxEntries;if(evict){// LRU 淘汰时把强引用降级到 Soft二级缓存putSoft(eldest.getKey(),eldest.getValue());}returnevict;}});// 后台清理线程把已被 GC 的 SoftRef 从 softCache 移除ThreadcleanerThread.ofPlatform().name(img-cache-cleaner).daemon().start(()-{while(true){try{ImageRefref(ImageRef)refQueue.remove();// 阻塞等待softCache.remove(ref.key,ref);// 防止误删新 ref}catch(InterruptedExceptione){Thread.currentThread().interrupt();break;}catch(Exceptionignore){// 不要让清理线程死掉}}});}publicbyte[]get(Stringkey){// 先查 LRU强引用byte[]vlru.get(key);if(v!null)returnv;// 再查二级 SoftImageRefrefsoftCache.get(key);if(ref!null){vref.get();if(v!null){// 回填到 LRU提高后续命中lru.put(key,v);returnv;}else{// ref 已被回收顺手清理softCache.remove(key,ref);}}returnnull;}publicvoidput(Stringkey,byte[]imageBytes){if(keynull||imageBytesnull)return;lru.put(key,imageBytes);}privatevoidputSoft(Stringkey,byte[]imageBytes){// 每次写入前顺手 drain 一下队列也行防止 map 空壳堆积drainQueueNonBlocking();softCache.put(key,newImageRef(key,imageBytes,refQueue));}privatevoiddrainQueueNonBlocking(){ImageRefref;while((ref(ImageRef)refQueue.poll())!null){softCache.remove(ref.key,ref);}}// 仅用于观察当前缓存状态publicStringstats(){returnlrulru.size(), softsoftCache.size();}// 模拟加载图片实际你会从磁盘/网络加载publicstaticbyte[]loadImage(Stringid){// 假装每张图 1MBbyte[]datanewbyte[1024*1024];data[0](byte)id.hashCode();returndata;}publicstaticvoidmain(String[]args)throwsException{ImageCachecachenewImageCache(50);// 模拟访问1000 张图随机访问RandomrnewRandom();for(inti0;i2000;i){Stringidimg-r.nextInt(1000);byte[]imgcache.get(id);if(imgnull){imgloadImage(id);cache.put(id,img);}if(i%2000){System.out.println(ii cache.stats());}}System.out.println(done cache.stats());}}这个示例为什么更“工程化”LRU 一级缓存可控、可预测容量上限明确Soft 二级缓存内存紧张时自动释放作为让步不作为主策略ReferenceQueue 清理避免 softCache 里堆积“已被回收的引用壳”避免 WeakHashMap 直接做缓存因为 key 的生命周期不可控会导致命中率飘如果你硬要 WeakHashMap 做 key→value 的缓存我的建议是key 必须是“外部强引用生命周期明确的对象”比如某个 Session 对象、图片对象本身、ClassLoader 等而不是字符串 ID。最后一句“带点情绪的中立结论”弱引用/软引用这类东西像“自动档刹车辅助”用对了能救你一命用错了你会在最不该失速的时候失速然后还不知道为什么 所以我更愿意这么建议缓存策略用 LRU/LFU 这种可控方案打底Weak/Soft 作为“内存压力下的退路”或“生命周期跟随”的辅助结构。别把系统稳定性交给 GC 心情。 写在最后如果你觉得这篇文章对你有帮助或者有任何想法、建议欢迎在评论区留言交流你的每一个点赞 、收藏 ⭐、关注 ❤️都是我持续更新的最大动力我是一个在代码世界里不断摸索的小码农愿我们都能在成长的路上越走越远越学越强感谢你的阅读我们下篇文章再见✍️ 作者某个被流“治愈”过的 Java 老兵 日期2026-01-07 本文原创转载请注明出处。