引言
《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。
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#reachability
SoftReference
垃圾回收器会根据内存使用情况对软引用对象进行回收,当然jvm会尽可能的不回收软引用对象,至于什么情况下回收,JDK并没有明确说明。根据其特点可以看出SoftReference可以用于缓存设计缓存,这样不用自己去设计LRU等算法对缓存进行清理。
一旦垃圾收集器认为软引用对象需要被清理时,JVM会解除软引用对对象的引用(即将referent字段置为null),同时或者稍后将软引用自己放到ReferenceQueue(如果创建软引用时有传入)
JDK保证在jvm抛出OutOfMemoryError前,清掉所有的软引用对象。除此之外,并不保证软引用对象被清理的时间点,而且也不保证软引用对象清理的顺序。
WeakReference
一旦垃圾回收器检测到对象只有弱引用,会立即解除弱引用(将弱引用的referent字段置为null),同时或者稍后将弱引用放入ReferenceQueue(如果创建软引用时有传入)。
弱引用不能阻止垃圾回收器对对象的回收,因此弱引用一般用于“规范化映射(canonicalizing mappings)”,例如WeakHashMap。
对canonicalizing mappings详细说明可以阅读
https://objectcomputing.com/resources/publications/sett/june-2000-collaborating-with-the-java-memory-manager
https://wiki.c2.com/?CanonicalizedMapping
PhantomReference
虚引用在垃圾回收器确定其引用对象可以被回收后放到ReferenceQueue,这一点与SoftReference及WeakReference有所区别。对于finalizable对象,后两者在垃圾回收器将对象放入finalization队列时,就会解除引用并将引用放入到ReferenceQueue。而PhantomReference必须在finalizable对象从finalization队列移除后,并且被垃圾回收器再次检测到不可达能够真正的回收其内存时,放入到ReferenceQueue中,而且引用不会自动解除。
PhantomReference不会被jvm自动清理引用关系的猜测
PhantomReference引入的目的是用于做对象回收前的清理,因此得继续保持引用关系,让JVM无法回收内存,然后应用层代码得到入队通知后,调用相关清理逻辑(可能涉及到访问被回收对象的相关字段,所以清理逻辑执行完对象不能被回收,不然就相当于c语言中的指针越界访问了),并手工清除引用,让对象彻底不可达,然后被JVM回收。
基于上述推测,对JVM来说对象的内存能够被回收的前提是,没有任何引用指向它,包括强引用、软引用、弱引用、虚引用。该推测也在oracle的官方文档得到印证。
https://docs.oracle.com/javase/6/docs/api/java/lang/ref/package-summary.html#reachability
Going 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类加入队列时机的验证
验证代码如下:
public class DemoApplication {
public static void main(String[] args) {
ReferenceQueue<Object> referenceQueue = new ReferenceQueue();
SoftReference<Object> weakReference = new SoftReference<>(new Object() {
private int a = 10;
protected void finalize() throws Throwable {
// 排除线程调度对执行顺序的影响
Thread.sleep(10000);
System.out.println("-----------finalize----");
}
}, referenceQueue);
new Thread(() -> {
try {
Reference<?> removed = referenceQueue.remove();
System.out.println("-----------referenceQueue----");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
while (true) {
System.gc();
}
}
}
SoftReference验证结果
可以看到程序一直不打印日志。因为代码就创建了一个对象,内存是充足的,所以JVM不会去尝试回收软引用对象。
WeakReference验证结果
从第一个图可以看到jvm会自动清除引用关系,第二个图可以看到jvm将弱引用入队、并清除引用关系,发生在finalize之前。
PhantomReference验证结果
从第一个图可以看到jvm不会自动清除引用关系,第二个图可以看到jvm将虚引用入队发生在finalize之后。
所以应用层从引用队列中移出PhantomReference后,一定要及时调用clear()函数解除引用。
反思
通过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.htm
- https://www.oracle.com/technical-resources/articles/javase/finalization.html
- https://wiki.c2.com/?CanonicalizedMapping
- https://objectcomputing.com/resources/publications/sett/june-2000-collaborating-with-the-java-memory-manager