公司动态
Java Finalization‘s Memory-Retention Issues 及Reference类解析
引言《Effective Java Programming Language Guide》 一书中强烈建议不要使用java的finalize()方法去做对象消亡前的清理。因为jvm调用finalize()方法的时机并不确定容易导致Memory-Retention Issues。通俗点讲就是内存没办法及时回收。详细的见oracle的官方说明https://www.oracle.com/technical-resources/articles/javase/finalization.html。Memory-Retention Issues问题简述如果类有重写finalize()方法JVM会将该类的对象标记为finalizable区别于普通对象被垃圾收集器判定为不可达时会立即回收内存finalizable的对象会经过更多的GC周期。下图来自oracle官方。finalizable对象回收通常经历以下过程垃圾回收器检查到该对象不可达垃圾回收器将该对象加入到finalization队列对象变成可达专门的Finalizer线程从finalization队列中将对象移除并执行对象的finalize()方法,并将对象标记为finalized垃圾回收器再次发现该对象不可达而且为finalized状态的(finalize方法已执行过)因此直接回收该对象内存。以上过程的一些个人分析jvm为什么不直接在回收对象前调用finalize()方法而是使用专门的线程去执行java每次GC需要通过可达性分析标记大量对象回收内存后还涉及内存整理如果把finalize()方法的调用也放到这个过程中GC耗时会更长影响系统的响应时间所以只能由另外的过程去处理。Memory-Retention问题从上述过程可以看到finalizable对象至少要2个GC周期才能将对象回收掉。更有甚者如果系统中有大量的对象是finalizable或者有些对象finalize()方法本身就比较耗时加上只有一个Finalizer线程这个线程优先级并不比别的高还会和其他线程竞争执行资源对象在finalization队列中呆的时间更长。在这期间如果有发生GC垃圾收集器也是无法清理这些对象的因为这些对象还在被finalization队列强引用。所以容易产生Memory-Retention问题。Memory-Retention问题解决方案《Effective Java Programming Language Guide》建议使用JDK的Cleaner来做对象消亡前的清理其基于PhantomReference下面系统的介绍JDK的Reference。更新《Effective Java Programming Language Guide》里面也不建议使用Cleaner了Reference解析java的Reference的相关子类用于应用层与垃圾收集器有更多的交互通俗点讲就是垃圾收集器给应用层暴露一些API让应用对对象的回收时机有了一定的控制能力。java官方文档对Reference引入的目的说明如下Provides reference-object classes, which support a limited degree of interaction with the garbage collector. A program may use a reference object to maintain a reference to some other object in such a way that the latter object may still be reclaimed by the collector. A program may also arrange to be notified some time after the collector has determined that the reachability of a given object has changed.翻译如下提供引用对象类支持与垃圾收集器进行有限程度的交互。程序可以使用引用对象来维护对某个其他对象的引用以便后一个对象仍然可以被收集器回收。程序还可以安排在收集器确定给定对象的可达性已更改之后的某个时间收到通知。原文见https://docs.oracle.com/javase/6/docs/api/java/lang/ref/package-summary.html#reachabilitySoftReference垃圾回收器会根据内存使用情况对软引用对象进行回收当然jvm会尽可能的不回收软引用对象至于什么情况下回收JDK并没有明确说明。根据其特点可以看出SoftReference可以用于缓存设计缓存这样不用自己去设计LRU等算法对缓存进行清理。一旦垃圾收集器认为软引用对象需要被清理时JVM会解除软引用对对象的引用(即将referent字段置为null)同时或者稍后将软引用自己放到ReferenceQueue(如果创建软引用时有传入)JDK保证在jvm抛出OutOfMemoryError前清掉所有的软引用对象。除此之外并不保证软引用对象被清理的时间点而且也不保证软引用对象清理的顺序。SoftReference在JDK中实际使用场景如上述介绍SoftReference适合用在缓存场景可以看到JDK中也确实是这样用的大家可以挑ImageSoftReference去看看源码因为代码比较简单这里就不过多介绍了。WeakReference一旦垃圾回收器检测到对象只有弱引用会立即解除弱引用(将弱引用的referent字段置为null)同时或者稍后将弱引用放入ReferenceQueue(如果创建软引用时有传入)。弱引用不能阻止垃圾回收器对对象的回收因此弱引用一般用于“规范化映射(canonicalizing mappings)”,例如WeakHashMap。关于规范化映射的解释何谓规范化映射这个大家可以问一下千问可以得到很形象的回答。我这里总结一下规范化映射是指逻辑上相等的对象在内存中只存在一份其他使用的地方都指向这一个对象。比如像商城类系统一般会定义商品类型。假设为Class Product。对于某个具体的商品比如说A。如果对于A商品在内存中只有唯一的一份实例Product类型。那么我们就称为规范化映射。StringID_AxxxProductAnewProduct(ID_A,xx,xx);MapmapnewHashMap();map.put(ID_A,A);doBusiness(A);从上述代码可以看到对于商品A在内存中只有唯一的一份其他所有地方对商品A的引用都是指向这唯一的实例。这样就有一个特点一旦指向A的引用置空后应用层无法在建立起对商品A对象的引用。对canonicalizing mappings详细说明可以阅读https://objectcomputing.com/resources/publications/sett/june-2000-collaborating-with-the-java-memory-managerhttps://wiki.c2.com/?CanonicalizedMappingWeakReference在JDK中实际使用场景如上所述WeakReference用于规范化映射场景一旦对象强引用全部解除后应用在也无法访问到该对象此时跟该对象关联的资源如果需要清理的话可以借助WeakReference得到通知从而有时机进行资源清理。简单来讲就是一个对象会被多处引用一旦所有的强引用被解除后才能清理资源此时利用WeakReference机制更容易写代码。不然应用层自己得维护引用计数当计数为0后执行清理动作。JDK中的WeakHashMap有使用到WeakReference这样可以自动清理WeakHashMap中无效的Entry。如下图WeakHashMap的Entry是WeakReference一旦对象的强引用被置空后根据WeakReference的特性JVM会自动清除WeakReference对其引用将对象回收。至于Entry本身此时也没有用处需要从WeakHashMap中清理掉该逻辑可以跟踪java.util.WeakHashMap#expungeStaleEntries函数。PhantomReference虚引用在垃圾回收器确定其引用对象可以被回收后放到ReferenceQueue这一点与SoftReference及WeakReference有所区别。对于finalizable对象后两者在垃圾回收器将对象放入finalization队列时就会解除引用并将引用放入到ReferenceQueue。而PhantomReference必须在finalizable对象从finalization队列移除后并且被垃圾回收器再次检测到不可达能够真正的回收其内存时放入到ReferenceQueue中而且引用不会自动解除。PhantomReference在JDK中实际使用场景PhantomReference不会被jvm自动清理引用关系的猜测PhantomReference引入的目的是用于做对象回收前的清理因此得继续保持引用关系让JVM无法回收内存然后应用层代码得到入队通知后调用相关清理逻辑(可能涉及到访问被回收对象的相关字段所以清理逻辑执行完对象不能被回收不然就相当于c语言中的指针越界访问了)并手工清除引用让对象彻底不可达然后被JVM回收。基于上述推测对JVM来说对象的内存能够被回收的前提是没有任何引用指向它包括强引用、软引用、弱引用、虚引用。该推测也在oracle的官方文档得到印证。https://docs.oracle.com/javase/6/docs/api/java/lang/ref/package-summary.html#reachabilityGoing from strongest to weakest, the different levels of reachability reflect the life cycle of an object. They are operationally defined as follows:1、 An object is strongly reachable if it can be reached by some thread without traversing any reference objects. A newly-created object is strongly reachable by the thread that created it.2、 An object is softly reachable if it is not strongly reachable but can be reached by traversing a soft reference.An object is weakly reachable if it is neither strongly nor softly reachable but can be reached by traversing a weak reference. When the weak references to a weakly-reachable object are cleared, the object becomes eligible for finalization.3、 An object is phantom reachable if it is neither strongly, softly, nor weakly reachable, it has been finalized, and some phantom reference refers to it.4、Finally, an object is unreachable, and therefore eligible for reclamation, when it is not reachable in any of the above ways.关于各种Reference类加入队列时机的验证验证代码如下publicclassDemoApplication{publicstaticvoidmain(String[]args){ReferenceQueueObjectreferenceQueuenewReferenceQueue();SoftReferenceObjectweakReferencenewSoftReference(newObject(){privateinta10;protectedvoidfinalize()throwsThrowable{// 排除线程调度对执行顺序的影响Thread.sleep(10000);System.out.println(-----------finalize----);}},referenceQueue);newThread(()-{try{Reference?removedreferenceQueue.remove();System.out.println(-----------referenceQueue----);}catch(InterruptedExceptione){e.printStackTrace();}}).start();while(true){System.gc();}}}SoftReference验证结果可以看到程序一直不打印日志。因为代码就创建了一个对象内存是充足的所以JVM不会去尝试回收软引用对象。WeakReference验证结果从第一个图可以看到jvm会自动清除引用关系第二个图可以看到jvm将弱引用入队、并清除引用关系发生在finalize之前。PhantomReference验证结果从第一个图可以看到jvm不会自动清除引用关系第二个图可以看到jvm将虚引用入队发生在finalize之后。所以应用层从引用队列中移出PhantomReference后一定要及时调用clear()函数解除引用。PS以上内容是基于JDK8当时发现PhantomReference得到通知时还引用着对象我以为PhantomReference就是利用这个时机调用对象清理函数释放资源但实际上又没办法拿到它觉得这个设计有点矛盾通过ai得到回答这个也是jvm不得已为之有兴趣的大家可以通过AI对话了解一下背景。jdk21PhantomReference对象得到通知时引用关系已经清除了这一点跟jdk8有点区别。反思通过PhantomReference实现对象消亡前的清理工作对比finalize()方法到底有什么优势无论是PhantomReference还是finalize()都无法降低清理工作的计算量总工作量不变的情况下要实现更快的清理速度那只能使用并发个人认为这是PhantomReference相对finalize()唯一优势。finalize()只能由单独的Finalizer线程执行清理逻辑而PhantomReference由应用层代码执行可以采用多线程增加并发度。但是PhantomReference要求应用层在将弱引用移出引用队列后及时调用clear()函数这个开发者很容易忽略而埋下潜在的风险。所以《Effective Java Programming Language Guide》推荐类定义清理函数然后由开发者主动调用finalize()函数作为最终的兜底。备注本文有些地方言辞可能并不严谨作者是刻意没去发散细节一旦要对所有细节都做阐述及证明那这边文章很难写完。本文主要只是想简明的阐述Memory-Retention问题以及Reference引入的目的及使用场景。文章中大部分是基于JDK的类注释及参考文档的理解写的该篇文章由于英文水平一般所以文中有些地方读起来可能不太好理解建议大家反复阅读参考文档及jdk的注释相互印证这样才能彻底理解英文文献想要表达的意思。参考文档汇总https://docstore.mik.ua/orelly/java-ent/jnut/ch13_01.htmhttps://www.oracle.com/technical-resources/articles/javase/finalization.htmlhttps://wiki.c2.com/?CanonicalizedMappinghttps://objectcomputing.com/resources/publications/sett/june-2000-collaborating-with-the-java-memory-manager