Reference,ReferenceQueue及GC垃圾回收机制分析

前言

java在最开始设计的时候一个对象只存在被引用和没有被引用两种状态,如此设计在概念上会比较清晰,且垃圾回收的判断与实现也会比较简单。但是随着应用场景的增加,实际上,我们更希望存在这样的一类对象:当有足够的内存时,这些对象能够继续存活;而当内存空间不足需要进行垃圾回收,或者在进行了垃圾回收之后空间还是非常紧张,则可以抛弃这些对象。这种特性,可以在很多场景下发挥作用,例如缓存功能、对象存活周期监控、堆外内存释放等等。

在JDK1.2之后对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种类型的引用,通过与JVM GC的紧密协作赋予各种引用以不同的垃圾收集行为。接着又基于较弱的引用(Weak,Phantom)不会影响到被引用对象在gc中的生存周期这一特性,扩展并实现了诸如对象析构方法的支持,以及nio DirectByteBuffer被GC回收时自动内存释放等等功能。此外,一些著名的第三方类库也利用这种特性实现了自己定制的对象回收逻辑处理机制。

本篇就是探究一下引用类型的相关源代码实现及其与JVM GC协同工作的机制,并介绍两个开源框架中对Reference的定制使用场景。

1. Reference的说明

在java中,存在着强引用(=),软引用(SoftReference),弱引用(WeakReference),虚引用(PhantomReference)这4种引用类型。这些引用的类型与jvmgarbage collector对对象的回收行为密切相关:如果一个对象强引用可达,就一定不会被GC回收,哪怕jvm抛出OOM异常;而如果一个对象只有软引用可达,则虚机会保证在out of memory之前会回收该对象;如果一个对象只是弱引用或者虚引用可达,则下一次GC时候就会将其回收。弱引用和虚引用的区别是,当一个对象只是弱引用可达时,它在下一次GC来临之前还有抢救的余地,也就是说弱引用仍可以获得该被引用的对象,然后将其赋值给某一个GC Root可达的变量,此时它就“得救”了;而当一个对象只是虚引用可达时候,它已经无法被抢救了,因为虚引用无法得到被引用对象。

另外,Java还提供了ReferenceQueue用于在一个对象被gc回收掉的时候可以进行额外的处理。ReferenceQueue即是这样的一个队列,当一个对象被gc回收之后,其相应的包装类,即Reference对象会被放入队列中。我们可以从queue中获取到相应的对象信息,同时进行额外的处理。比如反向操作,数据清理等。

由上面的说明可知,我们可以使用WeakReference或者PhantomReference来观察某些对象被gc回收的动作,并额外执行某些逻辑,而不会影响到他们的生命周期。事实上,JDK中自己对NIO DirectByteBuffer在GC回收后其申请的直接内存释放功能,以及netty提供的ObjectCleaner对象清理机制,都是通过WeakReference或者PhantomReference的子类来实现的。

Reference及ReferenceQueue相关的使用代码可参照另外一篇文章《使用ReferenceQueue实现对ClassLoader垃圾回收过程的观察、以及由此引发的ClassLoader内存泄露的场景及排查过程》,此处不再赘述。

2. JDK Reference/ReferenceQueue实现相关代码解析

本文代码分析基于JDK1.8,在JDK9+之后,关于这部分代码会有不小的变动,比如Reference对象的状态发生了变化,对Finalizer析构对象的弃用等,但是万变不离其宗,理解整体的设计实现思想就可以了解JDK及JVM GC是如何合作实现各种不同引用类型对象的垃圾收集及其被回收之前的行为。

2.1 Reference及其状态说明

Reference是所有引用对象(除了强引用)的抽象类型,其定义了所有引用对象的公共操作。由于Reference引用对象与jvm的gc模块在实现上是紧密合作的,因此并不适合被直接继承并扩展。一个Reference对象会处于4个可能的内部状态之中:

  • Active: 未被gc回收的对象状态为Active。一旦gc检测到对象的可达性的变化为适合的状态后,它会将该对象状态转换为Pending或Inactive,视该对象是否注册了ReferenceQueue。当注册了一个ReferenceQueue时,gc将该Reference的状态置为Pending,且将其添加到一个pending reference list中
  • Pending: 一个处于pending reference list中的node(pending reference list是一个全局的list,其链表头为Reference.pending),等待Reference Handler常驻线程来处理,逐个执行其enqueue入列行为。未注册对应的ReferenceQueue队列的Reference对象不会处于该状态中
  • Enqueued: 已加入到关联的ReferenceQueue中的对象。当一个对象被从其ReferenceQueue中删除时,其状态就会变为Inactive。在创建时未注册对应的ReferenceQueue队列的Reference对象不会处于该状态中
  • Inactive: 什么都不会再做的对象。一旦一个对象变为Inactive,它的状态就再也不会变化了,等待被gc清理

