引用以及Reference类

2020年3月31日 | 作者 Siran | 9600字 | 阅读大约需要19分钟
归档于 JVM | 标签 #JVM

简述

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。

  • 在JDK 1.2版之前,Java里面的引用是很传统的定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。

    这种定义并没有什么不对,只是现在看来有些过于狭隘了,一个对象在这种定义下只有“被引用”或者“未被引用”两种状态,对于描述一些“食之无味,弃之可惜”的对象就显得无能为力。譬如我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象——很多系统的缓存功能都符合这样的应用场景。

  • 在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)软引用(Soft Reference)弱引用(Weak Reference)虚引用(PhantomReference)4种,这4种引用强度依次逐渐减弱。

    • 强引用:普通的的引用类型,new一个对象默认得到的引用就是强引用,只要对象存在强引用,就不会被GC。

    • 软引用:相对较弱的引用,垃圾回收器会在内存不足时回收弱引用指向的对象。JVM会在抛出OOME前清理所有弱引用指向的对象,如果清理完还是内存不足, 才会抛出OOME。所以软引用一般用于实现内存敏感缓存。

    • 弱引用:更弱的引用类型,垃圾回收器在GC时会回收此对象,也可以用于实现缓存,比如JDK提供的WeakHashMap。

    • 虚引用:一种特殊的引用类型,不能通过虚引用获取到关联对象,只是用于获取对象被回收的通知。


假死亡的情况

即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程

  • 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记
  • 随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。
  • 假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。只会执行一次

如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。

这里所说的“执行”是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。

所以针对这种情况JVM不会等待。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。

从下面代码中我们可以看到一个对象的finalize()被执行,但是它仍然可以存活。

public class FinalizeEscapeGC{
  public static FinalizeEscapeGC SAVE_HOOK = null;
  
  public void isAlive(){
    System.out.println("yes, i am still alive");
  }
  
  @Override
  protected void finalize() throws Throwable{
    super.finalize();
    System.out.println("finalize method executed! ");
    FinalizeEscapeGC.SAVE_HOOK = this;
  }
  
  public static void main(String[] args) throws Throwable{
        SAVE_HOOK = new FinalizeEscapeGC();
        //<1> 对象第一次拯救自己  成功
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null)
            SAVE_HOOK.isAlive();
        else {
            System.out.println("no, i am dea :(");
        }

        //<2> 对象第二次拯救自己 失败
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null)
            SAVE_HOOK.isAlive();
        else {
            System.out.println("no, i am dea :(");
        }
  }
}

输出

