前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >线程数突增!领导说再这么写就GC掉我!

线程数突增!领导说再这么写就GC掉我!

作者头像
why技术
发布2024-01-10 15:30:42
1210
发布2024-01-10 15:30:42
举报
文章被收录于专栏:why技术why技术

你好呀,我是歪歪。

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

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,让我们回归本源,分析起来就简单一点了:

代码语言:javascript
复制
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周年”视频的时候,一瞬间就被吸引过去了,又想起了这部纪录片。

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

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

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

至此,已成艺术。

·············· END ··············

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-01-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 why技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开蹭
  • 为什么
  • 开始证明
  • 延伸一下
  • 荒腔走板
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档