Reference类持有的属性说明:

//该Reference关联的对象,该对象会被gc专门对待
private T referent 

//该Reference对象关联的队列
volatile ReferenceQueue queue

//已入自己注册的队列queue中的下一个对象(相同的queue)
volatile Reference next

//供jvm使用,用于将gc找到的active对象或pending list中的对象链接起来
transient private Reference discovered

//用于与gc collector同步使用,gc collector必须获得该lock才能开始collect回收,
//因此持锁以后应尽快做完、不分配新对象并避免调用用户代码
Lock lock

//这个是static类属性,全局唯一的,代表了pending list的链头。gc collector添加Reference对象到此list中,
//同时Reference Handler线程逐个将它们移出并处理(加入到queue中,或者直接执行其clean方法)。
//该链使用上述lock进行保护,并使用Reference对象的discovered属性链接起来
static Reference pending  
  

一个Reference的状态由next和queue属性共同确定如下:

  • Active状态:queue的值为ReferenceQueue(该对象注册的队列),或者ReferenceQueue.NULL(如果没有注册队列);next为null
  • Pending状态:queue的值为ReferenceQueue(该对象注册的队列);next值为this(由discovered类属性来将所有处于pending状态的Reference对象链接起来,共同构成一个头结点为Reference.pending的链)
  • Enqueued状态:queue的值为ReferenceQueue.ENQUEUED;next的值为入相同列的下一个对象,或者是this(如果仅有自己入列)
  • Inactive状态:queue的值为ReferenceQueue.NULL;next为自己this

为了确保一个并发执行的Garbage Collectors能发现活跃的Reference对象,而不用和应用线程(可能正在对这些对象应用enqueue(),通过显式调用Reference.enqueue())相互影响,garbage collectors会将发现了的Reference对象通过discovered属性链接起来。该discovered属性也用来在pending list中将pending的对象链接起来。

2.1.1 Reference Handler常驻线程

ReferenceHandler是一个高优先级的线程,在Reference类初始化时就启动了,是一个后台常驻的线程。用于不断的从Reference.pending代表的pending list中取出处于Pending状态的节点并判断,其会在一个循环中不断执行方法tryHandlePending(false),该方法代码为:

static boolean tryHandlePending(boolean waitForNotify) {

        Reference r;

        Cleaner c;

        try {

            synchronized (lock) {

                if (pending != null) {

                    r = pending;

                    // 'instanceof' might throw OutOfMemoryError sometimes

                    // so do this before un-linking 'r' from the 'pending' chain...

                    c = r instanceof Cleaner ? (Cleaner) r : null;

                    // unlink 'r' from 'pending' chain

                    pending = r.discovered;

                    r.discovered = null;

                } else {

                    // The waiting on the lock may cause an OutOfMemoryError

                    // because it may try to allocate exception objects.

                    if (waitForNotify) {

                        lock.wait();

                    }

                    // retry if waited

                    return waitForNotify;

                }

            }

        } catch (OutOfMemoryError x) {

            // Give other threads CPU time so they hopefully drop some live references

            // and GC reclaims some space.

            // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above

            // persistently throws OOME for some time...

            Thread.yield();

            // retry

            return true;

        } catch (InterruptedException x) {

            // retry

            return true;

        }



        // Fast path for cleaners

        if (c != null) {

            c.clean();

            return true;

        }



        ReferenceQueue q = r.queue;

        if (q != ReferenceQueue.NULL) q.enqueue(r);

        return true;

    } 
  

其代码逻辑描述如下:

  • 首先会在lock保护的临界区中从Reference.pending指向的pending list中取出下一个状态为Pending的引用节点,并判断其类型是否为Cleaner
  • 若pending list为空,则视参数waitForNotify的值决定是要阻塞等待(lock.wait()),还是直接返回false
  • 接下来,若该节点r是一个Cleaner类型,则直接执行fast path快速处理c.clean()方法并返回
  • 否则,从r中取出其r.queue,并在确认r.queue不为ReferenceQueue.NULL之后,执行queue.enquque(r)将引用对象入列

Reference类加载并初始化时,在执行clinit<>()类初始化构造函数中,会得到当前线程最顶层的threadGroup,并通过该threadGroup创建一个ReferenceHandler线程,其名称为"Reference Handler",并将其优先级设置为最高10,作为后台Deamon线程启动。接着通过SharedSecrets机制,将其执行逻辑tryHandlePending(false)开放给其他模块使用(例如nio的Bits类会使用),代码如下:

static {

        ThreadGroup tg = Thread.currentThread().getThreadGroup();

        for (ThreadGroup tgn = tg;

             tgn != null;

             tg = tgn, tgn = tg.getParent());

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



        // provide access in SharedSecrets

        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {

            @Override

            public boolean tryHandlePendingReference() {

                return tryHandlePending(false);

            }

        });

    }

2.2 ReferenceQueue

当java VM的GC检测到reference对象出现了合适的可达性变化之后,会与JDK的ReferenceHandler线程协作将其加入到其关联(构建时注册的)的ReferenceQueue中。其属性说明如下:

//两个预定义的空队列,如上所述用于结合其他属性来表示Reference对象的状态
static ReferenceQueue NULL, ENQUEUED

//用于表示该队列的对列头,队列通过单向链表形式维护,由Reference.next指向下一个
volatile Reference head

//用于表示队列的长度
long queueLength

//一个普通的对象作为锁,用于同步
Lock lock 
  

其定义的方法包括enqueue(Reference r), poll(), 和remove()/remove(timeout),用于入列与出列操作。

 

其中enqueue(Reference r)用于将Reference对象r入列,该方法只会被Reference类型调用,用于将自己入列(Reference类的enqueue()方法会由java代码调用),gc会直接执行入列逻辑,不需要调用Reference类的对应方法。代码如下:

    boolean enqueue(Reference r) { /* Called only by Reference class */

        synchronized (lock) {

            // Check that since getting the lock this reference hasn't already been

            // enqueued (and even then removed)

            ReferenceQueue queue = r.queue;

            if ((queue == NULL) || (queue == ENQUEUED)) {

                return false;

            }

            assert queue == this;

            r.queue = ENQUEUED;

            r.next = (head == null) ? r : head;

            head = r;

            queueLength++;

            if (r instanceof FinalReference) {

                sun.misc.VM.addFinalRefCount(1);

            }

            lock.notifyAll();

            return true;

        }

    }

其执行逻辑简单描述如下:

  • 首先使用lock锁,来与其他的enqueue及poll/remove互斥(此处使用的简单的对象object作为锁,该锁其实就是一个对象,借助于synchronized来执行同步,并使用Object原生的wait/notifyAll来进行基于条件的等待。其行为与ReentrantLock及Condition一样)
  • 接着在临界区中,首先判断r的r.queue,若为NULL或ENQUEUED,则说明该Reference r已经入列了或入列后又被删除了,此时直接返回false,入列失败
  • 随后判断r.queue(Reference对象r注册的队列)等于当前队列this(r只能被入列到其注册register的ReferenceQueue中)
  • 做完上述判断后,开始执行入列:首先将r.queue置为ENQUEUED,接着讲r.next置为head或自己(若当前head==null),并将head指向r(入列时是将对象r添加到表头head),并将队列长度queueLength++
  • 接着判断若r为FinalReference类型(其实是一个被封装为Finalizer的实现了finalize()方法的普通对象),则调用VM.addFinalRefCount(1),记录一下当前虚机正处于队列中的finalRefCount及peakFinalRefCount
  • 最后,通过lock.notifyAll()唤醒所有通过lock.wait(timeout)等待在lock上的线程(这些线程通过remove()/remove(timeout)方法阻塞等待在该队列上)

 

其poll()方法当队列为空时会直接返回null(而非阻塞),否则首先获取同步锁,接着执行reallyPoll()实际执行出列行为,代码如下:

public Reference poll() {

        if (head == null)

            return null;

        synchronized (lock) {

            return reallyPoll();

        }

    }

 

其remove(timeout)方法,首先获得锁,然后执行reallyPoll()行为实际执行出列得到Reference r,若r不为null则直接返回。否则,首先记下当前时间,并在无限循环中等待lock.wait(timeout),若被唤醒(参见上面enqueue(r)方法),则执行r = reallyPoll(),若不为null则返回,否则,若超时则返回null;若没有超时,则继续循环等待:

public Reference remove(long timeout)

        throws IllegalArgumentException, InterruptedException

    {

        if (timeout < 0) {

            throw new IllegalArgumentException("Negative timeout value");

        }

        synchronized (lock) {

            Reference r = reallyPoll();

            if (r != null) return r;

            long start = (timeout == 0) ? 0 : System.nanoTime();

            for (;;) {

                lock.wait(timeout);

                r = reallyPoll();

                if (r != null) return r;

                if (timeout != 0) {

                    long end = System.nanoTime();

                    timeout -= (end - start) / 1000_000;

                    if (timeout <= 0) return null;

                    start = end;

                }

            }

        }

    }

 