finalize method executed! 
yes, i am still alive
no, i am dea :(

可以看到,SAVE_HOOK对象的finalize()方法确实被垃圾收集器触发过,并且在被收集前成功逃脱了。

另外一个值得注意的地方就是,代码中有两段完全一样的代码片段,

  • 执行结果却是一次逃脱成功,一次失败了。这是因为任何一个对象的finalize()方法都只会被系统自动调用一次
  • 如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。

这也验证了在上面分析的假死亡的情况,finalize() 只会被调用一次。

不建议使用finalize()方法:运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。

有了上面的关于 引用finalize 方法的一些理论,下面让我们来看一下 Java 中关于引用和 finalize方法的具体实现以及逻辑。


Java有5种类型的可达性:

  • 强可到达,如果从GC Root搜索后,发现对象与GC Root之间存在强引用链则为强可到达。强引用链即有强引用对象,引用了该对象。
  • 软可到达,如果从GC Root搜索后,发现对象与GC Root之间不存在强引用链,但存在软引用链,则为软可到达。软引用链即有软引用对象,引用了该对象。
  • 弱可到达,如果从GC Root搜索后,发现对象与GC Root之间不存在强引用链与软引用链,但有弱引用链,则为弱可到达。弱引用链即有弱引用对象,引用了该对象。
  • 虚可到达,如果从GC Root搜索后,发现对象与GC Root之间只存在虚引用链则为虚可到达。虚引用链即有虚引用对象,引用了该对象。
  • 不可达,如果从GC Root搜索后,找不到对象与GC Root之间的引用链,则为不可到达。

根据上图的例子中,可以得到以下的结论:

  • Object A为强可到达,Object B也为强可到达,虽然Object B对象被SoftReference Objcet E 引用但由于其还被Object A引用所以为强可到达;
  • 而Object C和Object D为弱引用达到,虽然Object D对象被PhantomReference Objcet G引用但由于其还被Object C引用,而Object C又为弱引用达到,所以Object D为弱引用达到;
  • 而Object H与Object I是不可到达
  • 引用链的强弱有关系依次是 强引用 > 软引用 > 弱引用 > 虚引用,如果有更强的引用关系存在,那么引用链到达性,将由更强的引用有关系决定。

源码分析

类图

可以看到的是除了软引用(Soft Reference)弱引用(Weak Reference)虚引用(PhantomReference)多了一个

FinalReference 这个引用而Finalizer 继承于它,所以可以知道 这个引用就是用来处理上面所说的finalizer()方法的。

所有的子类引用都继承于 Reference 类,先来分析一下此类。


Reference

JVM在GC时如果当前对象只被Reference对象引用,JVM会根据Reference具体类型与堆内存的使用情况决定是否把对应的Reference对象加入到一个由Reference构成的pending链表上,如果能加入pending链表JVM同时会通知ReferenceHandler线程进行处理。ReferenceHandler线程是在Reference类被初始化时调用的,其是一个守护进程并且拥有最高的优先级。Reference类静态初始化块代码如下:

static {
    //省略部分代码...
    Thread handler = new ReferenceHandler(tg, "Reference Handler");
    handler.setPriority(Thread.MAX_PRIORITY);
    handler.setDaemon(true);
    handler.start();
    //省略部分代码...
}

ReferenceHandle线程

ReferenceHandler线程内部的run方法会不断地从Reference构成的pending链表上获取Reference对象,如果能获取则根据Reference的具体类型进行不同的处理,不能则调用wait方法等待GC回收对象处理pending链表的通知。ReferenceHandler线程run方法源码:

public void run() {
    //死循环,线程启动后会一直运行
    while (true) {
        tryHandlePending(true);
    }
}


static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        //上锁
        synchronized (lock) {
            //<1> 如果JVM 发现有满足回收的对象,则会加入Reference 中的 pending链表。
            //    这里会进行判断,如果有则取出来 并在pending链表上断开
            if (pending != null) {
                r = pending;
                //instanceof 可能会抛出OOME,所以在将r从pending链上断开前,做这个处理
                c = r instanceof Cleaner ? (Cleaner) r : null;
                //将将r从pending链上断开
                pending = r.discovered;
                r.discovered = null;
            //<2> 目前还没有,则wait等待
            } else {
                //等待CG后的通知
                if (waitForNotify) {
                    lock.wait();
                }
                //重试
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        //当抛出OOME时,放弃CPU的运行时间,这样有希望收回一些存活的引用并且GC能回收部分空间。同时能避免频繁地自旋重试,导致连续的OOME异常
        Thread.yield();
        //重试
        return true;
    } catch (InterruptedException x) {
         //重试
        return true;
    }
    //<3> 在上面的pending链表中取出来的结果,如果是Cleaner类型的Reference调用其clean方法并退出
    if (c != null) {
        c.clean();
        return true;
    }
    ReferenceQueue<? super Object> q = r.queue;
    //<4> 如果Reference有注册ReferenceQueue,则处理pending指向的Reference结点将其加入ReferenceQueue中
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}
  • <1> 如果JVM 发现有满足回收的对象,则会加入Reference 中的 pending链表。 这里会进行判断,如果有则取出来 并在pending链表上断开
  • <2> 目前pending链表上为空,则wait等待
  • <3> 在上面的pending链表中取出来的结果,如果是Cleaner类型的Reference调用其clean方法并退出
  • <4> 如果Reference有注册ReferenceQueue,则处理pending指向的Reference结点将其加入ReferenceQueue中

上面tryHandlePending方法中比较重要的点是c.clean()q.enqueue

  • Cleaner#clean 方法用于完成清理工作
  • q.enqueue 则是ReferenceQueue将被回收对象加入到对应的Reference列队中,等待其他线程的后继处理。

上面就是Reference的整理流程图,接下来回头看一下Reference类的属性、构造方法以及一些重要的方法


构造方法

//构造函数,指定引用的对象referent
Reference(T referent) {
    this(referent, null);
}
//构造函数,指定引用的对象referent与注册的queue
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

enqueue 方法

在上面流程图中,如果Reference#tryHandlePendingpending链表中取出的Reference对象不是Clean类型的,则调用此方法加入ReferenceQueue中,等待其他线程的后续处理。

//Reference

//将当前对象加入创建时注册的queue中
public boolean enqueue() {
    return this.queue.enqueue(this);
}

//ReferenceQueue

//入列队enqueue方法,只被Reference类调用,也就是上面分析中ReferenceHandler线程为调用
boolean enqueue(Reference<? extends T> r) {
	//获取同步对象lock对应的监视器对象
    synchronized (lock) {
        //获取r关联的ReferenceQueue,如果创建r时未注册ReferenceQueue则为NULL,同样如果r已从ReferenceQueue中移除其也为null
        ReferenceQueue<?> queue = r.queue;
        //判断queue是否为NULL 或者 r已加入ReferenceQueue中,是的话则入队列失败
        if ((queue == NULL) || (queue == ENQUEUED)) {
            return false;
        }
        assert queue == this;
        //设置r的queue为已入队列
        r.queue = ENQUEUED;
        //如果ReferenceQueue头节点为null则r的next节点指向当前节点,否则指向头节点
        r.next = (head == null) ? r : head;
        //更新ReferenceQueue头节点
        head = r;
        //列队长度加1
        queueLength++;
        //为FinalReference类型引用增加FinalRefCount数量
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(1);
        }
        //通知remove操作队列有节点
        lock.notifyAll();
        return true;
    }
}

