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