remove()方法调用了remove(0),意味着永远不会超时,一直等待直到返回对象为止。

 

私有方法reallyPoll()方法用于实际执行从队列中取出一个对象,若队列为空则直接返回null(该函数必须在一个lock保护的临界区中执行,因为其并非线程安全),代码如下:

private Reference reallyPoll() {       /* Must hold lock */

        Reference r = head;

        if (r != null) {

            @SuppressWarnings("unchecked")

            Reference rn = r.next;

            head = (rn == r) ? null : rn;

            r.queue = NULL;

            r.next = r;

            queueLength--;

            if (r instanceof FinalReference) {

                sun.misc.VM.addFinalRefCount(-1);

            }

            return r;

        }

        return null;

    }

reallyPoll()执行逻辑描述如下:

  • 首先取出head r(若head不为null)
  • 将head指向下一个节点next或null(若只有head指向的那一个节点)
  • 然后将r的r.queue置为NULL,并将r.next置为自己(代表修改了该r节点的状态为已经从队列中删除),将queueLength--
  • 判断,若r的类型为FinalReference,则调用VM.addFinalRefCount(-1),减少当前虚机正处于队列中的finalRefCount
  • 最后返回该节点r
  • 否则,若head为null,则直接返回null

2.3 Cleaner

Cleaner继承了PhantomReference,其本身是一个虚引用类型。在JDK内部专门用来管理DirectByteBuffer的gc回收时的直接内存清理工作。

其类持有类变量static final ReferenceQueue dummyQueue,及类变量static Cleaner first。其中,dummyQueue只是为了与普通Reference表述一致(在jvm gc中表述一致),其实并不实际承担入列行为。实际的Cleaner对象队列由Cleaner.first静态变量作为链表头统一维护。(也就是说,DirectByteBuffer在构建时通过Cleaner.first链表进行维护,并在gc回收时不走常规的入列ReferenceQueue再从ReferenceQueue中取出处理的流程,而是直接从Cleaner.first队列中删除,并执行其cleaner.thunk.run()逻辑)

另持有对象变量Cleaner next, Cleaner pre及Runnable thunk。每个Cleaner对象通过next, pre构成一个双向链表,其表头为Cleaner.first(全局唯一),并关联一个Runnable thunk,在Cleaner节点对象创建时赋予:对于DirectByteBuffer持有的Cleaner而言,为其注册的Runnable为Deallocator或Runnable unmapper(对于使用mmap方式构建的DirectByteBuffer而言)。

对于Cleaner类型,首先其继承了PhantomReference且在初始化时就注册了ReferenceQueue dummyQueue,因此当jvm的gc在发现其不可达后会将其通过disovered属性链入Reference.pending的链表中。而在ReferenceHandler线程执行tryHandlePending()时,若发现该Reference类型为Cleaner,则直接走快速处理逻辑(fast path),直接调用其cleaner.clean()方法,其中会将该cleaner从Cleaner类维护的链表中删除,并执行其cleaner.thunk.run()。而非像普通Reference对象一样,执行r.queue.enqueue(r)入列到自己注册的queue中。

在JDK8以前,在内部Cleaner类型是为DirectByteBuffer专门使用,为了加快其gc时的表现,因此专门定制了其处理逻辑,走快速通道(参见Reference Handler常驻线程的执行逻辑)。另外,通过Cleaner.create(obj, runnable)方法可以在应用中通过显式调用使用该机制。

在Java 9中,终结方法(Finalizer)已经被遗弃了,但它们仍被Java类库使用,相应用来替代终结方法的是清理方法(cleaner)。比起终结方法,清理方法相对安全点,但仍是不可以预知的,运行慢的,而且一般情况下是不必要的。JDK9中有很多原来使用覆盖Object#finalize()方法的清理工作实现都替换为Cleaner实现了,但是仍然不鼓励使用这种方式。

2.4 Finalizer对象

FinalReference继承自Reference,是内置的用来实现java finalization机制的一种引用类型,并不开放给外部应用程序使用。

Finalizer继承自FinalReference,用于实现普通对象由于实现了finalize()方法而需要在gc时对象被回收前执行一些逻辑的功能。Finalizer机制保证对象的finalize()方法一定会被调用(但不保证执行完成)。