poll方法以及remove方法

ReferenceQueue有入队方法enqueue方法,必然也是有获取和删除方法。

public Reference<? extends T> poll() {
    //头结点为null直接返回,代表Reference还没有加入ReferenceQueue中
    if (head == null)
        return null;
    //获取同步对象lock对应的监视器对象
    synchronized (lock) {
        return reallyPoll();
    }
}
//从队列中真正poll元素的方法
private Reference<? extends T> reallyPoll() {
    Reference<? extends T> r = head;
    //double check 头节点不为null
    if (r != null) {
    	//保存头节点的下个节点引用
        Reference<? extends T> rn = r.next;
        //更新queue头节点引用
        head = (rn == r) ? null : rn;
        //更新Reference的queue值,代表r已从队列中移除
		r.queue = NULL;
		//更新Reference的next为其本身
        r.next = r;
        queueLength--;
        //为FinalReference节点FinalRefCount数量减1
        if (r instanceof FinalReference) {
            sun.misc.VM.addFinalRefCount(-1);
        }
        //返回获取的节点
        return r;
    }
    return null;
}


public Reference<? extends T> remove(long timeout) throws IllegalArgumentException, InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("Negative timeout value");
    }
    //获取同步对象lock对应的监视器对象
    synchronized (lock) {
    	//获取队列头节点指向的Reference
        Reference<? extends T> r = reallyPoll();
        //获取到返回
        if (r != null) return r;
        long start = (timeout == 0) ? 0 : System.nanoTime();
        //在timeout时间内尝试重试获取
        for (;;) {
        	//等待队列上有结点通知
            lock.wait(timeout);
            //获取队列中的头节点指向的Reference
            r = reallyPoll();
            //获取到返回
            if (r != null) return r;
            if (timeout != 0) {
                long end = System.nanoTime();
                timeout -= (end - start) / 1000_000;
                //已超时但还没有获取到队列中的头节点指向的Reference返回null
                if (timeout <= 0) return null;
                start = end;
            }
        }
    }
}

