引用回收问题
背景
本文由:https://github.com/Snailclimb/JavaGuide/issues/975 讨论延伸而来。
主要回顾了对栈帧引用局部变量回收问题、FinalizableDelegatedExecutorService 导致的 jdk p4 bug。
最后总结了正确的编码风格。
引用基础
强软弱虚 +FinalReference 参见:
- https://club.perfma.com/article/125010
- https://www.infoq.cn/article/jvm-source-code-analysis-finalreference
因为我自己对引用,尤其是 referenceQueue 以及 FinalReference 理解不够透彻,怕写的有问题,并且本文也和引用区别的关系不是很大,就直接贴出我看的博文了,讲解的很细致。
强引用也会被回收
本小节理论参考自:https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope
你看的没错,我们传统理解的强引用也会被回收。如下示例代码,可能有点复杂,但是它是为了证明在某种情况下,回收对象和是什么引用无关。其中 Entry 是继承自 WeakReference,而其中使用了 super(k)
,表明我们将第一个入参作为弱引用传入,value
则不作任何表示,是一个普通变量赋值:
1 | public static void main(String[] args) { |
其中大概在 6W 次循环后,car
引用被回收。注意 car
它本身有强引用在局部变量中的: Car car = new Car()
,我开始觉得因为它被弱引用了,于是接下来改动 Entry
,将 v
入参作为弱引用(记得改泛型):
1 | Entry(Car k, Object v) { |
但是在循环 6W 次左右后仍然出现了和弱引用一样的情况。说明这个回收对强弱引用都执行相同的逻辑。为什么这里的强引用也会回收,这里已经有了解释:https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope。
我理解为什么强引用会被回收,是因为 JIT 的优化。为什么我们说局部变量(本例中的 car
变量)是强引用,因为它被栈帧引用了,而栈帧是 GCRoots 之一。而这 car
被回收的前提是 GCRoot 不可达,但是栈帧还没被销毁怎么会不可达呢,因为 JIT 在这里会做优化操作:Java 编译器或代码生成器可以选择将不再使用的变量或参数设置为 NULL,以使此类对象的存储可能更快地可回收。
a Java compiler or code generator may choose to set a variable or parameter that will no longer be used to
null
to cause the storage for such an object to be potentially reclaimable sooner来源:https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.6.1。后面简称 jls-12.6.1
在此示例代码中,car 被 JIT 优化设置为 null(当然,它在循环了 6W 次才“学会了”优化)。
为什么扯到 ThreadLocal
因为这篇文章是因 ThreadLocal
而起,我们都知道 ThreadLocal
内部使用 ThreadLocalMap
+ Entry
实现,而 Entry
继承自 WeakReference
,而 ThreadLocalMap
将其 value
设置为了弱引用,也就是我们业务设置的值,而 ThreadLocalMap
的 key
则为 ThreadLocal
本身的实例。
因此这里会产生一个答案,这个答案的问题是:为什么我们建议 ThreadLocal 实例时作为 static,而不是作为一个局部变量。这个问题的答案是:因为局部变量 ThreadLocal
如果操作不当可能会被回收!而 ThreadLocal
风险很高,因为它除了栈帧在业务上很少会有其它引用(当然,你可以在后面使用它的实例方法)。
还有什么情况会被奇怪的回收
前面我们通过 jls-12.6.1 知道,编译器可以选择将不再使用的变量设为 null,但是该文档前面还有一句:
一个可到达对象是可以从任何活动线程在任何潜在的持续计算中访问的任何对象。
A reachable object is any object that can be accessed in any potential continuing computation from any live thread.
关键点在于“任何活动线程”,那如果某个线程被错误的识别出不活跃,那里面的强引用会被回收吗?但是下面其实和活动线程关系不大,倒是和前面的强引用回收问题有关系。
SingleThreadExecutor 的 GC 回收问题
本小节中文参考自:https://segmentfault.com/a/1190000021109130。其中错误代码如下:
1 | public class ThreadPoolTest { |
原文评论道出了真相,描述和 stackoverflow 的高赞回答类似:
executeService 已经是 unreachable,被 gc 是正常的。不觉得是什么 bug,代码本身编写的问题。返回的是 FutureTask, 就是对 FutureTask 的引用,抛出的异常是在你提交任务后,还没有执行完的情况下 ExecuteService 被 gc 了,同时被 shutdown(笔者注:executorService.execute(futureTask); 这行代码并不表示 executorService 被 futureTask 引用,也就是说这行代码对于 executorService 的引用关系是没有任何改变的)
这里参见:https://bugs.openjdk.java.net/browse/JDK-8145304 这个 BUG 一直是 OPEN 状态,里面的示例代码非常简单是:
1 | public void afterPropertiesSet() { |
PS:这种代码竟然有 bug,虽然 bug 等级是 P4。
核心在于 newSingleThreadExecutor 返回的是包装类 FinalizableDelegatedExecutorService,该类重写了 finalize()
方法,在回收时调用 shutdown()
,但是 JIT 在优化时,对局部引用对象 executorService 也可能会回收(不活跃线程),导致的 JDK bug。
如何避免这种问题呢?我们可以将该 executeService 设置为静态变量!或者保证在代码方法块中对 executeService 仍然有访问。但是设为静态变量是最为方便直接的方案。
总结
- 局部变量虽然被栈帧(GCRoot 之一)引用,但它会被 JIT 优化从而被回收
- Executors.newSingleThreadExecutor 有 bug,其 bug 等级是 P4
- JIT 优化会触发意想不到的问题
- 静态变量的设计有很多考究的(不要过度用)