其内部持有类变量static ReferenceQueue queue和static Finalizer unfinalized,其中queue是Finalizer对象作为Reference对象,其本身同样遵守jvm在gc时将其放入pending list中,并由Reference Handler线程将其入列的行为(实际的入列,而非Cleaner的快速处理)。那为何还要维护一个unfinalized队列呢?原来所有的Finalizer对象在创建时都会链入该unfinalized链表中,以记录该对象属于无论如何(就算进程要退出)都需要被调用finalize()方法的对象。因此,专门有一个unfinalized链表用于保存这些对象,并在必要时(例如对象尚未被gc回收,但是进程要退出),遍历该链表并逐个调用其finalize()方法。相对的,被gc标记而入列的Finalizer对象会由常驻的Finalizer线程执行其出列及finalize()方法调用。

另持有实例变量next和prev用于将unfinalized链表构建为一个双向链表(此处的双向链表,是为了方便节点随机删除),还持有一个普通对象锁lock(用于保护unfinalized链表的操作)。

一个Finalizer对象已经被执行了析构(finalized)的标志是next==prev==this,在其被从unfinalized队列中删除时就是其被finalized时,此时会将其next与prev都置为this。

其register(Object finalizee)方法会被jvm调用,当对象初始化时,若jvm判断出其需要被finalize(),则会调用该方法将其封装为一个Finalizer对象并添加到Finalizer.unfinalized双向链表中,并为其注册ReferenceQueue queue:

/* Invoked by VM */

    static void register(Object finalizee) {

        new Finalizer(finalizee);

    }

    private Finalizer(Object finalizee) {

        super(finalizee, queue);

        add();

    }

    private void add() {

        synchronized (lock) {

            if (unfinalized != null) {

                this.next = unfinalized;

                unfinalized.prev = this;

            }

            unfinalized = this;

        }

    }

其runFinalization()方法会被Runtime.runFinalization()调用,用于显式调用finalization的行为。其中会在forkSecondaryFinalizer(runnable)方法中构建一个新线程来跑runnable,其run()行为是从queue中逐个poll()出已入列的Finalizer f,并调用其finalize()方法。其中又借助了看上去高大上的SharedSecrets与JavaLangAccess jla,调用的f.runFinalizer(jla)中最终调用的就是jla.invokeFinalizer(r)(其实际逻辑就是r.finalize()),只是f.runFinalizer(jla)中还包含了一些其他逻辑(后面详述)。一旦发现了队列为空则立刻break退出。代码如下:

/* Called by Runtime.runFinalization() */

static void runFinalization() {

        if (!VM.isBooted()) {

            return;

        }



        forkSecondaryFinalizer(new Runnable() {

            private volatile boolean running;

            public void run() {

                // in case of recursive call to run()

                if (running)

                    return;

                final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();

                running = true;

                for (;;) {

                    Finalizer f = (Finalizer)queue.poll();

                    if (f == null) break;

                    f.runFinalizer(jla);

                }

            }

        });

    }

其runAllFinalizers()方法会被Shutdown调用,也就是说进程退出之前会调用该方法。其中会在forkSecondaryFinalizer(runnable)中构建一个新线程执行runnable,其run()行为是遍历unfinalized链表,对其中的每一个Finalizer f,调用其finalize()方法,同样通过f.runFinalizer(jla)->最终调用到f.finalize()。一旦发现unfinalized链表为空之后,立刻break退出:

/* Invoked by java.lang.Shutdown */

    static void runAllFinalizers() {

        if (!VM.isBooted()) {

            return;

        }



        forkSecondaryFinalizer(new Runnable() {

            private volatile boolean running;

            public void run() {

                // in case of recursive call to 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);

                }}});

    }

其私有的runFinalizer(JavaLangAccess jla)方法供runFinalization()与runAllFinalizers()方法调用(还有Finalizer线程),代码如下:

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

    }

runFinalizer方法执行逻辑为:

  • 首先判断,若已经被finalized过了(hasBeenFinalized()==true),则直接返回;否则执行remove()从unfinalized队列中删除本Finalizer节点
  • 接着从this中得到要执行finalize()方法的finalizee对象
  • 判断若finalizee不为null,且其类型不为Enum,则调用jla.invokeFinalize(finalizee)->其逻辑就是finalizee.finalize()
  • 最后调用super.clear(),也就是Reference.clear()方法,将this.referent置为null

2.4.1 Finalizer常驻线程

在Finalizer类中声明了一个FinalizerThread,命名为"Finalizer",其run方法逻辑为在无限循环中通过queue.remove()阻塞的获取queue中入列的Finalizer对象f,若得到了则执行其f.runFinalizer(jla)(逻辑如上所述)。该线程捕捉interrupt异常并ignore,因此会永久处于循环状态,也就是说只要java进程未退出,则该线程会一直常驻。