Reference 以及ReferenceQueue到这里是分析完了。

总结一下:

  • JVM在GC时如果当前对象只被Reference对象引用,JVM会根据Reference具体类型与堆内存的使用情况决定是否把对应的Reference对象加入到一个由Reference构成的pending链表
  • 如果能加入pending链表JVM同时会通知ReferenceHandler线程进行处理。ReferenceHandler线程收到通知后会调用Cleaner#cleanReferenceQueue#enqueue方法进行处理

现在就差一个Cleaner 还没有分析。来看一下Cleaner的实现


Cleaner 属性

//继承了PhantomReference类也就是虚引用,PhantomReference源码很简单只是重写了get方法返回null
public class Cleaner extends PhantomReference<Object> {
	/* 虚队列,命名很到位。之前说CG把ReferenceQueue加入pending-Reference链中后,ReferenceHandler线程在处理时
     * 是不会将对应的Reference加入列队的,而是调用Cleaner.clean方法。但如果Reference不注册ReferenceQueue,GC处理时
     * 又无法把他加入到pending-Reference链中,所以Cleaner里面有了一个dummyQueue成员变量。
     */
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
    //Cleaner链表的头结点
    private static Cleaner first = null;
    //当前Cleaner节点的后续节点
    private Cleaner next = null;
    //当前Cleaner节点的前续节点
    private Cleaner prev = null;
    //真正执行清理工作的Runnable对象,实际clean内部调用thunk.run()方法
    private final Runnable thunk;
    //省略部分代码...
}


Cleaner 构造函数以及方法

//私有方法,不能直接new
private Cleaner(Object var1, Runnable var2) {
    super(var1, dummyQueue);
    this.thunk = var2;
}
//创建Cleaner对象,同时加入Cleaner链中。
public static Cleaner create(Object var0, Runnable var1) {
    return var1 == null ? null : add(new Cleaner(var0, var1));
}
//头插法将新创意的Cleaner对象加入双向链表,synchronized保证同步
private static synchronized Cleaner add(Cleaner var0) {
    if (first != null) {
        var0.next = first;
        first.prev = var0;
    }
    //更新头节点引用
    first = var0;
    return var0;
}

public void clean() {
	//从Cleaner链表中先移除当前节点
    if (remove(this)) {
        try {
        	//调用thunk.run()方法执行对应清理逻辑
            this.thunk.run();
        } catch (final Throwable var2) {
           //省略部分代码..
        }

    }
}

Cleaner.clean中的run方法有具体的子类实现。


DirectByteBuffer 堆外内存回收

在创建DirectByteBuffer时我们实际是调用ByteBuffer#allocateDirect方法,而其实现如下:

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

DirectByteBuffer(int cap) {
    //省略部分代码...
    try {
    	//调用unsafe分配内存
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
       //省略部分代码...
    }
    //省略部分代码...
    //<1> 前面分析中的Cleaner对象创建,持有当前DirectByteBuffer的引用
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

  • <1> 里面和DirectByteBuffer堆外内存回收相关的代码便是Cleaner.create(this, new Deallocator(base, size, cap))这部分。还记得之前说实际的清理逻辑是里面和DirectByteBuffer堆外内存回收相关的代码便是Cleaner里面的Runnable#run方法吗?直接看Deallocator.run方法源码:
public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    //通过unsafe.freeMemory释放创建的堆外内存
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}
  • 创建DirectByteBuffer对象时会创建一个Cleaner对象,Cleaner对象持有了DirectByteBuffer对象的引用
  • 当JVM在GC时,如果发现DirectByteBuffer被地方法没被引用了,JVM会将其对应的Cleaner加入到pending-reference链表中,同时通知ReferenceHandler线程处理,ReferenceHandler收到通知后,会调用Cleaner#clean方法,而对于DirectByteBuffer创建的Cleaner对象其clean方法内部会调用unsafe.freeMemory释放堆外内存。

