Java中的Finalizer类以及GC二次标记过程中的Java源码解析

FinalReference、Finalizer、ReferenceQueue、Reference均位于java.lang.ref包中,与对象引用和垃圾回收有关。

1 Finalizer类结构概述

  先看Finalizer的父类:

class FinalReference<T> extends Reference<T> {

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

  它的父类继承了Reference,这表明Finalizer可以使用Reference的字段和方法该类,并且该类访问权限是package的,我们不能直接去对其进行继承扩展,只是在JDK里的Finalizer类继承了该类。
  下面看看Finalizer类部分源码:

final class Finalizer extends FinalReference<Object> {
    //关联的队列,将会在第一次加载类时初始化。Finalizer引用的对象被gc之前,jvm会把相应的Finalizer对象放入队列。
    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();

    //静态的Finalizer对象链。
    private static Finalizer unfinalized = null;

    //作为add、remove时获取的锁
    private static final Object lock = new Object();

    //该节点前后节点
    private Finalizer next = null, prev = null;

    /**
     * 私有构造方法
     *
     * @param finalizee FinalReference指向的对象引用
     */
    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        //将当前对象插入到Finalizer对象链里,链里的对象和Finalizer类静态相关联,在这个链里的对象都无法被gc掉,除非将这种引用关系剥离掉(因为Finalizer类是无法被正常卸载的)
        add();
    }

    /**
     * 添加到Finalizer对象链里
     */
    private void add() {
        //获取锁
        synchronized (lock) {
            //如果不是第一个Finalizer对象
            if (unfinalized != null) {
                //当前节点的下一个节点就是unfinalized指向的节点
                this.next = unfinalized;
                //unfinalized指向的节点的上一个节点就是当前节点
                unfinalized.prev = this;
            }
            //unfinalized指向自己,这样只保存后进的Finalizer对象引用。
            unfinalized = this;
        }
    }


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

  我们可以看到,构造器是私有的,说明不希望我们来创建该类对象,而是由JVM自己来创建。Jvm会主动调用register方法,同时将这个对象加入到Finalizer对象链里,unfinalized指向最新加的Finalizer对象。

2 register方法的调用

  在JVM对对象初始化过程中会判断两个字段标志has_finalizer_flag和RegisterFinalizersAtInit,来表示这个类是否是一个finalizer类,简称f类,gc在处理这种类的对象的时候要做一些特殊的处理,如在这个对象被回收之前会调用一下它的finalize方法。
  当然用Java代码来说,判断当前类是否是一个f类的标准很简单,如果类重写了finalize方法,且方法体不为空,那么该类就算一个f类。比如我们的Object类虽然含有一个finalize方法,但是并不是一个f类,Object的对象在被gc回收的时候其实并不会去调用它的finalize方法。我们的类在被加载过程中其实就已经被标记为是否为f类了。然后会调用register_finalizer函数将该类注册到Finalizer中,register_finalizer函数对应的就是Java中的Finalizer.register方法。
  对象的创建其实是被拆分成多个步骤的,最简单的构造器应该包含三步:
在这里插入图片描述

第一条指令的意思是根据类型分配一块内存区域
第二条指令是把第一条指令返回的内存地址压入操作数栈顶
第三条指令是调用类的构造函数,对字段进行显示初始化操作。

  jvm里其实可以让用户选择在这两个时机中的任意一个将当前对象传递给Finalizer.register方法来注册到Finalizer对象链里,这个选择依赖于RegisterFinalizersAtInit这个vm参数是否被设置,默认值为true,也就是在调用构造函数返回之前调用Finalizer.register方法,如果通过-XX:-RegisterFinalizersAtInit关闭了该参数,那将在对象空间分配好之后就将这个对象注册进去。
  通过clone的方式复制一个对象的时候,不会有第三条指令,如果当前类是一个f类,那么在clone完成的时候将调用Finalizer.register方法进行注册。

3 finalize类的回收

/**
 * Finalizer的静态块,初始化finalizer垃圾回收线程
 * 一个JVM实例至少有一个垃圾回收线程.
 */
static {
    ThreadGroup tg = Thread.currentThread().getThreadGroup();
    //创建finalizer垃圾回收线程
    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的静态内部类
 */
private static class FinalizerThread extends Thread {
    private volatile boolean running;

    FinalizerThread(ThreadGroup g) {
        //命名为Finalizer
        super(g, "Finalizer");
    }

    /**
     * run方法中,循环从queue里取Finalizer对象,然后执行该对象的runFinalizer方法
     * 这个方法主要是将Finalizer对象从Finalizer对象链里剥离出来,这样意味着下次gc发生的时候就可能将其关联的f对象gc掉了,
     * 最后将这个Finalizer对象关联的f对象传给了一个native方法invokeFinalizeMethod
     */
    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;
        /*循环从queue里取Finalizer对象,然后执行该对象的runFinalizer方法*/
        for (; ; ) {
            try {
                Finalizer f = (Finalizer) queue.remove();
                f.runFinalizer(jla);
            } catch (InterruptedException x) {
                // ignore and continue    runFinalizer方法里对Throwable的异常都进行了捕获,不会出现FinalizerThread因异常未捕获而退出的情况。
            }
        }
    }
}

/**
 * Finalizer的runFinalizer方法
 * 这个方法主要是将Finalizer对象从Finalizer对象链里剥离出来,这样意味着下次gc发生的时候就可能将其关联的f对象GC掉了。
 * 最后将这个Finalizer对象关联的f对象传给了一个native方法invokeFinalizeMethod
 * 其实invokeFinalizeMethod方法就是调了这个f对象的finalize方法,因此在该方法里可以对对象进行一些监控或者”复活"
 * @param jla
 */
private void runFinalizer(JavaLangAccess jla) {
    /*1 将Finalizer对象从Finalizer对象链里剥离出来*/
    synchronized (this) {
        //如果为true,则直接返回,不会执行后面的方法了,包括finalize,这样该对象直接被回收
        if (hasBeenFinalized()) return;
        //否则,该对象是否还在链表中,将该Finalizer对象从链表中删除,这样下次gc时就可以把原对象给回收掉了。
        remove();
    }
    /*2 执行一次finalize,清除关联的引用,下次直接回收*/
    try {
        //get方法获取关联的引用对象
        Object finalizee = this.get();
        if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
            jla.invokeFinalize(finalizee);
            //清空该引用,方便下次直接回收。
            finalizee = null;
        }
    } catch (Throwable x) { }
    //调用Reference的clear方法,将referent的值置为null,方便回收
    super.clear();
}