final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
            running = true;
            for (;;) {
                try {
                    Finalizer f = (Finalizer)queue.remove();
                    f.runFinalizer(jla);
                } catch (InterruptedException x) {
                    // ignore and continue
                }
            }

该线程优先级被设置为MAX_PRIORITY-2,并在Finalizer类初始化时在clinit<>()方法中启动:

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

由上所述,在java进程中会存在两个常驻线程:

  • Reference Handler线程(高优先级),其作用是将处于pending list中的Reference对象执行入列ReferenceQueue,或Cleaner c.clean()
  • Finalizer线程(低优先级),其作用是执行Finalizer.queue中的被入列(被Reference Handler线程入列)对象的finalize()方法

通过jstack也可以确认每一个java进程确实都有Reference Handler和Finalizer这两个常驻线程:

......

"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007fabc580b000 nid=0x4503 in Object.wait() [0x0000700008b14000]

   java.lang.Thread.State: WAITING (on object monitor)

at java.lang.Object.wait(Native Method)

- waiting on <0x00000007b5730518> (a java.lang.ref.ReferenceQueue$Lock)

at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)

- locked <0x00000007b5730518> (a java.lang.ref.ReferenceQueue$Lock)

at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)

at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)



"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007fabc600c800 nid=0x3103 in Object.wait() [0x0000700008a11000]

   java.lang.Thread.State: WAITING (on object monitor)

at java.lang.Object.wait(Native Method)

- waiting on <0x00000007b5740688> (a java.lang.ref.Reference$Lock)

at java.lang.Object.wait(Object.java:502)

at java.lang.ref.Reference.tryHandlePending(Reference.java:191)

- locked <0x00000007b5740688> (a java.lang.ref.Reference$Lock)

at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

3. java各种引用对象垃圾回收行为概述

综合上述可得,JDK结合JVM GC对对象的回收行为可以 分为如下五大类:

  • 普通Object,未实现finalize()方法(强引用对象)
  • 注册了ReferenceQueue的Reference对象(软、弱、虚引用对象)
  • 未注册ReferenceQueue的Reference对象
  • nio DirectByteBuffer对象(封装为Cleaner进行处理)
  • 实现了finalize()方法的析构对象(封装为Finalizer对象进行处理)

JVM GC判断所有GC Root不可达的对象,并将其中的注册了ReferenceQueue的Reference类型的对象链入pending list中(上文提到的);将其他的状态直接置为Inactive或直接清理。各引用类型对象的垃圾回收行为描述如下:

3.1 普通Object

普通Object在GC Root不可达之后会被直接执行标记(mark),并在清理阶段清理掉,并不具备Reference引用对象的各种状态

3.2 未注册ReferenceQueue的Reference对象

未注册ReferenceQueue的Reference对象,在被gc判断出GC Root不可达之后会被直接设置为Inactive状态

3.3 nio DirectByteBuffer对象

DirectByteBuffer在构建时,jdk内部会为其创建一个Cleaner对象,并注册一个dummyQueue,然后将该cleaner添加到Cleaner.first为head的双向链表中。当gc判断出DirectByteBuffer为GC Root不可达之后会将其对应的Cleaner放入pending list中。而常驻线程Reference Handler在处理Cleaner c时并不会将其入列,而是直接调用c.clean()快速处理方法,其中会从Cleaner.first链中删除该节点,并调用其c.thunk.run()完成clean逻辑

3.4 注册了ReferenceQueue的Reference对象

注册了ReferenceQueue的Reference对象,被gc处理后会被放入pending list中(Reference.pending为head),常驻线程Reference Handler在处理Reference r时,会将其入列自己注册的ReferenceQueue q:r.queue.enqueue(r)。此后,对该ReferenceQueue中对象的监控及后续处理就交给应用自行实现(开放给应用程序来定制,例如netty的ObjectCleaner,及guava的FinalizableReference/FinalizableReferenceQueue都是具体的定制案例)

3.5 实现了finalize()方法的析构对象

实现了finalize()方法的对象,在对象创建时由jvm自行判断出该对象需要执行finalize逻辑,因此将其封装为一个Finalizer对象并为其注册Finalizer.queue队列,然后将其链入Finalizer.unfinalized链表中。当被gc处理之后,会将该Finalizer对象放入pending list中,并由常驻线程Reference Handler放入Finalizer.queue中(入列)。接着入列的对象会由另外一个常驻线程Finalizer逐个处理,执行其finalize()方法中的逻辑(处理之后会从Finalizer.unfinalized队列中删除)。另外在shutdown时,会从Finalizer.unfinalized链表中逐个取出Finalizer f,并执行其f.finalize()逻辑,以确保每一个实现了finalize()方法的对象的finalize()方法都会被调用一次