WeakHashMap敏感内存回收

WeakHashMap实现上其Entry继承了WeakReference。只要发生GC 就会清除

//Entry继承了WeakReference, WeakReference引用的是Map的key
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;
    /**
     * 创建Entry对象,上面分析过的ReferenceQueue,这个queue实际是WeakHashMap的成员变量,
     * 创建WeakHashMap时其便被初始化 final ReferenceQueue<Object> queue = new ReferenceQueue<>()
     */
    Entry(Object key, V value,
          ReferenceQueue<Object> queue,
          int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash  = hash;
        this.next  = next;
    }
    //省略部分原码...
}

  • WeakHashMap添加元素时,实际都会调用Entry的构造方法,也就是会创建一个WeakReference对象,这个对象的引用的是WeakHashMap刚加入的Key
  • 而所有的WeakReference对象关联在同一个ReferenceQueue上。我们上面说过JVM在GC时,如果发现当前对象只有被WeakReference对象引用,那么会把其对应的WeakReference对象加入到pending-reference链表上,并通知ReferenceHandler线程处理
  • ReferenceHandler线程收到通知后,对于WeakReference对象会调用ReferenceQueue#enqueue方法把他加入队列里面。
  • WeakHashMap 则通过expungeStaleEntries方法不断的从ReferenceQueue中取出Reference,然后进行清除
