JAVA FinalReference
引入
使用MAT分析dump出的内存时,常会看到java.lang.ref.Finalizer占用内存也不小,比较纳闷我们在编程中并没有用到这个东西,为什么他会出现并且占用分量不算小的一部分内存呢?
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()方法。
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,他的一生也就结束了。
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方法被调用后,这个对象其实还并没有被回收,虽然可能在不久的将来会被回收。
举个例子
/**
* -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() });
}
}
}
}