我决定蹭一下“线程数突增...”这篇文章的热度。

你好呀,我是歪歪。

前两天在社区冲浪的时候,看到首页推了一篇文章:

juejin.cn/post/719742...

我看了一下,文章写的还是挺好的,案例经典,思路清晰,行文流畅。

你可以看一下,文章写的基本没啥毛病,有些小瑕疵在评论区指出了,问题也不大。

但是歪师傅觉得这里面有一个非常关键的点没有说到,导致这个文章还差点意思,所以歪师傅想要沿着这个话题再补充一下。

主要是看到文章在社区里面有点热度,引发了大家的讨论。而对于这个问题歪师傅之前又刚好浅浅的研究过一番,所以准备蹭一蹭热度。

开蹭

先上个代码:

请问,上面代码中,位于 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,让我们回归本源,分析起来就简单一点了:

csharp 复制代码
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 条:静态内部类优于非静态内部类。

你看,他用的是"优于",意思是优先考虑,而不是强行怼。

好啦,本文的技术部分就到这里了。

下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息。

荒腔走板

昨天在 B 站上冲浪的时候,看到首页推荐了这个视频。

导演何苦,一个陌生而又熟悉的名字。陌生是因为好多年没听到了,熟悉是因为这个导演在 10 年前开拍了一部叫做《最后的棒棒》的 13 集纪录片,并于 2016 年上映。

江湖传言这个纪录片是导演斥资 1300 元,自导自演自己剪辑自己唱主题曲的纪录片。

8 年过去了,我还是当年看完之后的那句评语:这是我心目中最好的国产纪实类纪录片,在任何时候让我打分,我都会给满分。

所以我当在 B 站上看到何苦导演的"10周年"视频的时候,一瞬间就被吸引过去了,又想起了这部纪录片。

十年过去了,自力巷早就消失了,纪录片里面的主人公老黄病逝了,老金据说死在了天桥下,老甘和河南通过和别人拍短视频过上了还算不错的日子,更多的人就不知所踪了。

甚至,这个纪录片似乎也找不到资源了。

倒也印证了近年来比较流行的一句话:

至此,已成艺术。

相关推荐
代码调试2 小时前
Springboot校园失物招领平台
java·spring boot
camellias_3 小时前
SpringBoot(二十三)SpringBoot集成JWT
java·spring boot·后端
tebukaopu1483 小时前
springboot如何获取控制层get和Post入参
java·spring boot·后端
昔我往昔3 小时前
SpringBoot 创建对象常见的几种方式
java·spring boot·后端
q567315233 小时前
用 PHP或Python加密字符串,用iOS解密
java·python·ios·缓存·php·命令模式
灭掉c与java3 小时前
第三章springboot数据访问
java·spring boot·后端
啊松同学4 小时前
【Java】设计模式——工厂模式
java·后端·设计模式
枫叶_v4 小时前
【SpringBoot】20 同步调用、异步调用、异步回调
java·spring boot·后端
鸣弦artha4 小时前
蓝桥杯——杨辉三角
java·算法·蓝桥杯·eclipse
大波V54 小时前
设计模式-参考的雷丰阳老师直播课
java·开发语言·设计模式