您的位置:首页 > 编程语言 > Java开发

JDK源码分析之FinalReference完全解读

2017-03-03 20:08 363 查看
转载自:http://lovestblog.cn/blog/2015/07/09/final-reference/

       JAVA对象引用体系除了强引用之外,出于对性能、可扩展性等方面考虑还特地实现了四种其他引用:SoftReference、WeakReference、PhantomReference、FinalReference,本文主要想讲的是FinalReference,因为在使用内存分析工具比如zprofiler、mat等在分析一些oom的heap的时候,经常能看到 java.lang.ref.Finalizer占用的内存大小远远排在前面,而这个类占用的内存大小又和FinalReference有着密不可分的关系。

       对于FinalReference及关联的内容,可能有如下印象:自己代码里从没有使用过,线程dump之后,能看到一个叫做Finalizer的java线程,偶尔能注意到java.lang.ref.Finalizer的存在、我们在类里可能会写finalize方法。

       那FinalReference到底存在的意义是什么,以怎样的形式和我们的代码相关联呢?结合ReferenceQueue、Reference看,会帮助理解。

FinalReference

       首先我们看看FinalReference在JDK里的实现:
class FinalReference<T> extends Reference<T> {

public FinalReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}

}
       该类访问权限是package的,这也就意味着不能直接去对其进行扩展,但是JDK里对此类进行了扩展实现java.lang.ref.Finalizer,这个类也是我们在概述里提到的,而此类的访问权限也是package的,并且是final的,意味着真的不能被扩展了,接下来的重点围绕java.lang.ref.Finalizer展开。
package java.lang.ref;

import java.security.PrivilegedAction;
import java.security.AccessController;
import sun.misc.JavaLangAccess;
import sun.misc.SharedSecrets;
import sun.misc.VM;

final class Finalizer extends FinalReference<Object> { /* Package-private; must be in
same package as the Reference
class */

private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
private static Finalizer unfinalized = null;
private static final Object lock = new Object();

private Finalizer
next = null,
prev = null;

private boolean hasBeenFinalized() {
return (next == this);
}

private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}

private void remove() {
synchronized (lock) {
if (unfinalized == this) {
if (this.next != null) {
unfinalized = this.next;
} else {
unfinalized = this.prev;
}
}
if (this.next != null) {
this.next.prev = this.prev;
}
if (this.prev != null) {
this.prev.next = this.next;
}
this.next = this;   /* Indicates that this has been finalized */
this.prev = this;
}
}

private Finalizer(Object finalizee) {
super(finalizee, queue);
add();
}

/* Invoked by VM */
static void register(Object finalizee) {
new Finalizer(finalizee);
}

private void runFinalizer(JavaLangAccess jla) {
synchronized (this) {
if (hasBeenFinalized()) return;
remove();
}
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
jla.invokeFinalize(finalizee);

/* Clear stack slot containing this variable, to decrease
the chances of false retention with a conservative GC */
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}

/* Create a privileged secondary finalizer thread in the system thread
group for the given Runnable, and wait for it to complete.

This method is used by both runFinalization and runFinalizersOnExit.
The former method invokes all pending finalizers, while the latter
invokes all uninvoked finalizers if on-exit finalization has been
enabled.

These two methods could have been implemented by offloading their work
to the regular finalizer thread and waiting for that thread to finish.
The advantage of creating a fresh thread, however, is that it insulates
invokers of these methods from a stalled or deadlocked finalizer thread.
*/
private static void forkSecondaryFinalizer(final Runnable proc) {
AccessController.doPrivileged(
new PrivilegedAction<Void>() {
public Void run() {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread sft = new Thread(tg, proc, "Secondary finalizer");
sft.start();
try {
sft.join();
} catch (InterruptedException x) {
/* Ignore */
}
return null;
}});
}

/* Called by Runtime.runFinalization() */
static void runFinalization() {
if (!VM.isBooted()) {
return;
}

forkSecondaryFinalizer(new Runnable() {
private volatile boolean running;
public void run() {
if (running)
return;
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
Finalizer f = (Finalizer)queue.poll();
if (f == null) break;
f.runFinalizer(jla);
}
}
});
}

