前请先看《一提到Reference 99.99%的java程序员都懵逼了》,否则里面的讲解会看不懂!
前面一文将了Reference类,现在来看看FinalReference,相信大部分程序员都遇到过Finalizer对象占用内存过高或导致oom的问题,这篇文章告诉你为什么会出现这种问题:
为什么需要FinalReference
因为jvm只能管理jvm内存空间,但是对于应用运行时需要的其它native资源(jvm通过jni暴漏出来的功能):例如直接内存DirectByteBuffer,网络连接SocksSocketImpl,文件流FileInputStream等与操作系统有交互的资源,jvm就无能为力了,需要我们自己来调用释放这些资源方法来释放,为了避免对象死了之后,程序员忘记手动释放这些资源,导致这些对象有的外部资源泄露,java提供了finalizer机制通过重写对象的finalizer方法,在这个方法里面执行释放对象占用的外部资源的操作,这样使用这些资源的程序员即使忘记手动释放,jvm也可以在回收对象之前帮助释放掉这些外部资源,帮助我们调用这个方法回收资源的线程就是我们在导出jvm线程栈时看到的名为Finalizer的守护线程;
FinalReference简介
Finalizer继承FinalReference类,FinalReference继承Reference类,对象最终会被封装为Finalizer对象,如果去查看源码会发现Finalizer的构造方法是不对外暴漏,所以我们无法自己创建Finalizer对象,FinalReference是由jvm自动封装;
什么样的对象会被封装为Finalizer对象?
这里重点要说明一下什么样的类才能被封装为Finalizer:
1. 当前类或其父类含有一个参数为空,返回值为void,名为finalize的方法;
2. 这个finalize方法体不能为空;
满足以上条件的类称之为f类
f类的对象是如何被封装为Finalizer对象的?
Finalizer类的两个关键的方法
//私有的构造方法
private Finalizer(Objectfinalizee) {
//被封装的对象和全局的f-queue
super(finalizee,queue);
//调用add方法,将对象入队
add();
}
//静态的register方法,注意它的注释“被vm调用”,所以jvm是通过调用这个方法将对象封装为Finalizer对象的;
/* Invoked by VM */
staticvoid register(Objectfinalizee) {
new Finalizer(finalizee);
}
那么jvm又是在何时调用register方法的呢?
取决于-XX:+RegisterFinalizersAtInit这个参数,默认为true,在调用构造函数返回之前调用Finalizer.register方法,果通过-XX:-RegisterFinalizersAtInit关闭了该参数,那将在对象空间分配好之后就将这个对象注册进去。所以我们创建一个重写了finalize方法的类
本文Finalizer对象注册内容参考的笨神的一篇文章,要了解更详细的内容请,请看这里:
http://lovestblog.cn/blog/2015/07/09/final-reference/
Finalizer解析
重要的成员变量:
//它是unfialized队列的队头,这个队列里面存了Finalizer对象,这里面的Finalizer对象引用的f类都还没有执行finalized方法,unfialized与next、prev三个成员变量组成一个双向链表的数据结构,unfialized永远指向这个链表的头,在Finalizer对象创建的时候会加入到此队列头部,它是静态全局唯一的,所以在这个链表里面的对象都是无法被回收的;在执行f类的finalizer之前,会将引用它的Finalizer对象从unfinalized队列里移除;这个队列的作用是保存全部的只存在FinalizerReference引用、且没有执行过finalize方法的f类的Finalizer对象,防止finalizer对象在其引用的对象之前被gc回收掉;
privatestaticFinalizerunfinalized=null;
//队列链表中后一个对象和前一个对象的引用,由此可见它是一个双向链表
private Finalizernext =null,prev =null;
//操作unfinalized队列的全局锁,入队出队操作都需要加锁
privatestaticfinal Objectlock =new Object();
//ReferenceQueue队列,传说中的f-queue队列,这个队列是全局唯一的,当gc线程发现f类对象除了Finalizer引用外,没有强引用了,就会把它放入到pending队列, HanderReference线程在pending队列取到FinalReference对象的时候,会将把他们都放到这个f-queue队列里面,然后Finalizer线程就可以去这个队列里取出Finalizer对象,在将其移出unfinized队列,最后调用f类的finalizer方法;
privatestaticReferenceQueue<Object>queue =newReferenceQueue<>();
根据上面的成员变量可以看到Finalizer有两个队列,一个是unfialized,一个是f-queue队列;
从上面的介绍我们可以知道,f类对象都有一个返回值为void、参数为空且方法体非空finalize()方法,在f类对象创建的时候,jvm同时也会创建一个Finalizer对象引用这个f类对象, Finalizer对象创建时就被加入到了unfialized里面;
Finalizer线程
privatestatic class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
//如果run方法已经在执行了,直接退出;running值会在后面标记为true
if (running)
return;
// Finalizer thread starts before System.initializeSystemClass
// is called. Wait untilJavaLangAccess is available
//等待jvm初始化完成后才继续执行
while(!VM.isBooted()) {
// delay until VM completesinitialization
try {
VM.awaitBooted();
} catch (InterruptedExceptionx) {
// ignore and continue
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
try {
//将对象从ReferenceQueue中移除,
Finalizer f =(Finalizer)queue.remove();
//通过runFinalizer调用finalizer方法
f.runFinalizer(jla);
} catch (InterruptedExceptionx) {
// ignore and continue
}
}
}
}
//静态代码块中初始化启动FinalizerThread线程,注意它的优先级是Thread.MAX_PRIORITY– 2 = 8,很多文章说它的优先级很低,这是不对的,java里面线程优先级默认都是5;
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY- 2);
finalizer.setDaemon(true);
finalizer.start();
}
创建第二个Finalizer线程
默认Finalizer线程只有一个,在Finalizer里面有这样的一个方法,可以第二个名为“Secondary finalizer ”的finalizer线程来执行回收操作;
/* Create a privileged secondaryfinalizer thread in the system thread
group for the given Runnable, and waitfor it to complete.
This method is used by bothrunFinalization and runFinalizersOnExit.
The former method invokes all pendingfinalizers, while the latter
invokes all uninvoked finalizers ifon-exit finalization has been
enabled.
These two methods could have beenimplemented by offloading their work
to the regular finalizer thread andwaiting for that thread to finish.
The advantage of creating a freshthread, however, is that it insulates
invokers of these methods from a stalledor deadlocked finalizer thread.
*/
privatestaticvoid forkSecondaryFinalizer(final Runnable proc) {
AccessController.doPrivileged(
new PrivilegedAction<Void>() {
public Void run() {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGrouptgn =tg;
tgn !=null;
tg =tgn,tgn=tg.getParent());
Thread sft =new Thread(tg,proc,"Secondary finalizer");
sft.start();
try {
sft.join();
} catch (InterruptedExceptionx) {
/* Ignore */
}
returnnull;
}});
}
/* Called by Runtime.runFinalization() */
staticvoid runFinalization() {
if (!VM.isBooted()) {
return;
}
forkSecondaryFinalizer(new Runnable() {
privatevolatilebooleanrunning;
publicvoid run() {
if (running)
return;
final JavaLangAccessjla = SharedSecrets.getJavaLangAccess();
running =true;
for (;;) {
Finalizer f = (Finalizer)queue.poll();
if (f ==null)break;
f.runFinalizer(jla);
}
}
});
}
runFinalization可以创建第二个Finalizer线程,它无法直接调用,可以通过以下方法调用
System.runFinalization();
Runtime.getRuntime().runFinalization();
如下图所示
finalizer导致的问题
问题一:Finalizer线程是一个单线程来处理F-queue,虽然可以再启动第二个,但是也是两个线程而已,如果系统中有很多线程争用cpu,在系统压力比较大的情况下,Finalizer线程获取到cpu时间片的时间是不确定的,在其获取到时间片之前,应该被回收的Finalizer对象一直在队列中积累,占用大量内存,经过n次gc后,仍然没有机会被释放掉,这些对象都进入到老年代,导致old剩余空间变小,从而使fullgc会更加频繁,如果Finalizer对象积压严重的甚至会导致oom;
问题二:如果Finalizer对象生产的速度比Finalizer线程处理的速度要快,也会导致F-queue队列里面的Finalizer对象积压,这些对象一直占用jvm的内存,直到oom;如果执行某个f类的finalizer方法执行非常耗时,或这个方法里面的操作被锁阻塞了Finalizer线程,那么就会导致队列里面其它的Finalizer对象一直在等待队列里面无法被回收释放空间,最终导致oom;
问题三:Reference对象是在gc的时候来处理的,如果没有触发GC就没有机会触发Reference引用的处理操作,那么应该被回收的FinalReference对象就一直在unfinalized队列里,无法被回收,导致被它引用的对象也无法回收,然后又导致被引用对象占用的资源也不会释放,最终可能会导致native资源耗尽;
问题四:可能导致资源泄露,例如当jvm退出时,很可能unfinalizer队列里的对象没有被处理完就退出了;
问题五:对象有可能在执行过finalize方法后,又被强引用引用到了,于是对象就复活了;
所以释放资源一定要手动去释放,如果忘记释放,依靠finalizer的机制是不靠谱的,很可能会导致一些严重的内存问题或native资源泄露问题,如果一定要用,必须保证调用finalize方法能够快速执行完成;
另外java里面还有一个sun.misc.Cleaner类,它继承自PhantomReference,作用同Finalize一样,它的清理工作是在ReferenceHandel线程里面完成的,只是少了Finalizer线程处理这一步,Finalize存在的问题,它基本都有,如果clean方法使用不当,阻塞ReferenceHander线程,会导致比finalizer线程更加严重的问题;
在java里面DirectByteBuffer这个类就是使用Cleaner清理的,有空再写一篇文章讲解一下DirectByteBuffer的坑!