你好呀,我是歪歪。

今日带咱们盘一个有点意思的根底知识啊。

有多根底呢,先给你上个代码:

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

请问,上面代码中,坐落 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,回归根源,剖析起来就简单一点了:

publicclassOuter{
privateintnum=0;
publicintgetNum(){
returnnum;
}
publicvoidsetNum(intnum){
this.num=num;
}
//内部类
classInner{
privatevoidcallOuterMethod(){
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 引证逸出。

我第一次看到的时分,整个人都是懵的,看了好几遍都没看懂:

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

然后就跳过了…

直到好久之后,我才明白作者想要表达的意思。

现在我就带你盘一盘这个代码,把它盘明白。

我先把书上的代码补全,悉数代码是这样的:

publicclassThisEscape{
publicThisEscape(EventSourcesource){
source.registerListener(newEventListener(){
publicvoidonEvent(Evente){
doSomething(e);
}
});
}
voiddoSomething(Evente){
}
interfaceEventSource{
voidregisterListener(EventListenere);
}
interfaceEventListener{
voidonEvent(Evente);
}
interfaceEvent{
}
}

代码要是你一眼看不明白,没联系,主要是关注 EventListener 这个玩意,你看它其实是一个接口对不对。

好,我给你变个型,变个你更加眼熟一点的写法:

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

Runnable 和 EventListener 都是接口,所以这样的写法和书中的示例代码没有本质上的差异。

可是让人看起来就眼熟了一点。

然后其实这个 EventSource 接口也并不影响我最终要给你演示的东西,所以我把它也干掉,代码就能够简化到这个姿态:

publicclassThisEscape{
publicThisEscape(){
newRunnable(){
@Override
publicvoidrun(){
doSomething();
}
};
}
voiddoSomething(){
}
}

在 ThisEscape 类的无参结构里边,有一个 Runnable 接口的完结,这种写法叫做匿名内部类。

看到内部类,再看到书中提到的 this 逸出,再想起前面刚刚才说的非静态内部类持有外部类的引证你是不是想起了什么?

验证一下你的想法,我经过 javac 编译这个类,然后查看它的 class 文件如下:

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

咱们果然看到了 this 要害字,所以 “this 逸出”中的 this 指的便是书中 ThisEscape 这个类。

逸出,它带来了什么问题呢?

来看看这个代码:

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

因为 ThisEscape 目标在结构办法还未履行完结时,就经过匿名内部类“逸”了出去,这样外部在运用的时分,比方 doSomething 办法就拿到可能是一个还未彻底完结初始化的目标,就会导致问题。

我觉得书中的这个事例,读者只要是抓住了“内部类”和“this是谁”这两个要害点,就会比较简单吸收。

针对“this逸出”的问题,书中也给出了对应的处理方案:

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

做个导读,就不细说了,有兴趣自己去翻一翻。