jdk cleaner和finalizer的联动

83 阅读3分钟

jdk实现了2种资源回收方式。cleaner和finalizer,并在在高版本jdk中会用cleaner替换现有的jdk内实现finalizer回收资源的方式。在jdk8中2者是同时存在的。在一个案例中,我发现了2者在某些场景下是联动的。下面讲解的主要是java8的实现

ReferenceHandler

jdk会启动一个线程,去处理java的引用的相关处理,主要是负责2个事情,一个是cleaner对象触发,1个是把对象加入到引用队列里。

    // Fast path for cleaners
    if (c != null) {
        c.clean();
        return true;
    }

    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);

finalizer对象有关的就是后者,他的回收依赖ReferenceQueue。如果一个finalizer对象放入到queue里就会触发到另外一个线程中。

        while (!VM.isBooted()) {
            // delay until VM completes initialization
            try {
                VM.awaitBooted();
            } catch (InterruptedException x) {
                // ignore and continue
            }
        }
        final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
        running = true;
        for (;;) {
            try {
                Finalizer f = (Finalizer)queue.remove();
                f.runFinalizer(jla);
            } catch (InterruptedException x) {
                // ignore and continue
            }
        }
    }

在这个线程里,只要进程活着,就会一直循环。从finalizer的引用队列上,取出对象,然后执行方法的finalizer。

联动

通过上面的流程,我们明白回收其实主要是通过ReferenceHandler线程去做,cleaner立马执行,剩下的对象如果有引用队列就直接入队列。finalizer是通过队列消费来进行资源的回收,而且线程是个死循环,有对象就释放。

两者的联动来自ReferenceHandler中处理引用的时机。

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

ReferenceHandler的线程也是一个死循环,但是在调用的时候传递一个true给tryHandlePending。


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

tryHandlePending每次调用只处理一个引用,并且如果没有引用处理的时候,这个线程就会被挂起。没有地方唤醒,等待的只可能是线程oom或者异常中断。

SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
    @Override
    public boolean tryHandlePendingReference() {
        return tryHandlePending(false);
    }
});

tryHandlePending也提供给了外部调用。参数为false是不中断的。这个地方只会被directbutebuffer调用。directbutebuffer的回收是依赖于cleaner。

final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
while (jlra.tryHandlePendingReference()) {
    if (tryReserveMemory(size, cap)) {
        return;
    }
}

在直接内存申请不足时,会一直调用tryHandlePending去触发cleaner对内存的释放。直到没有引用对象或者内存空间足够了。 这里的联动就触发了。 ReferenceHandler是触发回收的必经路径。但是他自身的调用依赖有对象已经被回收了需要处理,一旦发生了一次清理完成,就会挂起。在挂起之后,新生产的对象就会被阻塞起来。这时候最有效的路径就是来自外部的调用。directbutebuffer只要申请频繁达到上限就会去主动的处理引用,期待cleaner对象回收内存,但同时也会让finalizer对象更快的被放入到引用队列里。最终达到了加速finalizer释放的效果。

从这里也就看出通过ReferenceQueue来观测对象是否被回收并不会很实时。

小结

如果代码中有大量的对象依赖finalizer去回收,那么如果想加速这个过程,可以通过directbutebuffer去触发回收。这里可以减小上限来实现形似的效果。上限的设置默认是Runtime.getRuntime().maxMemory()的值和heap的设置有关,也可以通过参数直接指定-XX:MaxDirectMemorySize。如果确实没有好的时机触发联动,也可以自己做一些代码层的调用。主动去调用释放的方法。通过JavaLangRefAccess去做。这里需要自己去控制触发的时机。


 JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();