4. 对Reference的扩展应用

如上对于注册了ReferenceQueue的Reference对象在垃圾回收时的行为说明,其对于入列ReferenceQueue之后对相关引用对象的监控及后续处理就开放给应用自行实现,因此可以通过扩展该功能实现自己的逻辑。

4.1 netty ObjectCleaner

ObjectCleaner是netty提供的用于给对象object注册其被回收时需要被执行逻辑,最初netty会在设置FastThreadLocal对象的值时默认将当前线程注册进ObjectCleaner中,其被gc回收时调用的逻辑是remove(threadLocalMap)。现在对这部分逻辑做了修改,InternalThreadLocalMap threadLocalMap会在FastThreadLocalThread线程中自动管理释放,但是若在普通用户线程中使用netty提供的FastThreadLocal时,就需要自己管理FastThreadLocal数据的清理,此时可以通过ObjectCleaner来注册清理逻辑FastThreadLocal.removeAll(),需要特别注意的是在线程池中线程对象并不会被实际回收,此时注册的对象不应该是线程本身,而应该是一个本线程执行完毕后就会被gc收回的对象,否则仍然会导致FastThreadLocal数据无法释放。

netty的ObjectCleaner实现部分借鉴了jdk的Cleaner及Finalizer的思路,通过显示调用ObjectCleaner.register(Object obj, Runnable cleanupTask)来显式管理对象在被回收时应执行的逻辑。目前netty自身并未使用该方案,由应用自行决定使用。

其自身维护了一个ConcurrentSet LIVE_SET用于通过register方法注册在gc回收时需要执行runnable逻辑的对象。另外定义了自己的ReferenceQueue REFERENCE_QUEUE,并由一个AtomicBoolean CLEANER_RUNNING来标识当前是否有Cleaner线程正在执行。其清理线程Cleaner不像jdk的Finalizer一样常驻,而是通过定义一个Runnable CLEANER_TASK来描述Cleaner线程的动作,并在有必要时(LIVE_SET非空)才会启动Cleaner线程以执行CLEANER_TASK定义的逻辑,代码如下:

public void run() {
            boolean interrupted = false;
            for (;;) {
                // Keep on processing as long as the LIVE_SET is not empty and once it becomes empty
                // See if we can let this thread complete.
                while (!LIVE_SET.isEmpty()) {
                    final AutomaticCleanerReference reference;
                    try {
                        reference = (AutomaticCleanerReference) REFERENCE_QUEUE.remove(REFERENCE_QUEUE_POLL_TIMEOUT_MS);
                    } catch (InterruptedException ex) {
                        // Just consume and move on
                        interrupted = true;
                        continue;
                    }
                    if (reference != null) {
                        try {
                            reference.cleanup();
                        } catch (Throwable ignored) {
                            // ignore exceptions, and don't log in case the logger throws an exception, blocks, or has
                            // other unexpected side effects.
                        }
                        LIVE_SET.remove(reference);
                    }
                }
                CLEANER_RUNNING.set(false);

                // Its important to first access the LIVE_SET and then CLEANER_RUNNING to ensure correct
                // behavior in multi-threaded environments.
                if (LIVE_SET.isEmpty() || !CLEANER_RUNNING.compareAndSet(false, true)) {
                    // There was nothing added after we set STARTED to false or some other cleanup Thread
                    // was started already so its safe to let this Thread complete now.
                    break;
                }
            }
            if (interrupted) {
                // As we caught the InterruptedException above we should mark the Thread as interrupted.
                Thread.currentThread().interrupt();
            }
        }

其执行逻辑为,在无限循环中执行如下:

  • 首先,若LIVE_SET非空,则在循环中逐个从REFERENCE_QUEUE中取出reference(已被Reference Handler入列的),若reference非null,则执行reference.cleanup()方法,并从LIVE_SET中删除该reference。该循环会捕捉各种异常,因此在LIVE_SET非空之前不会退出。
  • 当LIVE_SET为空了,说明上面循环已经将所有加入LIVE_SET中的对象都回收并处理了,此时将CLEANER_RUNNING标志设置为false,标记当前线程处理未执行状态准备退出了。
  • 接着再次判断一下LIVE_SET是否又变得非空了(可能在本线程退出循环之后,又有通过register方法将对象注册进了LIVE_SET),若非空,则cas设置CLEANER_RUNNING为true(原值为false),此处需要cas设置,因为该值有可能会被调用register方法的线程给cas的设置为true,只能有一个线程设置为true成功。
  • 如果LIVE_SET为空,或者cas设置CLEANER_RUNNING没有成功,则直接break打断外层无限循环,标志着本CLEANER_TASK线程执行完毕了
  • 否则,说明LIVE_SET非空,且CLEANER_RUNNING又被本线程cas的设置为true了,则继续循环的处理