/* Invoked by java.lang.Shutdown */
static void runAllFinalizers() {
if (!VM.isBooted()) {
return;
}

forkSecondaryFinalizer(new Runnable() {
private volatile boolean running;
public void run() {
if (running)
return;
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
Finalizer f;
synchronized (lock) {
f = unfinalized;
if (f == null) break;
unfinalized = f.next;
}
f.runFinalizer(jla);
}}});
}

private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
if (running)
return;

// Finalizer thread starts before System.initializeSystemClass
// is called.  Wait until JavaLangAccess is available
while (!VM.isBooted()) {
// delay until VM completes initialization
try {
VM.awaitBooted();
} catch (InterruptedException x) {
// ignore and continue
}
}
final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
}

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类的构造函数可以获得下面的几个关键信息:private意味着在外面无法自己构建这类对象,finalizee参数表示FinalReference指向的对象引用,调用add方法是将当前对象插入到Finalizer对象链里,链里的对象和Finalizer类静态相关联,言外之意是在这个链里的对象都无法被gc掉,除非将这种引用关系剥离掉(因为Finalizer类无法被unload)。

       虽然外面无法创建Finalizer对象,但是注意到有一个register的静态方法,在方法里会创建这种对象,同时将这个对象加入到Finalizer对象链里,这个方法是被vm调用的,那么问题来了,vm在什么情况下会调用这个方法呢?

       GC在处理finalizer类的对象的时候要做一些特殊的处理,如在这个对象被回收之前会调用一下它的finalize方法。在java.lang.Object里有这样一个方法:
protected void finalize() throws Throwable { }
       这意味着所有类都会继承这个方法,甚至可以覆写该方法。而判断当前类是否是一个finalizer类的标准并不仅仅是当前类是否含有一个参数为空,返回值为void的名为finalize的方法,而另外一个要求是finalize方法必须非空,因此我们的Object类虽然含有一个finalize方法,但是并不是一个finalizer类,Object的对象在被gc回收的时候其实并不会去调用它的finalize方法。

       需要注意的是我们的类在被加载过程中其实就已经被标记为是否为finalizer类了(遍历所有方法,包括父类的方法,只要有一个非空的参数为空返回void的finalize方法就认为是一个finalizer类)。

       finalizer类的对象何时传到Finalizer.register方法呢?对象的创建其实是被拆分成多个步骤的,先执行new分配好对象空间,然后再执行invokespecial调用构造函数,jvm里其实可以让用户选择在这两个时机中的任意一个将当前对象传递给Finalizer.register方法来注册到Finalizer对象链里。另外需要提一点的是当通过clone的方式复制一个对象的时候,如果当前类是一个finalizer类,那么在clone完成的时候将调用Finalizer.register方法进行注册。

       一个构造函数执行的时候,会去调用父类的构造函数,主要是为了能对继承自父类的属性也能做初始化,那么任何一个对象的初始化最终都会调用到Object的空构造函数里,任何空的构造函数其实并不空,会含有三条字节码指令,如下:
0: aload_0
1: invokespecial #21                 // Method java/lang/Object."<init>":()V
4: return
       为了不对所有的类的构造函数都做埋点调用Finalizer.register方法,hotspot的实现是在Object这个类在做初始化的时候将构造函数里的return指令替换为_return_register_finalizer指令,该指令并不是标准的字节码指令,是hotspot扩展的指令,这样在处理该指令的时候调用Finalizer.register方法,这样就在侵入性很小的情况下完美地在构造函数执行完毕后调用Finalizer.register。

