关于后端:从局部变量说起关于一个莫得名堂的引用和一个坑

5次阅读

共计 6312 个字符,预计需要花费 16 分钟才能阅读完成。

明天带大家盘一个有点意思的基础知识啊。
有多根底呢,先给你上个代码:

请问,下面代码中,位于 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(live thread) -> ThreadPoolExecutor(executorService)

也就是找 Worker 类到 ThreadPoolExecutor 类的援用关系。
有的同学立马就站起来抢答了:hi,就这?我认为多狠呢?这个我相熟啊,不就是它吗?

你看,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 条:动态外部类优于非动态外部类。
你看,他用的是“优于”,意思是优先思考,而不是强行怼。
再延长一下
对于“动态外部类”这个叫法,我记得我从第一次接触到的时候就是这样叫它的,或者说大家都是这样叫的。
而后我写文章的时候,始终在 JLS 外面找“Static Inner Class”这样的关键词,然而的确是没找到。
在 Inner Class 这一部分,Static Inner Class 这三个单词并没有间断的呈现在一起过:

docs.oracle.com/javase/spec…

直到我找到了这个中央:

docs.oracle.com/javase/tuto…

在 Java 官网教程外面,对于外部类这部分,有这样一个小贴士:

嵌套类分为两类:非动态和动态。非动态的嵌套类被称为外部类(inner classes)。被申明为动态的嵌套类被称为动态嵌套类(static nested classes)。

看到这句话的时候,我一下就反馈过去了。大家司空见惯的 Static Inner Class,其实是没有这样的叫法的。
nested,嵌套。
我感觉这里就有一个翻译问题了。
首先,在一个类外面定义另外一个类这种操作,在官网文档这边叫做嵌套类。
没有加 static 的嵌套类被称为外部类,从应用上来说,要实例化外部类,必须首先实例化外部类。
代码得这样写:

// 先搞出外部类
OuterClass outerObject = new OuterClass();
// 能力搞出外部类
OuterClass.InnerClass innerObject = outerObject.new InnerClass();

所以这个 Inner 就很传神,打个比分,它就像是我的肾,是我身材的一部分,它 Inner 我。
加了 static 的嵌套类被称为动态嵌套类,和 Inner 齐全就不沾边。
这个 nested 也就很传神,它的意思就是我原本是能够独立存在的,不必依附于某个类,我附丽你也只是借个壳而已,我嵌套一下。
打个比分,它就像是我的手机,它随时都在我的身上,然而它并不 Inner 我,它也能够独立于我存在。
所以,一个 Inner,一个 nested。一个肾,一个手机,它能一样吗?
当然了,如果你非得用肾去换一个手机 …

这种翻译问题,也让我想起了在知乎看到的一个相似的问题:

为什么很多编程语言要把 0 设置为第一个元素下标索引,而不是直观的 1?

上面有一个长篇累牍、醍醐灌顶的答复:

还能够延长一下
接下来,让咱们把眼光放到《Java 并发编程实战》这本书上来。
这外面也有一段和本文相干的代码,初看这段代码,让有数人摸不着头脑。
书上说下这段代码是有问题的,会导致 this 援用逸出。
我第一次看到的时候,整个人都是懵的,看了好几遍都没看懂:

而后就跳过了 …
直到很久之后,我才明确作者想要表白的意思。
当初我就带你盘一盘这个代码,把它盘明确。
我先把书上的代码补全,全副代码是这样的:
public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
    }

    void doSomething(Event e) {
    }

    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}
复制代码
代码要是你一眼看不明确,没关系,次要是关注 EventListener 这个玩意,你看它其实是一个接口对不对。
好,我给你变个型,变个你更加眼生一点的写法:

Runnable 和 EventListener 都是接口,所以这样的写法和书中的示例代码没有实质上的区别。
然而让人看起来就眼生了一点。
而后其实这个 EventSource 接口也并不影响我最初要给你演示的货色,所以我把它也干掉,代码就能够简化到这个样子:
public class ThisEscape {

    public ThisEscape() {
        new Runnable() {
            @Override
            public void run() {
                doSomething();
            }
        };
    }

    void doSomething() {
    }
}
复制代码
在 ThisEscape 类的无参结构外面,有一个 Runnable 接口的实现,这种写法叫做匿名外部类。
看到外部类,再看到书中提到的 this 逸出,再想起后面刚刚才说的非动态外部类持有外部类的援用你是不是想起了什么?
验证一下你的想法,我通过 javac 编译这个类,而后查看它的 class 文件如下:

咱们果然看到了 this 关键字,所以“this 逸出”中的 this 指的就是书中 ThisEscape 这个类。
逸出,它带来了什么问题呢?
来看看这个代码:

因为 ThisEscape 对象在构造方法还未执行实现时,就通过匿名外部类“逸”了进来,这样内部在应用的时候,比方 doSomething 办法就拿到可能是一个还未齐全实现初始化的对象,就会导致问题。
我感觉书中的这个案例,读者只有是抓住了“外部类”和“this 是谁”这两个关键点,就会比拟容易排汇。
针对“this 逸出”的问题,书中也给出了对应的解决方案:

做个导读,就不细说了,有趣味本人去翻一翻。

正文完
 0