0%

JDK源码 FinalReference

JAVA FinalReference

引入

使用MAT分析dump出的内存时,常会看到java.lang.ref.Finalizer占用内存也不小,比较纳闷我们在编程中并没有用到这个东西,为什么他会出现并且占用分量不算小的一部分内存呢?

1
2
3
4
final class Finalizer extends FinalReference {
private static ReferenceQueue queue = new ReferenceQueue();
//... ...
}

结合它的数据结构基本可以看出来,Finalizer中持有一个一个引用队列。猜测是这个队列吃掉了那些内存。

引用类型

Java开发不必关心内存的释放、申请和垃圾回收,这些事情都有JVM代劳,但是JVM依然提供了一些方式,让我们能够在应用的层次利用内存或者GC特性,从而更好的使用内存。Reference(引用)就是其中一种。

  • StrongReference(强引用)
    我们平时开发中new一个对象出来,这种引用便是强引用。 JVM 系统采用 Finalizer 来管理每个强引用对象 , 并将其被标记要清理时加入 ReferenceQueue, 并逐一调用该对象的 finalize() 方法。
  • SoftReference(软引用)
    当内存足够的时候,软引用所指向的对象没有其他强引用指向的话,GC的时候并不会被回收,当且只当内存不够时才会被GC回收(调用finalize方法)。强度仅次于强引用。
  • WeakReference(弱引用)
    弱引用指向的对象没有任何强引用指向的话,GC的时候会进行回收。
  • PhantomReference(虚引用)
    如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

TODO引用详细介绍单独在另一篇作介绍

java.lang.ref包简介

包结构

ref包下对应了Java中对应几种引用类型,改包下的类的可见性均为包内可见。FinalReference可以看作是强引用的一个对应。

FinalReference

FinalReference由JVM来实例化,VM会对那些实现了Object中finalize()方法的类实例化一个对应的FinalReference。
注意:实现的finalize方法体必须非空。

Finalizer

Finalizer是FinalReference的子类,该类被final修饰,不可再被继承,JVM实际操作的是Finalizer。当一个类满足实例化FinalReference的条件时,JVM会调用Finalizer.register()进行注册。(PS:后续讲的Finalizer其实也是在说FinalReference。)

何时注册(实例化FinalReference)

JVM在类加载的时候会遍历当前类的所有方法,包括父类的方法,只要有一个参数为空且返回void的非空finalize方法就认为这个类在创建对象的时候需要进行注册。

对象的创建其实是被拆分成多个步骤,注册的时机可以在为对象分配好内存空间后,也可以在构造函数返回之前,这个点由-XX:-RegisterFinalizersAtInit控制,这个参数默认为true,即:在构造函数返回之前调用。注册入口是Finalizer的register()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
final class Finalizer extends FinalReference {
private static ReferenceQueue queue = new ReferenceQueue();
private static Finalizer unfinalized = null;
private static final Object lock = new Object();

private Finalizer
next = null,
prev = null;

//构造一个对象链表,如图
/**
* +------+ prev +-----+ +-----+
*unfinalized | f3 | <----> | f2 | <----> | f1 |
* +------+ next +-----+ +-----+
**/
private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}

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

/* Invoked by VM 入口在这里 */
static void register(Object finalizee) {
new Finalizer(finalizee);
}

//...
}
何时进入ReferenceQueue

GC工作时,如果发现对象只被Finalizer类引用,说明他可以被回收了,那么就把该对象从对象链中取出,放入ReferenceQueue,并通知FinalizerThread去消费。也就是说,本次GC并不能回收掉这个对象占用的内存。

ReferenceQueue是个典型的生产消费队列,此处不在赘述,可看其源码,实现很简单。

FinalizerThread线程

在Finalizer类的clinit方法(静态块)里,会创建一个FinalizerThread守护线程,这个线程的优先级不是最高的,这就意味着在CPU很紧张的情况下其被调度的优先级可能会受到影响。

FinalizerThread业务很简单,从ReferenceQueue拿出Finalizer,执行finalize方法,并且忽略其抛出的所有异常。执行完毕后,该对象称为真正的垃圾对象,再次发生GC,他的一生也就结束了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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(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();
}
GC回收问题
  • 对象因为Finalizer的引用而变成了一个临时的强引用,即使没有其他的强引用,还是无法立即被回收;

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

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

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

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

举个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* -Xms4m -Xmx4m -XX:+PrintGCDetails -Xloggc:/Users/childe/logs/gc-f.log
* -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/childe/logs/oom-f.hprof
* Created by childe on 2017/3/31.
*/
public class Finalizable {
static AtomicInteger aliveCount = new AtomicInteger(0);

Finalizable() {
//如果注释掉改行,在GC日志中仅能看到简单的新生代GC,程序不会因为内存问题停止
//如果未注释,程序跑上几分钟就挂掉了,因为生产和消费的能力不对等。GC日志中大部分是Full GC。
aliveCount.incrementAndGet();
}

@Override
protected void finalize() throws Throwable {
Finalizable.aliveCount.decrementAndGet();
}

public static void main(String args[]) {
for (int i = 0;; i++) {
Finalizable f = new Finalizable();
if ((i % 100_000) == 0) {
System.out.format("After creating %d objects, %d are still alive.%n", new Object[] {i, Finalizable.aliveCount.get() });
}
}
}
}

finalizer的生存周期

转自http://blog.2baxb.me/archives/974

  1. 在创建对象时,如果对象override了finalize()方法,jvm会同时创建一个Finalizer对象
  2. 所有Finalizer对象组成了一个双向链表
  3. 所有Finalizer对象都有一个名为queue的成员变量,指向的都是Finalizer类的静态Queue。
  4. cms gc执行到mark阶段的最后时,会把需要gc的对象加入到Reference的pending list中。
  5. 有一个专门的高级别线程Reference Handler处理pending list,把pending list中的对象取出来,放到这个对象所指的Reference Queue中,对于Finalizer对象来说,这个queue指向Finalizer类的静态Queue。
  6. Finalizer类有一个专门的线程负责从queue中取对象,并且执行finalizer引用的对象的finalize函数。

Java引用类型可参见
http://benjaminwhx.com/2016/09/10/%E8%AF%A6%E8%A7%A3java-lang-ref%E5%8C%85%E4%B8%AD%E7%9A%844%E7%A7%8D%E5%BC%95%E7%94%A8/
https://www.ibm.com/developerworks/cn/java/j-lo-langref/
http://www.importnew.com/20468.html
http://www.infoq.com/cn/articles/cf-java-garbage-references

小生不才,以上如有描述有误的地方还望各位不吝赐教 !^_^!