private void expungeStaleEntries() {
    //不断地从ReferenceQueue中取出,那些只有被WeakReference对象引用的对象的Reference
    for (Object x; (x = queue.poll()) != null; ) {
        synchronized (queue) {
            //转为 entry
            Entry<K,V> e = (Entry<K,V>) x;
            //计算其对应的桶的下标
            int i = indexFor(e.hash, table.length);
            //取出桶中元素
            Entry<K,V> prev = table[i];
            Entry<K,V> p = prev;
            //桶中对应位置有元素,遍历桶链表所有元素
            while (p != null) {
                Entry<K,V> next = p.next;
                //如果当前元素(也就是entry)与queue取出的一致,将entry从链表中去除
                if (p == e) {
                    if (prev == e)
                        table[i] = next;
                    else
                        prev.next = next;
                    // Must not null out e.next;
                    //清空entry对应的value
                    e.value = null;
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

接下来分析一个 finalizer()方法被调用的过程。


finalizer()方法被调用的过程

JVM 中的实现

//share/vm/oops/instanceKlass.cpp
instanceOop InstanceKlass::allocate_instance(TRAPS) {
  bool has_finalizer_flag = has_finalizer(); // Query before possible GC
  int size = size_helper();  // Query before forming handle.

  KlassHandle h_k(THREAD, this);

  instanceOop i;

  i = (instanceOop)CollectedHeap::obj_allocate(h_k, size, CHECK_NULL);
  //<1> 如果该类复写 Object.finalizer() 方法 则把_has_finalizer_flag置为true
  if (has_finalizer_flag && !RegisterFinalizersAtInit) {
    i = register_finalizer(i, CHECK_NULL);
  }
  return i;
}

//然后会调用register_finalizer
instanceOop InstanceKlass::register_finalizer(instanceOop i, TRAPS) {
  if (TraceFinalizerRegistration) {
    tty->print("Registered ");
    i->print_value_on(tty);
    tty->print_cr(" (" INTPTR_FORMAT ") as finalizable", (address)i);
  }
  instanceHandle h_i(THREAD, i);
  // Pass the handle as argument, JavaCalls::call expects oop as jobjects
  JavaValue result(T_VOID);
  JavaCallArguments args(h_i);
  //<2> 这里就是调用Java里面Finalizer 类中的 register 方法
  methodHandle mh (THREAD, Universe::finalizer_register_method());
  JavaCalls::call(&result, mh, &args, CHECK_NULL);
  return h_i();
}
  • <1> 在上面的JVM 代码中//share/vm/oops/instanceKlass.cpp 在解析 class 的时候,如果这个类复写了 Object里面的finalizer方法的话,会把_has_finalizer 置为true 并且调用 register_finalizer()方法
  • <2> 这里会调用 Java里面Finalizer 类中的 register 方法

到这里就是JVM 发现如果一个类复写了finalizer方法就会调用Finalizer类里面的register方法,接下来看一下Finalizer类是如何实现的


Finalizer.register( )

/* Invoked by VM */
static void register(Object finalizee) {
        //<1> 创建实例
        new Finalizer(finalizee);
    }

private Finalizer(Object finalizee) {
        //<2> 放入父类中的ReferenceQueue中(Reference类)
        super(finalizee, queue);
        //<3> 所有的复写finalizer 方法的类组成一个双向链表 叫unfinalized
        add();
    }

private void add() {
        synchronized (lock) {
            if (unfinalized != null) {
                this.next = unfinalized;
                unfinalized.prev = this;
            }
            unfinalized = this;
        }
    }
  • <1> 可以看到Finalizer.register()方法中创建Finalizer的实例, Finalizer 类中的静态代码块。
  • <2> 放入父类中的ReferenceQueue中(Reference类)
  • <3> 所有的复写finalizer 方法的类组成一个双向链表 叫unfinalized
static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        //创建FinalizerThread 线程
        Thread finalizer = new FinalizerThread(tg);
        finalizer.setPriority(Thread.MAX_PRIORITY - 2);
        finalizer.setDaemon(true);
        finalizer.start();
    }


private static class FinalizerThread extends Thread {
        private volatile boolean running;
        FinalizerThread(ThreadGroup g) {
            super(g, "Finalizer");
        }
        public void run() {
            // in case of recursive call to 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;
            //死循环调用 runFinalizer方法
            for (;;) {
                try {
                    Finalizer f = (Finalizer)queue.remove();
                    f.runFinalizer(jla);
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }
        }
    }

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();
    }

可以看到在Finalizer 类中的静态代码块中创建了FinalizerThread线程,在线程中又死循环从ReferenceQueue中不断取Reference对象,取到之后调用invokeFinalize方法,这此方法就是调用该类的 finalizer 方法。


总结

  • 创建对象时,如果对象overridefinalize()方法,jvm会同时创建一个Finalizer对象
  • 所有Finalizer对象组成了一个双向链表
  • 所有Finalizer对象都有一个名为queue的成员变量,指向的都是Finalizer类的静态Queue。
  • cms gc执行到mark阶段的最后时,会把需要gc的对象加入到Reference的pending list中。
  • 有一个专门的高级别线程Reference Handler处理pending list,把pending list中的对象取出来,放到这个对象所指的Reference Queue中,对于Finalizer对象来说, 这个queue指向Finalizer类的静态Queue。
  • Finalizer类有一个专门的线程负责从queue中取对象,并且执行finalizer引用的对象的finalize函数。 根据cms来将如果一个对象override finalize()方法,那么并不意味着这个对象的会立马被gc给回收,在cms中会进行remark 重新标记,如果该对象原本是要被回收的但是又被引用了那么它就不会被回收,而finalize只会被调用一次,因为前面已经调用过了,所以不会在调用finalize方法,而产生问题。
  • DirectByteBuffer 创建实例的时候会创建Cleaner,通过Cleaner#run方法进行回收内存
  • WeakHashMap 操作expungeStaleEntries方法,不断的从ReferenceQueue中获取然后进行清除对应的Entry。

参考

《深入理解Java虚拟机》

Java Reference核心原理分析

finalize、Finalizer和Finalizer Queue的原理