上篇文章介绍了自己写的延迟队列工具。我们提到,延迟队列不需要长久存活,我们使用带有lru功能的LinkedHashMap来淘汰一些不常用的LimitUtil。但是对象有没有真的会回收呢?
简单写了一个测试类,建了三个对象,Lru容量设为1
LRU<String, LimitUtil> map = new LRU<>(1, 0.75f);
@Test
public void test() throws InterruptedException {
LimitUtilFactory limitUtilFactory = new LimitUtilFactory();
for (int i = 0; i < 3; i++) {
int finalI = i;
LimitUtil instance = limitUtilFactory.getInstance(String.valueOf(finalI), 10, 10, 10, (x) -> {
log.info("执行逻辑 {}", x);
});
instance.put("LimitUtilTest test" + finalI);
}
Thread.sleep(1000000000);
}
我们看下VisualVm的堆快照,第一个对象是被LinkedHashMap所引用
另外两个对象都没有被LinkedHashMap引用
但是我们手动点击GC,堆里LimitUtil的数量依然是三个,我们的程序已经无法取到LimitUtil的实例,但是堆里的对象没有被回收,这里发生了内存泄漏。
我们如果看了垃圾回收的书,我们可以了解到一个对象决定回不回收有两种算法,一种引用计数算法
,一种可达性分析算法(GC Root)
,可达性就是给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。这种算法 有一个致命问题,如果两个对象相互引用,那就没法判断它们是否真的需要被回收,所以此算法已被淘汰。
可达性分析算法
则换了种思路,就是通过一系列的称为“GC Roots”的对象作为起始点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链( Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。GC会收集那些不是GC Roots且没有被GC Roots引用的对象。
那什么可以作为“GC Roots”的对象作为起始点?
讲到这里,是不是觉得LimitUtil即使没有被 LinkedHashMap引用,但是,是否会存在GC Root
对象而导致自身无法回收? 我们再回顾VisualVm的堆快照引用分析。 我们可以看见VisualVm把GC Root对象都用蓝色三角
修饰了,再展开看引用分析, 我们的两个ThreadPool都被虚拟机当作GC Root对象,而不可回收。
在LimitUtil的实现里,我们都用了ThreadPoolExecutor,而且当没有元素put进去的时候,线程会挂起等待,貌似符合gc root对象所定义的活的线程,目标有了,我们需要将这个线程池关闭。对于ThreadPoolExecutor的源码,可以推荐看下面的文章:
【死磕Java并发】—–J.U.C之线程池:ThreadPoolExecutor
shutdownNow方法可以关闭ThreadPoolExecutor内部活跃的线程。我们在LinkedHashMap的removeEldestEntry方法里面写一个关闭方法。保证LinkedHashMap丢弃这个对象的时候可以执行对应的清理代码。
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当元素个数大于了缓存的容量, 就移除元素
if (size() > this.capacity) {
LimitUtil value = (LimitUtil) eldest.getValue();
value.close();
}
return size() > this.capacity;
}
public void close() {
service.shutdownNow();
executorService.shutdownNow();
}
做完之后,我们重新执行程序,在执行gc前,我们看看VisualVm的堆快照。蓝色三角形没有了。这个对象理论上只剩下被回收。
我们手动执行一遍gc,LimitUtil只剩下1个。对象被成功回收!
在VisualVm堆快照中,我发现有些对象会被referent引用。通过查阅资料,了解到所有覆写了finalize方法的对象在创建时 都会被Finalizer包装并且 在引用不可达时放入ReferenceQueue中,队列会执行对象的finalize方法,所以以前只知道finalize方法是 对象被gc前执行的方法,现在也知道了前因后果。
Finalizer跟WeakReference(弱引用),SoftReference(软引用) 同系一脉,适合当缓存。感兴趣的同学可以深入了解。