天天看點

從局部變量說起,關于一個莫得名堂的引用和一個坑

作者:why技術

你好呀,我是歪歪。

今天帶大家盤一個有點意思的基礎知識啊。

有多基礎呢,先給你上個代碼:

從局部變量說起,關于一個莫得名堂的引用和一個坑

請問,上面代碼中,位于 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 這三個單詞并沒有連續的出現在一起過:

https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.1.3
從局部變量說起,關于一個莫得名堂的引用和一個坑

直到我找到了這個地方:

https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html
從局部變量說起,關于一個莫得名堂的引用和一個坑

在 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逸出”的問題,書中也給出了對應的解決方案:

從局部變量說起,關于一個莫得名堂的引用和一個坑

做個導讀,就不細說了,有興趣自己去翻一翻。

繼續閱讀