1 引用队列的监控和回调
当创建一个引用队列时,我们需要对这个队列进行监控,即开启一个新的线程来循环判断此队列信息,并从中获取相应的数据信息。在guava中,也是通过创建一个线程然后在循环中进行判断,如下所示(类com.google.common.base.internal.Finalizer):
Thread thread = new Thread(finalizer); thread.setName(Finalizer.class.getName()); thread.setDaemon(true); thread.start();
即创建一个以finalizer runnable对象的线程,然后启动之,为避免阻止进程结束,采用了后台线程的模式。那在这个finalizer中,其运行如下所示:
while (true) { try { if (!cleanUp(queue.remove())) { break; } } catch (InterruptedException e) { /* ignore */ } }
即不断地获取队列中的数据,然后调用cleanUp方法.而在cleanUp方法中,其实就是调用相应对象的回调方法,即当一个对象已经被gc时的回调。
在guava中,从队列中移除的是一个是reference对象。在java体系中,并没有在reference对象中定义相应的回调方法,因此guava为jdk的reference增加了新的定义接口,称之为FinalizableReference。在这个接口中,即定义了一个相应的回调函数,如下所示:
/** 当引用被gc之后,此方法会被触发调用 */ void finalizeReferent();
因为增加了新的接口,因此我们自己来使用的话,就即要继承于jdk的weakReference,又要实现此接口。为方便,guava定义了3套与jdk兼容的引用对象。即FinalizableWeakReference,FinalizableSoftReference和FinalizablePhantomReference。可以理解为就是一个对jdk原类型的一个适配。
2 封装和使用
我们在使用guava时,也只需要按照如下的方式使用就行了:
FinalizableReferenceQueue queue = new FinalizableReferenceQueue(); byte[] bytes = new byte[_1M]; FinalizableWeakReference<byte[]> weakReference = new FinalizableWeakReference<byte[]>(bytes, queue) { public void finalizeReferent() { //这里是被gc之后的回调 //do your thing } };
在这个代码中,监控线程是在queue的定义中产生,即每次创建一个queue对象,即会产生相应的监控线程,同时已经开始运行中。
回调即我们在定义weakReference时,需要创建原对象及对象,同时需要马上实现的回调函数。
在这里的FinalizableReferenceQueue对象其实并不是真的引用队列,只是充当一个引用队列的代理,其在内部封装了一个queue。这样的目的在于后面提到的保证这个queue对象能够被正常回收。
3 如何优雅的退出线程
在第2中,我们提到每一次创建一个queue就要创建一个线程对象,然后这个线程对象就会不断的循环调用,那么在这个线程中,何时才能安全的停止这个线程。
首先,这个线程没有向外部发布引用对象,因此我们不能直接简单地interrupt掉,只能从如何中止相应的循环条件入手。在上面的循环中,我们看到只有当cleanUp返回为false时,这个循环才能停止。在guava中,只有在2个条件下,这个cleanUp才能返回false。
3.1 队列中确定没有新的数据了
如何保证用户程序确定没有在放入队列了呢,我们认为如果队列本身都被gc了。即这个列队已经没有再使用了,自然这个队列中就没有新的数据了。
在guava中,队列对象(FinalizableReferenceQueue)自身也是通过虚引用放在引用队列中的。因此,一旦这个finalizableQueue被放入队列之后(即已经准备回收了)。我们就认为这个队列的监控线程已经没有继续监控的需要了。代码如下所示:
if (reference == frqReference) { /* * The client no longer has a reference to the * FinalizableReferenceQueue. We can stop. */ return false; }
上面代码reference即放入队列中的对象,而frtFeference即当时在创建finalizer时封装的的虚引用。在这个逻辑中,即认为这个finalizerQueue被gc了,因此线程可以被停止。
3.2 队列应用部分正在被回收
如果对象的应用已经被回收了,即使用此线程的应用场景已经不存在了,那么一样地停止此线程。guava通过一个对原应用的一个class(这个类即FinalizableReference接口)类的引用来进行判断,这个class类被包装在一个weakReference中。如果这个class被回收了,即认为相应的应用已正在被回收。即这个class类都被回收,那么相应的队列对象肯定亦没有在使用了(class类回收的前提即这个类定义的所有对象都被回收)。
guava通过将class类进行包装,然后在循环中每次去获取相应的类信息(从而再获取到相应的回调方法),如果类信息不存在,则认为被回收了。代码如下:
Class<?> finalizableReferenceClass = finalizableReferenceClassReference.get(); if (finalizableReferenceClass == null) { /* * FinalizableReference's class loader was reclaimed. While there's a * chance that other finalizable references could be enqueued * subsequently (at which point the class loader would be resurrected * by virtue of us having a strong reference to it), we should pretty * much just shut down and make sure we don't keep it alive any longer * than necessary. */ return null; }
4 垃圾回收相关
4.1 如何回收当前应用及classLoader
试想在一个web应用或者osgi应用下,我们想要直接回收整个应用。但在这个点上,相应的队列及整个应用加载器处于一个大的循环引用体中。而这个应用体如果要被正常回收的话,首先需要保证这个应用中已经被任何在运行的程序了,简单点来说就是没有线程在运行。web应用及osgi应用可以通过close监听事件,安全地停止应用内的各个程序(包括停止线程,关闭连接等)。
在guava的这个程序中,finalizer的线程并没有通过向外发布关闭接口来接收关闭事件。这就意味着guava需要自己来主动地探测回收信息。如果finalizer线程不能停止运行的话,那么所引用的classloader也不能被回收,如果这个classLoader恰好也是应用的classLoader,那么这个子应用就不能干净地回收了。在FinalizableReferenceQueue原文中,有如下描述:
* If this library is loaded in an application class loader, it's important that Finalizer not * have a strong reference back to the class loader. Otherwise, you could have a graph like this: * * Finalizer Thread runs instance of -> Finalizer.class loaded by -> Application class loader * which loaded -> ReferenceMap.class which has a static -> FinalizableReferenceQueue instance * * Even if no other references to classes from the application class loader remain, the Finalizer * thread keeps an indirect strong reference to the queue in ReferenceMap, which keeps the * Finalizer running, and as a result, the application class loader can never be reclaimed. * * This means that dynamically loaded web applications and OSGi bundles can't be unloaded.
即在上面的场景中,引用队列不能被回收,而且由于classLoader被线程所引用,因此也不能被回收。而线程正在运行,自然也不能被回收,导致形成一个引用链。这样在web应用及osgi下就形成额外的垃圾,最终生成占用永久代以及运行线程空间。
为了解决这个问题,guava采用一种打断联系的方法来处理这个问题,就是将finalizer空间和应用空间分开,两个空间运行在不同的classLoader中。这样的话由于finalizer不是由应用classloader定义的,自然不会造成上面的循环引用链。当用户程序安全停止之后,整体即会被回收。然后在finalizer这边也是因为用户的相应队列和class被回收之后,自然也能够停止,然后被回收了。
guava通过额外的类加载器来完成这个操作,分别为systemLoader和urlClassLoader。
- systemLoader
如果finalizer这个类能够被systemLoader加载(即意味着这个类被放在系统路径中),这样同样也意味着根本没有回收的必要。
- urlClassLoader
尝试采用额外的urlClassLoader来加载,即尝试去读取finalizer的位置,然后新建urlClassLoader来专门加载finalizer。这样即可以保证空间的分离。在用户空间被卸载的同时,finalizer这个urlClassLoader也同样可以因为finalizer线程被停止而被回收掉。
4.2 如何有效地退出线程
如第3点所示,如果线程还在运行的话,运行此线程的相应类及classLoader是不能被回收的。因此guava使用特殊的处理方式将列队的使用类和finalizer运行在不同的classLoader环境中,这样就保证了两边的应用不会互相干扰。即可以保证finalizer回收之后,queue应用可以正常运行,或者queue应用的回收不会被finalizer线程阻止。
转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201407160001.html