其对外提供的static void register(Object obj, Runnable cleanupTask)方法代码如下:

public static void register(Object object, Runnable cleanupTask) {
        AutomaticCleanerReference reference = new AutomaticCleanerReference(object,
                ObjectUtil.checkNotNull(cleanupTask, "cleanupTask"));
        // Its important to add the reference to the LIVE_SET before we access CLEANER_RUNNING to ensure correct
        // behavior in multi-threaded environments.
        LIVE_SET.add(reference);

        // Check if there is already a cleaner running.
        if (CLEANER_RUNNING.compareAndSet(false, true)) {
            final Thread cleanupThread = new FastThreadLocalThread(CLEANER_TASK);
            cleanupThread.setPriority(Thread.MIN_PRIORITY);
            // Set to null to ensure we not create classloader leaks by holding a strong reference to the inherited
            // classloader.
            // See:
            // - https://github.com/netty/netty/issues/7290
            // - https://bugs.openjdk.java.net/browse/JDK-7008595
            AccessController.doPrivileged(new PrivilegedAction() {
                @Override
                public Void run() {
                    cleanupThread.setContextClassLoader(null);
                    return null;
                }
            });
            cleanupThread.setName(CLEANER_THREAD_NAME);

            // Mark this as a daemon thread to ensure that we the JVM can exit if this is the only thread that is
            // running.
            cleanupThread.setDaemon(true);
            cleanupThread.start();
        }
    }

其执行逻辑为:

  • 将obj和该obj被回收时要执行的逻辑cleanupTask封装为一个AutomaticCleanerReference,并将其添加到LIVE_SET中
  • 然后,cas的修改CLEANER_RUNNING标志(在确认其值为false时,将其设置为true,为了确保只有一个CLEANER_TASK线程在执行)
  • 若修改成功,则首先使用Runnable CLEANER_TASK创建一个FastThreadLocalThread并将其优先级设置为最低,接着在特权代码中修改该线程的contextClassLoader为null(防止由于继承的classLoader而导致强引用该classLoader,造成内存泄露),最后启动该CLEANER_TASK线程,线程执行逻辑如上所述
  • 若未修改成功,则说明有CLEANER_TASK正在运行,不需要再次启动该线程

ObjectCleaner内部使用的AutomaticCleanerReference继承了WeakReference,其内部定义了一个Runnable cleanupTask用于保存通过register注册进来的对象obj在被回收之前应该执行的逻辑。

AutomaticCleanerReference实例在初始化时,将参数Object obj构建为一个WeakReference并为其注册队列REFERENCE_QUEUE,并将参数cleanupTask赋予this.cleanupTask。其cleanup()方法就是执行cleanupTask.run(),其重写了WeakReference的get()方法,直接返回null(与直接继承PhantomReference没什么区别)。另外,重写了Reference的clear()方法,其行为除了调用super.clear()方法以外,还从LIVE_SET中删除本对象。

4.2 guava FinalizableReference/FinalizableReferenceQueue

对guava FinalizableReference/FinalizableReferenceQueue的使用及源代码分析请参见另一篇文章《使用ReferenceQueue实现对ClassLoader垃圾回收过程的观察、以及由此引发的ClassLoader内存泄露的场景及排查过程》,此处不再赘述。

5. 总结

Reference/ReferenceQueue机制是与JVM GC紧密协作实现的,用于支持不同强度的引用对象在垃圾回收中的不同行为。另外JDK自身基于较弱的引用(Weak,Phantom)不会影响到被引用对象在gc中的生存周期这一特性,扩展并实现了诸如对象析构方法的支持,以及nio DirectByteBuffer被gc回收时自动内存释放等等功能。此外,一些著名的第三方类库也利用这种特性实现了自己定制的对象回收逻辑处理机制。因此,正确的了解这一机制背后的原理有助于帮助我们在特定的场景中更优雅合理的使用各种引用。

你可能感兴趣的:(源代码分析,java,垃圾回收,Reference,ReferenceQueue,ObjectCleaner)