👋你好,欢迎来到我的博客!我是【菜鸟不学编程】
我是一个正在奋斗中的职场码农,步入职场多年,正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上,我决定记录下自己的学习与成长过程,也希望通过博客结识更多志同道合的朋友。
🛠️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等,也会分享一些踩坑经历与面试复盘,希望能为还在迷茫中的你提供一些参考。
💡 我相信:写作是一种思考的过程,分享是一种进步的方式。
如果你和我一样热爱技术、热爱成长,欢迎关注我,一起交流进步!
全文目录:
- I. 引用类型:Strong / Soft / Weak / Phantom(先把“权力”讲清楚)
- 1)Strong Reference(强引用)
- 2)Soft Reference(软引用)
- 3)Weak Reference(弱引用)
- 4)Phantom Reference(虚引用)
- II. `WeakHashMap`:键是弱引用,值不是(很多人第一反应就理解错了)
- 经典误区(请你务必避开)
- 它最适合的场景是什么?
- III. `ReferenceQueue`:引用清理通知(“谁死了我知道”)
- 一个最小可用示例:弱引用 + ReferenceQueue
- IV. 使用场景:缓存与内存敏感应用(别一上来就 WeakHashMap,先想清楚目标)
- 1)“我想要稳定命中率的缓存”
- 2)“我不想因为缓存导致内存泄漏,但命中率不重要”
- 3)“我想要内存紧张时自动让步”
- V. 与 GC 交互:`finalize` 的替代(别再指望 finalize 了)
- VI. 示例:图像缓存系统(WeakHashMap + ReferenceQueue + LRU 兜底)
- 代码:ImageCache(带 LRU + Soft + 清理线程)
- 这个示例为什么更“工程化”?
- 最后一句“带点情绪的中立结论”
- 📝 写在最后
I. 引用类型:Strong / Soft / Weak / Phantom(先把“权力”讲清楚)
Java 里对象能不能活下去,根本问题是:GC Roots 到它的可达性。引用类型就是在可达性上做“不同强度的约定”。
1)Strong Reference(强引用)
Objecto=newObject();- 只要强引用还在,对象基本不会被回收。
- 你平时写的 99% 都是强引用。
- 最可靠,但也最容易内存泄漏(尤其是静态集合/单例缓存)。
2)Soft Reference(软引用)
SoftReference<Object>ref=newSoftReference<>(newObject());- 设计初衷:做“内存敏感缓存”。
- GC 在内存紧张时更倾向回收软引用指向对象(并不是立刻回收)。
- 特点:比 Weak “更能活”,但也更不可控(跟 JVM 策略、堆压力相关)。
3)Weak Reference(弱引用)
WeakReference<Object>ref=newWeakReference<>(newObject());- 只要对象只剩弱引用,下一次 GC 就可能回收(基本是“看见就收”)。
- 适合做“对象生命周期跟随外部强引用”的映射关系。
- 典型:
WeakHashMap的 key。
4)Phantom Reference(虚引用)
PhantomReference<Object>ref=newPhantomReference<>(obj,queue);get()永远返回null。- 用于对象即将被回收时的通知(必须配合
ReferenceQueue)。 - 常见用途:管理堆外资源释放(DirectByteBuffer 类似思路)、清理器(Cleaner)方案等。
你可以粗暴理解:
Strong:护身符
Soft:缺钱才卖的保险
Weak:一阵风就散
Phantom:临终通知(但你摸不到人)
II.WeakHashMap:键是弱引用,值不是(很多人第一反应就理解错了)
WeakHashMap<K,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入队,你就能在后台线程里做清理、统计、释放资源等。
一个最小可用示例:弱引用 + ReferenceQueue
importjava.lang.ref.*;importjava.util.concurrent.*;publicclassRefQueueDemo{staticclassKeyRefextendsWeakReference<Object>{finalStringid;KeyRef(Objectreferent,ReferenceQueue<Object>q,Stringid){super(referent,q);this.id=id;}}publicstaticvoidmain(String[]args)throwsException{ReferenceQueue<Object>q=newReferenceQueue<>();Objectkey=newObject();KeyRefref=newKeyRef(key,q,"k1");key=null;// 断开强引用System.gc();// 提示 GC(不保证立刻,但示例里通常会发生)Reference<?>polled=q.remove(2000);System.out.println(polled!=null?"collected: "+((KeyRef)polled).id:"not collected yet");}}现实项目里你不会
System.gc(),你会用一个后台清理线程一直remove()或poll()来处理队列。
IV. 使用场景:缓存与内存敏感应用(别一上来就 WeakHashMap,先想清楚目标)
我一般把需求拆成三类:
1)“我想要稳定命中率的缓存”
- 用 LRU/LFU(Caffeine、Guava Cache,或自己实现)
- 明确容量、过期策略、统计指标
- 不要用 Weak/Soft 去赌 GC
2)“我不想因为缓存导致内存泄漏,但命中率不重要”
- WeakReference/WeakHashMap 可以考虑
- 典型:附属数据、监听器注册表(某些场景下)
3)“我想要内存紧张时自动让步”
- SoftReference 可能适合,但要注意:不同 JVM/参数下行为差异大
- 生产环境要监控命中率和回收行为,别拍脑袋上
我的中立建议是:
Weak/Soft 更像“最后一道保险”,不是“缓存策略本身”。
缓存策略必须可控,保险才有意义。
V. 与 GC 交互:finalize的替代(别再指望 finalize 了)
finalize()这东西在工程里基本属于“能不用就不用”的古董。原因很简单:
- 调用时机不确定
- 会拖慢 GC
- 还容易复活对象(更阴间)
更现代、可控的替代路线:
- 显式释放资源:
try-with-resources(最可靠) - Cleaner:JDK 提供的清理机制(比 finalize 现代得多)
- PhantomReference + ReferenceQueue:更底层、更灵活(也更考验功力)
你要做图像缓存这种“纯堆内存对象”,一般不需要 finalize;但如果你缓存里带堆外资源(比如 DirectByteBuffer 或 native handle),那就必须认真对待“回收通知”和“资源释放”。
VI. 示例:图像缓存系统(WeakHashMap + ReferenceQueue + LRU 兜底)
来,重点来了:我们做一个图像缓存系统,目标很明确:
key:
String imageId(注意:不能直接用 WeakHashMap<String,…> 作为核心,因为 String 往往被强引用导致回收不如预期)value:
BufferedImage(示例用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(可控、稳定)privatefinalMap<String,byte[]>lru;// 二级:SoftReference(内存紧张时自动释放)privatefinalConcurrentHashMap<String,ImageRef>softCache=newConcurrentHashMap<>();privatefinalReferenceQueue<byte[]>refQueue=newReferenceQueue<>();// 用于从 ReferenceQueue 反查 keyprivatestaticclassImageRefextendsSoftReference<byte[]>{finalStringkey;ImageRef(Stringkey,byte[]referent,ReferenceQueue<byte[]>q){super(referent,q);this.key=key;}}publicImageCache(intlruMaxEntries){this.lru=Collections.synchronizedMap(newLinkedHashMap<>(16,0.75f,true){@OverrideprotectedbooleanremoveEldestEntry(Map.Entry<String,byte[]>eldest){booleanevict=size()>lruMaxEntries;if(evict){// LRU 淘汰时,把强引用降级到 Soft(二级缓存)putSoft(eldest.getKey(),eldest.getValue());}returnevict;}});// 后台清理线程:把已被 GC 的 SoftRef 从 softCache 移除Threadcleaner=Thread.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[]v=lru.get(key);if(v!=null)returnv;// 再查二级 SoftImageRefref=softCache.get(key);if(ref!=null){v=ref.get();if(v!=null){// 回填到 LRU,提高后续命中lru.put(key,v);returnv;}else{// ref 已被回收,顺手清理softCache.remove(key,ref);}}returnnull;}publicvoidput(Stringkey,byte[]imageBytes){if(key==null||imageBytes==null)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(){return"lru="+lru.size()+", soft="+softCache.size();}// 模拟加载图片(实际你会从磁盘/网络加载)publicstaticbyte[]loadImage(Stringid){// 假装每张图 1MBbyte[]data=newbyte[1024*1024];data[0]=(byte)id.hashCode();returndata;}publicstaticvoidmain(String[]args)throwsException{ImageCachecache=newImageCache(50);// 模拟访问:1000 张图随机访问Randomr=newRandom();for(inti=0;i<2000;i++){Stringid="img-"+r.nextInt(1000);byte[]img=cache.get(id);if(img==null){img=loadImage(id);cache.put(id,img);}if(i%200==0){System.out.println("i="+i+" "+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
🧵 本文原创,转载请注明出处。