/**判断该对象是否还在链表中
 * 如果next==this,则返回true,不再链表中;否则返回false,在链表中。
 * @return
 */
private boolean hasBeenFinalized() {
    return (next == this);
}

/**
 * 移除当前Finalizer对象
 */
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;
    }
}

  在Finalizer类的静态块中我们看到它会创建了一个名为Finalizer的FinalizerThread类型的守护线程,这个线程的优先级并不是最高的,意味着在cpu很紧张的情况下其被调度的优先级可能会受到影响。这个线程实际上就是该JVM实例的垃圾回收线程
  该线程被创建之后立即执行了start方法运行起来,在run方法中,会循环从queue里取Finalizer对象,然后执行该对象的runFinalizer方法。
  runFinalizer主要是将Finalizer对象从Finalizer对象链里剥离出来,通过hasBeenFinalized方法判断该对象是否还在链表中,并将该Finalizer对象从链表中删除,这样下次gc时就可以把原对象给回收掉了
  如果hasBeenFinalized返回true,说明不在链表中,直接返回,不会执行后面的方法了,包括finalize,这样该对象直接被回收;如果hasBeenFinalized返回false,先解除链表的关系,然后将这个Finalizer对象关联的f对象传给了一个native方法invokeFinalizeMethod,并清除相关引用,下次就直接GC回收。
  其实invokeFinalizeMethod方法就是调了这个f对象的finalize方法,因此在该方法里可以对对象进行一些监控或者”复活"。

4 Finalizer对象插入到ReferenceQueue队列的时机

  上面说到,Finalizer线程会循环从queue里取Finalizer对象,那么Finalizer对象设么时候加入到queue队列中的呢?Finalizer对象的加入有以下几步:

  1. Reference类中的静态块在类加载时,会初始化handler线程,由于是Finalizer的父类,在Finalizer加载时,会加载Reference类,handler线程会循环执行tryHandlePending方法。tryHandlePending方法执行时,如果pending为空,会调用lock.wait(),释放锁对象并让该handler线程进入阻塞状态。
  2. 当ReferenceQueue为空的时候,Finalizer线程也会调用ReferenceQueue的lock对象的wait方法进入等待,直到被JVM唤醒。
  3. 当GC发生的时候,GC算法会判断f类对象是不是只被Finalizer类引用(f类对象被Finalizer对象引用,然后放到Finalizer对象链里),如果这个类仅仅被Finalizer对象引用的时候,说明这个对象在不久的将来会被回收,现在可以执行它的finalize方法,于是会将这个Finalizer对象放到Finalizer类的ReferenceQueue里:JVM会把Finalizer对象赋给Reference的pending属性,并调用lock. notifyAll ()。
  4. 一旦JVM给pending赋值并调用了lock.notify(),ReferenceHandler线程将被唤醒,将Finalizer对象加入ReferenceQueue,此时ReferenceQueue不为空。
  5. Finalizeer线程被唤醒后,说明有数据了,唤醒之后此时就会执行上面Finalizeer线程里看到的其他逻辑了,即queue.remove()获取一个Finalizer对象来执行finalize方法。

部分源码如下:

public abstract class Reference<T> {
    static private class Lock { }
    private static Lock lock = new Lock();

    private static Reference<Object> pending = null;

    private static class ReferenceHandler extends Thread {
        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }
        //...
    }

    /**
     * Reference静态块,在类加载时,初始化handler线程;在Finalizer加载时,会加载Reference类,并循环执行tryHandlePending方法
     */
    static {
        //...
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
        //...
    }

    static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        //...
        if (pending != null) {
        //...
        } else {
            //否则等待
            if (waitForNotify) { lock.wait(); }
            return waitForNotify;
        }
       // ...
        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

}

5 总结

  在两次标记过程中。Finalizer线程会对即将被回收的对象(第一次标记)执行finalize方法,第二次标记时,就直接GC回收了。
  有些人喜欢在finalize中写一些代码比如对象监控,释放资源等,但是由于Finalizer线程的优先级很低,而且默认的单线程的,可能会导致相应的finalize方法并不会立即执行、或者由于某个finalize的方法执行时间过长而导致任务积压,此时对象由于具有referent引用,也无法被立即GC回收,至少要经历两次GC(两次标记)才会回收,而因为长时间不能回收但是代码中我们用不到了该对象,那么就导致内存泄漏,进而可能导致OOM,因此,建议不要重写finalize方法,对于资源的关闭等操作,建议使用try-finally操作。
  java.lang.ref包中的其他类,比如各种引用比如强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)的简介,可看这篇文章:Java中的四种对象引用详解和案例演示

发布了58 篇原创文章 · 获赞 105 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43767015/article/details/105228733