ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

缓存明明还留着引用,为什么内存紧张时它说没就没了

缓存明明还留着引用,为什么内存紧张时它说没就没了 「Java 进阶之路」系列 Day35写在前面前几篇一直在讲垃圾怎么回收标记清除/整理/复制、分代收集、CMS/G1/ZGC但更前置的问题其实是JVM怎么判断一个对象是不是垃圾很多人第一反应是引用计数但Java压根没用这个方案还有人写缓存时纳闷——明明一个Map里还攥着对象的引用为什么内存紧张时这些对象说没就没了。这篇把这两个问题一次讲清楚。一、是什么可达性分析怎么判断一个对象活着还是死了为什么不用引用计数最直观的判活方案是给每个对象挂一个计数器被引用一次加一引用失效减一计数器归零就判定为垃圾——这就是引用计数法简单直接但有一个硬伤处理不了循环引用。对象A持有对B的引用对象B持有对A的引用如果A和B互相引用即使外部所有代码都不再持有A、B的引用了它们两个的计数器依然都是1永远不会归零会被误判为还活着造成内存泄漏。正是因为这个致命缺陷主流JVM都没有采用引用计数法Python等语言用了引用计数但要靠额外的循环检测器来弥补这个漏洞。可达性分析从根出发摸得到就是活的Java采用的方案是可达性分析Reachability Analysis选定一批特殊的对象作为GC Roots从这些Roots出发沿着引用链一路往下摸凡是能够被摸到可达的对象都判定为存活摸不到的对象才判定为垃圾。GC Roots对象A对象B对象C对象D对象E对象F上图里A、B、C、D、E都能从GC Roots出发摸到判定为存活F虽然自己引用着E但没有任何Roots能摸到F本身F就是垃圾即使F和E之间有引用关系也无济于事——这正好解决了引用计数法处理不了的循环引用问题即使F和E互相引用只要它们整体上摸不到GC Roots照样会被正确回收。常见能作为GC Roots的对象包括各个线程当前调用栈里的局部变量、方法区里的静态变量、被JNINative方法引用的对象、以及一些和类加载、类型相关的固定引用。二、为什么明明有引用还是被回收强引用之外还有三种引用如果只有有引用就存活、没引用就回收这一条规则会有一类场景很难处理想让JVM在内存充裕时尽量保留一批对象比如缓存但内存紧张、快要OOM之前又希望能优先把它们清掉而不是眼睁睁看着系统因为死抱着缓存不放而崩溃。Java用四种强度不同的引用类型来满足这类需求引用类型什么时候被回收典型场景强引用Strong Reference只要还可达永远不会被回收哪怕内存溢出也不放日常的new Object()赋值软引用SoftReference内存足够时不回收内存不足、快要OOM之前会被回收内存敏感的缓存弱引用WeakReference不管内存够不够只要发生一次GC就会被回收ThreadLocal的Entry key虚引用PhantomReference形同虚设随时可能被回收拿不到引用的对象本身只用来在对象被回收时收到一个通知配合Cleaner做资源清理跟踪强引用 永远不回收软引用 内存不足才回收弱引用 只要GC就回收虚引用 完全不影响生存 只用来收通知回到开头的疑问如果缓存的Map里存的是软引用而不是强引用即使这个引用链条一直存在、对象在可达性分析里依然可达只要JVM快要内存不足了垃圾回收器依然会主动把软引用指向的对象清掉把内存腾出来——这不是可达性分析出了错而是软引用本身在设计上就允许JVM在特定条件下选择性地把它们视为可以牺牲的对象。三、怎么用软引用做缓存弱引用防内存泄漏软引用一个天然的、内存敏感的缓存实现MapString,SoftReferencebyte[]cachenewHashMap();// 存入缓存cache.put(key1,newSoftReference(loadBigData()));// 取用时要判断是否已经被回收SoftReferencebyte[]refcache.get(key1);byte[]data(ref!null)?ref.get():null;if(datanull){dataloadBigData();// 已被回收重新加载cache.put(key1,newSoftReference(data));}用软引用包一层缓存就能自动获得内存宽裕时尽量保留内存紧张时自动让路的能力不需要手写LRU淘汰逻辑去猜测什么时候该清理——代价是每次取用都要判空重新加载且软引用对象本身如果引用关系复杂也会带来一定的额外管理开销所以规模较小、访问频繁的缓存用现成的Guava Cache/Caffeine这类工具会比自己手搓软引用更省心。弱引用ThreadLocal为什么用它做key回到Day09讲过的ThreadLocal内存泄漏问题——ThreadLocalMap的Entry对key也就是ThreadLocal实例本身用的就是弱引用只要一次GC发生只要没有其他强引用指着这个ThreadLocal实例它就会被回收Entry里的key会自动变成null。这样设计是为了让外部代码已经不再持有这个ThreadLocal引用这件事能尽快被JVM感知到而不需要用户显式调用remove()才能让key被回收——但value依然是强引用不会跟着自动清理这才是ThreadLocal真正的内存泄漏风险点key没了、value还在占着位置出不去所以remove()该调用还是要调用。虚引用连拿到对象这件事都不允许虚引用是四种里最特殊的一种——通过PhantomReference.get()永远返回null也就是说你压根不能靠虚引用去访问这个对象它唯一的作用是配合一个引用队列ReferenceQueue在对象被垃圾回收器真正回收前的某个时机往队列里塞一个通知。这给了开发者一个对象即将/已经被回收的钩子常用来替代已经过时的finalize()方法做一些堆外内存、文件句柄之类的资源清理收尾工作JDK 9之后的java.lang.ref.Cleaner底层就是基于虚引用实现的。四、面试追问Q1为什么Java不用引用计数法判断对象是否存活引用计数法无法正确处理循环引用的场景两个互相引用的对象即使外部已经没有任何代码持有它们各自的计数器依然不为零永远不会被判定为垃圾导致内存泄漏。Java采用的可达性分析是从GC Roots出发摸引用链天然能正确处理循环引用——只要整体上摸不到GC Roots即使对象之间互相引用也会被回收。Q2哪些对象可以作为GC Roots常见的包括各线程当前调用栈里的局部变量、方法区里的静态变量、被JNINative方法引用的对象以及一些和类加载、类型信息相关的固定引用。可达性分析就是从这批对象出发判断哪些对象能被引用链摸到。Q3软引用和弱引用的核心区别是什么软引用只有在内存不足、即将发生OutOfMemoryError之前才会被回收内存充裕时会尽量保留适合做内存敏感的缓存弱引用则不管内存够不够只要发生一次垃圾回收就会被清理生存周期比软引用短得多典型应用是ThreadLocalMap里Entry对key的引用。Q4ThreadLocal为什么会内存泄漏跟弱引用有什么关系ThreadLocalMap的Entry对keyThreadLocal实例用的是弱引用只要没有其他强引用一次GC后key就会被回收变成null但Entry对value的引用依然是强引用不会自动清理。如果线程是线程池里的核心线程、长期存活且用完ThreadLocal后没有调用remove()这些key已经为null、但value还在的Entry会一直占用内存无法被回收这才是真正的内存泄漏点不是弱引用本身导致的问题而是value没有配套清理导致的。Q5虚引用有什么实际用途虚引用无法通过get()方法获取到对象本身永远返回null唯一作用是配合引用队列在对象被垃圾回收前后收到一个通知。它常被用来替代已经过时、不推荐使用的finalize()方法跟踪对象生命周期的结束时机去做一些堆外内存释放、文件句柄关闭之类的资源清理工作JDK 9之后推荐的java.lang.ref.Cleaner底层就是基于虚引用实现的。下一篇预告Day36 结合线上实战场景讲讲OOM排查和连接池问题该怎么定位——从这几篇讲的GC原理落地到真实的故障排查思路上。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表