finalizer类对象的GC回收

       在Finalizer类的clinit方法(静态块)里会创建了一个FinalizerThread的守护线程,这个线程的优先级并不是最高的,意味着在cpu很紧张的情况下其被调度的优先级可能会受到影响。
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
if (running)
return;
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer();
} catch (InterruptedException x) {
continue;
}
}
}
}

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();
}
       这个线程主要就是从queue里取Finalizer对象,然后执行该对象的runFinalizer方法,这个方法主要是将Finalizer对象从Finalizer对象链里剥离出来,这样意味着下次gc发生的时候就可能将其关联的finalizer对象gc掉了,最后将这个Finalizer对象关联的finalizer对象传给了一个native方法invokeFinalizeMethod。invokeFinalizeMethod方法其实就是调了这个finalizer对象的finalize方法。

       如果在finalizer对象的finalize方法里重新将当前对象赋值出去,变成可达对象,当这个finalizer对象再次变成不可达的时候还会被执行finalize方法吗?答案是否定的,因为在执行完第一次finalize方法之后,这个finalizer对象已经和之前的Finalizer对象关系剥离了,也就是下次gc的时候不会再发现Finalizer对象指向该finalizer对象了,自然也就不会调用这个finalizer对象的finalize方法了。

       当gc发生的时候,gc算法会判断finalizer类对象是不是只被Finalizer类引用(finalizer类对象被Finalizer对象引用,然后放到Finalizer对象链里),如果这个类仅仅被Finalizer对象引用的时候,说明这个对象在不久的将来会被回收了现在可以执行它的finalize方法了,于是会将这个Finalizer对象放到Finalizer类的ReferenceQueue里,但是这个finalizer类对象其实并没有被回收,因为Finalizer这个类还对他们持有引用,在gc完成之前,jvm会调用ReferenceQueue里的lock对象的notify方法(当ReferenceQueue为空的时候,FinalizerThread线程会调用ReferenceQueue的lock对象的wait方法直到被jvm唤醒),此时就会执行上面FinalizeThread线程里看到的其它逻辑了。

Finalizer导致的内存泄露

       这里举一个简单的例子,使用挺广的socket通信,SocksSocketImpl的父类其实就实现了finalize方法:
/**
* Cleans up if the user forgets to close it.
*/
protected void finalize() throws IOException {
close();
}
       其实这么做的主要目的是万一用户忘记关闭socket了,那么在这个对象被回收的时候能主动关闭socket来释放一些系统资源,但是如果真的是用户忘记关闭了,那这些socket对象可能因为FinalizeThread迟迟没有执行到这些socket对象的finalize方法,而导致内存泄露,这种问题碰到过多次,需要特别注意的是对于已经没有地方引用的这些finalizer对象,并不会在最近的那一次gc里马上回收掉,而是会延迟到下一个或者下几个gc时才被回收,因为执行finalize方法的动作无法在gc过程中执行,万一finalize方法执行很长呢,所以只能在这个gc周期里将这个垃圾对象重新标活,直到执行完finalize方法从queue里删除,这样下次gc的时候就真的是漂浮垃圾了会被回收,因此建议是千万不要在运行期不断创建finalizer对象,不然会很悲剧。

总结

       finalizer对象因为Finalizer的引用而变成了一个临时的强引用,即使没有其他的强引用了,还是无法立即被回收;

       finalizer对象至少经历两次GC才能被回收,因为只有在FinalizerThread执行完了finalizer对象的finalize方法的情况下才有可能被下次gc回收,而有可能期间已经经历过多次gc了,但是一直还没执行finalizer对象的finalize方法;

       cpu资源比较稀缺的情况下FinalizerThread线程有可能因为优先级比较低而延迟执行finalizer对象的finalize方法;

       因为finalizer对象的finalize方法迟迟没有执行,有可能会导致大部分finalizer对象进入到old分代,此时容易引发old分代的gc,甚至fullgc,gc暂停时间明显变长;

       finalizer对象的finalize方法被调用了,但是这个对象其实还并没有被回收,虽然可能在不久的将来会被回收。

补充:

       对象因为Finalizer的引用而变成了一个临时的强引用,即使没有其他的强引用,还是无法立即被回收;

       对象至少经历两次GC才能被回收,因为只有在FinalizerThread执行完了f对象的finalize方法的情况下才有可能被下次GC回收,而有可能期间已经经历过多次GC了,但是一直还没执行对象的finalize方法;

       CPU资源比较稀缺的情况下FinalizerThread线程有可能因为优先级比较低而延迟执行对象的finalize方法;

       因为对象的finalize方法迟迟没有执行,有可能会导致大部分f对象进入到old分代,此时容易引发old分代的GC,甚至Full GC,GC暂停时间明显变长,甚至导致OOM;

       对象的finalize方法被调用后,这个对象其实还并没有被回收,虽然可能在不久的将来会被回收。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: