你好呀,我是苏三。
前两天在掘金社区冲浪的时候,看到首页推了一篇文章:
https://juejin.cn/post/7197424371991855159
我看了一下,文章写的还是挺好的,案例经典,思路清晰,行文流畅。
你可以看一下,文章写的基本没啥毛病,有些小瑕疵在评论区指出了,问题也不大。
但是歪师傅觉得这里面有一个非常关键的点没有说到,导致这个文章还差点意思,所以歪师傅想要沿着这个话题再补充一下。
主要是看到文章在社区里面有点热度,引发了大家的讨论。而对于这个问题歪师傅之前又刚好浅浅的研究过一番,所以准备蹭一蹭热度。
开蹭
先上个代码:
请问,上面代码中,位于 method 方法中的 object 对象,在方法执行完成之后,是否可以被垃圾回收?
这还思考个啥呀,这必须可以呀,因为这是一个局部变量,它的作用域在于方法之间。
JVM 在执行方法时,会给方法创建栈帧,然后入栈,方法执行完毕之后出栈。
一旦方法栈帧出栈,栈帧里的局部变量,也就相当于不存在了,因为没有任何一个变量指向 Java 堆内存。
换句话说:它完犊子了,它不可达了。
那么我现在换个写法:
你说在 method 方法执行完成之后,executorService 对象是否可以被垃圾回收呢?
别想复杂了,这个东西和刚刚的 Object 一样,同样是个局部变量,肯定可以被回收的。
但是接下来我就要开始搞事情了:
我让线程池执行一个任务,相当于激活线程池,但是这个线程池还是一个局部变量。
那么问题就来了:在上面的示例代码中,executorService 对象是否可以被垃圾回收呢?
这个时候你就需要扣着脑壳想一下了...
别扣了,先说结论:不可以被回收。
由于不可用被回收,这个方法被调用的次数越多,那么创建的线程就越多。
这就是前面提到的文章中的现象:
问题也就随之引出来了:
这也是个局部变量,它为什么就不可以被回收呢?
这是文章中给出的结论,整体看来没有什么大问题。但是少了非常关键的一环,如果这一环没有捋顺,那么整个结论都是可以被推翻的,我给你扯掰扯。
为什么
你知道线程池里面有活跃线程,所以从直觉上讲应该是不会被回收的。
但是证据呢,你得拿出完整的证据链来才行啊。
好,我问你,一个对象被判定为垃圾,可以进行回收的依据是什么?
这个时候你脑海里面必须马上蹦出来“可达性分析算法”这七个字,刷的一下就要想起这样的图片:
必须做到我告诉你明天是星期四,你立马就想到 KFC,然后脱口而出"v 我 50" 一样自然。
这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到 GC Roots 间没有任何引用链相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象是不可能再被使用的。
所以如果要推理 executorService 是不会被回收的,那么就得推理出 GC Root 到 executorService 对象是可达的。
那么哪些对象是可以作为 GC Root 呢?
老八股文了,不过多说。
只看本文关心的部分:live thread,是可以作为 GC Root 的。
这一点在文章中也有相关说明:
所以,由于我在线程池里面运行了一个线程,即使它把任务运行完成了,它也只是 wait 在这里,还是一个 live 线程:
因此,我们只要能找到这样的一个链路就可以证明 executorService 这个局部变量不会被回收:
live thread(GC Root) -> executorService
一个 live thread 对应到代码,就是一个调用了 start 方法的 Thread,并且未处于终止状态。这个 Thread 里面是一个实现了 Runnable 接口的对象。
这个实现了 Runnable 接口的对象对应到线程池里面的代码就是这个玩意:
java.util.concurrent.ThreadPoolExecutor.Worker
再看看文章的结论部分,写的是 Worker 全部被 GC 之后,那么对应的线程池也就会被 GC 了:
这个结论没有任何毛病,但是文章少了一步论证过程。
即证明:
Worker(live thread) -> ThreadPoolExecutor(executorService)
也就是找 Worker 类到 ThreadPoolExecutor 类的引用关系。
歪师傅就来“狗尾续貂”一把,帮忙把这一步衔接上。
看到这个问题,有的同学立马就站起来抢答了这个我熟悉啊,不就是它吗?
你看,ThreadPoolExecutor 类里面有个叫做 workers 的成员变量。
就证明 ThreadPoolExecutor 类是持有 workers 的引用啊?
没毛病,但是,请注意我的问题是:
找 Worker 类到 ThreadPoolExecutor 类的引用关系。
有的同学立马又要说了:这个问题,直接看 Worker 类不就行了,看看里面有没有一个 ThreadPoolExecutor 对象的成员变量。
不好意思,这个真没有:
咋回事?难道是可以被回收的?
但是如果 ThreadPoolExecutor 对象被回收了,Worker 类还存在,那岂不是很奇怪,线程池没了,线程还在?
开始证明
接下来,先忘记线程池,我给大家搞个简单的 Demo,让我们回归本源,分析起来就简单一点了:
public class Outer {
private int num = 0;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
//内部类
class Inner {
private void callOuterMethod() {
setNum(18);
}
}
}
Inner 类是 Outer 类的一个内部类,所以它可以直接访问 Outer 类的变量和方法。
这个写法大家应该没啥异议,日常的开发中有时也会写内部类,我们稍微深入的想一下:为什么 Inner 类可以直接用父类的东西呢?
因为非静态内部类持有外部类的引用。
这句话很重要,后面全部都是围绕这句话展开的。
接下来我来证明一下这个点。
怎么证明呢?
很简单,javac 编译一波,答案都藏在 Class 里面。
可以看到, Outer.java 反编译之后出来了两个 Class 文件:
它们分别是这样的:
在 Outer&Inner.class 文件中,我们可以看到 Outer 在构造函数里面被传递了进来,这就是为什么我们说:为非静态内部类持有外部类的引用。
好的,理论知识有了,也验证完成了,现在我们再回过头去看看线程池:
Worker 类是 ThreadPoolExecutor 类的内部类,所以它持有 ThreadPoolExecutor 类的引用。
因此这个链路是成立的,executorService 对象不会被回收。
Worker(live thread) -> ThreadPoolExecutor(executorService)
你要不信的话,我再给你看一个东西。
我的 IDEA 里面有一个叫做 Profile 的插件,程序运行起来之后,在这里面可以对内存进行分析:
我根据 Class 排序,很容易就能找到内存中存活的 ThreadPoolExecutor 对象:
点进去一看,这不就是我定义的核心线程数、最大线程数都是 3,且只激活了一个线程的线程池吗:
从 GC Root 也能直接找到我们需要验证的链路:
所以,我们回到最开始的问题:
在上面的示例代码中,executorService 对象是否可以被垃圾回收呢?
答案是不可以,因为线程池里面有活跃线程,活跃线程是 GC Root。这个活跃线程,其实就是 Woker 对象,它是 ThreadPoolExecutor 类的一个内部类,持有外部类 ThreadPoolExecutor 的引用。所以,executorService 对象是“可达”,它不可以被回收。
道理,就这么一个道理。
然后,问题又来了:应该怎么做才能让这个局部线程池回收呢?
调用 shutdown 方法,干掉 live 线程,也就是干掉 GC Root,整个的就是个不可达。
这样,整体上就和前面提到的文章中的总结部分呼应起来了。
延伸一下
再看看我前面说的那个结论:
非静态内部类持有外部类的引用。
强调了一个“非静态”,如果是静态内部类呢?
把 Inner 标记为 static 之后, Outer 类的 setNum 方法直接就不让你用了。
如果要使用的话,得把 Inner 的代码改成这样:
或者改成这样:
也就是必须显示的持有一个外部内对象,来,大胆的猜一下为什么?
难道是静态内部类不持有外部类的引用,它们两个之间压根就是没有任何关系的?
答案我们还是可以从 class 文件中找到:
当我们给 inner 类加上 static 之后,它就不在持有外部内的引用了。
此时我们又可以得到一个结论了:
静态内部类不持有外部类的引用。
那么延伸点就出来了。
也就是《Effective Java(第三版)》中的第 24 条:
比如,还是线程池的源码,里面的拒绝策略也是内部类,它就是 static 修饰的:
为什么不和 woker 类一样,弄成非静态呢?
这个就是告诉我:当我们在使用内部类的时候,尽量要使用静态内部类,免得莫名其妙的持有一个外部类的引用,又不用上。
其实用不上也不是什么大问题。
真正可怕的是:内存泄露。
比如网上的这个测试案例:
Inner 类不是静态内部类,所以它持有外部类的引用。但是,在 Inner 类里面根本就不需要使用到外部类的变量或者方法,比如这里的 data。
你想象一下,如果 data 变量是个很大的值,那么在构建内部类的时候,由于引用存在,不就不小心额外占用了一部分本来应该被释放的内存吗。
所以这个测试用例跑起来之后,很快就发生了 OOM:
怎么断开这个“莫名其妙”的引用呢?
方案在前面说了,用静态内部类:
只是在 Inner 类上加上 static 关键字,不需要其他任何变动,问题就得到了解决。
但是这个 static 也不是无脑直接加的,在这里可以加的原因是因为 Inner 类完全没有用到 Outer 类的任何变量和属性。
所以,再次重申《Effective Java(第三版)》中的第 24 条:静态内部类优于非静态内部类。
你看,他用的是“优于”,意思是优先考虑,而不是强行怼。
最后欢迎加入苏三的星球,你将获得:商城系统实战、秒杀系统实战、
代码生成工具、系统设计、性能优化、技术选型、高频面试题、底层原理、
Spring源码解读、工作经验分享、痛点问题等多个优质专栏。
还有1V1答疑、修改简历、职业规划、送书活动、技术交流。
目前星球已经更新了4400+篇优质内容,还在持续爆肝中..星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1400+小伙伴加入学习。
我的技术专栏《程序员最常见的100个问题》,目前已经更新了80篇干货文章,里面收录了很多踩坑经历,对你的职业生涯或许有些帮助,最近收到的好评挺多的。
这个专栏总结了我10年工作中,遇到过的100个非常有代表性的技术问题,非常有参考和学习价值。
Java、Spring、分布式、高并发、数据库、海量数据、线上问题什么都有。
每篇文章从发现问题、分析问题、解决问题和问题总结等多个维度,深入浅出,分享了很多技术细节,定位和排查问题思路,解决问题技巧,以及实际工作经验。
你能从中学到很多有用知识,帮你少走很多弯路。
扫描下方二维码即可订阅:
原价199,现价只需23,即将涨价。