天天看點

有沒有搞錯?Java 對象不使用時,為什麼要指派 null?

有沒有搞錯?Java 對象不使用時,為什麼要指派 null?

>>号外:關注“Java精選”公衆号,回複“2021面試題”關鍵詞,領取全套500多份Java面試題檔案。

前言

最近,許多Java開發者都在讨論說,“不使用的對象應手動指派為null“ 這句話,而且好多開發者一直信奉着這句話;問其原因,大都是回答“有利于GC更早回收記憶體,減少記憶體占用”,但再往深入問就回答不出來了。

鑒于網上有太多關于此問題的誤導,本文将通過執行個體,深入JVM剖析“對象不再使用時指派為null”這一操作存在的意義,供君參考。本文盡量不使用專業術語,但仍需要你對JVM有一些概念。

示例代碼

我們來看看一段非常簡單的代碼:

public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
    }
    System.gc();
}
           

我們在if中執行個體化了一個數組

placeHolder

,然後在if的作用域外通過

System.gc();

手動觸發了GC,其用意是回收

placeHolder

,因為

placeHolder

已經無法通路到了。來看看輸出:

65536
[GC 68239K->65952K(125952K), 0.0014820 secs]
[Full GC 65952K->65881K(125952K), 0.0093860 secs]
           

Full GC 65952K->65881K(125952K)

代表的意思是:本次GC後,記憶體占用從65952K降到了65881K。意思其實是說GC沒有将

placeHolder

回收掉,是不是不可思議?

下面來看看遵循“不使用的對象應手動指派為null“的情況:

public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
        placeHolder = null;
    }
    System.gc();
}
           

其輸出為:

65536
[GC 68239K->65952K(125952K), 0.0014910 secs]
[Full GC 65952K->345K(125952K), 0.0099610 secs]
           

這次GC後記憶體占用下降到了345K,即

placeHolder

被成功回收了!對比兩段代碼,僅僅将

placeHolder

指派為null就解決了GC的問題,真應該感謝“不使用的對象應手動指派為null“。

等等,為什麼例子裡

placeHolder

不指派為null,GC就“發現不了”

placeHolder

該回收呢?這才是問題的關鍵所在。

運作時棧

典型的運作時棧

如果你了解過編譯原理,或者程式執行的底層機制,你會知道方法在執行的時候,方法裡的變量(局部變量)都是配置設定在棧上的;當然,對于Java來說,new出來的對象是在堆中,但棧中也會有這個對象的指針,和int一樣。

比如對于下面這段代碼:

public static void main(String[] args) {
    int a = 1;
    int b = 2;
    int c = a + b;
  }
           

其運作時棧的狀态可以了解成:

索引 變量
1 a
2 b
3 c

“索引”表示變量在棧中的序号,根據方法内代碼執行的先後順序,變量被按順序放在棧中。

再比如:

public static void main(String[] args) {
        if (true) {
        int a = 1;
        int b = 2;
        int c = a + b;
        }
        int d = 4;
        }
           

這時運作時棧就是:

索引 變量
1 a
2 b
3 c
4 d

容易了解吧?其實仔細想想上面這個例子的運作時棧是有優化空間的。

Java的棧優化

上面的例子,

main()

方法運作時占用了4個棧索引空間,但實際上不需要占用這麼多。當if執行完後,變量

a

b

c

都不可能再通路到了,是以它們占用的1~3的棧索引是可以“回收”掉的,比如像這樣:

索引 變量
1 a
2 b
3 c
1 d

變量

d

重用了變量

a

的棧索引,這樣就節約了記憶體空間。

提醒

上面的“運作時棧”和“索引”是為友善引入而故意發明的詞,實際上在JVM中,它們的名字分别叫做“局部變量表”和“Slot”。而且局部變量表在編譯時即已确定,不需要等到“運作時”。還請注意

GC一瞥

這裡來簡單講講主流GC裡非常簡單的一小塊:如何确定對象可以被回收。另一種表達是,如何确定對象是存活的。

仔細想想,Java的世界中,對象與對象之間是存在關聯的,我們可以從一個對象通路到另一個對象。如圖所示。

有沒有搞錯?Java 對象不使用時,為什麼要指派 null?

再仔細想想,這些對象與對象之間構成的引用關系,就像是一張大大的圖;更清楚一點,是衆多的樹。

如果我們找到了所有的樹根,那麼從樹根走下去就能找到所有存活的對象,那麼那些沒有找到的對象,就是已經死亡的了!這樣GC就可以把那些對象回收掉了。

現在的問題是,怎麼找到樹根呢?JVM早有規定,其中一個就是:棧中引用的對象。也就是說,隻要堆中的這個對象,在棧中還存在引用,就會被認定是存活的。

提醒

上面介紹的确定對象可以被回收的算法,其名字是“可達性分析算法”。

JVM的“bug”

我們再來回頭看看最開始的例子:

public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
    }
    System.gc();
}
           

看看其運作時棧:

LocalVariableTable:
Start  Length  Slot  Name   Signature
    0      21     0  args   [Ljava/lang/String;
    5      12     1 placeHolder   [B
           

棧中第一個索引是方法傳入參數

args

,其類型為

String[]

;第二個索引是

placeHolder

,其類型為

byte[]

聯系前面的内容,我們推斷

placeHolder

沒有被回收的原因:

System.gc();

觸發GC時,

main()

方法的運作時棧中,還存在有對

args

placeHolder

的引用,GC判斷這兩個對象都是存活的,不進行回收。 也就是說,代碼在離開if後,雖然已經離開了

placeHolder

的作用域,但在此之後,沒有任何對運作時棧的讀寫,

placeHolder

所在的索引還沒有被其他變量重用,是以GC判斷其為存活。

為了驗證這一推斷,我們在

System.gc();

之前再聲明一個變量,按照之前提到的“Java的棧優化”,這個變量會重用

placeHolder

的索引。

public static void main(String[] args) {
    if (true) {
        byte[] placeHolder = new byte[64 * 1024 * 1024];
        System.out.println(placeHolder.length / 1024);
    }
    int replacer = 1;
    System.gc();
}
           

看看其運作時棧:

LocalVariableTable:
Start  Length  Slot  Name   Signature
    0      23     0  args   [Ljava/lang/String;
    5      12     1 placeHolder   [B
   19       4     1 replacer   I
           

不出所料,

replacer

重用了

placeHolder

的索引。來看看GC情況:

65536
[GC 68239K->65984K(125952K), 0.0011620 secs]
[Full GC 65984K->345K(125952K), 0.0095220 secs]
           

placeHolder

被成功回收了!我們的推斷也被驗證了。

再從運作時棧來看,加上

int replacer = 1;

和将

placeHolder

指派為null起到了同樣的作用:斷開堆中

placeHolder

和棧的聯系,讓GC判斷

placeHolder

已經死亡。

現在算是理清了“不使用的對象應手動指派為null“的原理了,一切根源都是來自于JVM的一個“bug”:代碼離開變量作用域時,并不會自動切斷其與堆的聯系。為什麼這個“bug”一直存在?你不覺得出現這種情況的機率太小了麼?算是一個tradeoff了。

總結

希望看到這裡你已經明白了“不使用的對象應手動指派為null“這句話背後的奧義。我比較贊同《深入了解Java虛拟機》作者的觀點:在需要“不使用的對象應手動指派為null“時大膽去用,但不應當對其有過多依賴,更不能當作是一個普遍規則來推廣。

作者:category

http://olarxiong.com/category/java/

往期精選  點選标題可跳轉

面試官問:Java 中如何處理含有泛型的 JSON 反序列化問題?

MySQL 資料庫中 52 條 SQL 語句性能優化方法,幹貨必收藏!

“裝 X ”就是牛,試一試 IDEA 解決 Maven 依賴沖突的超級神器!

如何用 Spring Cloud 建構面向企業的大型分布式、微服務快速開發架構?

代碼總是被嫌棄寫的太爛?裝上這個 IDEA 插件再試試!

Java 中注解與反射的使用方法及場景分析,有必要解釋一下!

終于來了,IDEA 2021.1 正式版本釋出,一起看看又有哪些神奇功能!

你還在用 Logback ?Log4j2 功能與異步性能已經無敵了,還不快試試?

Spring Boot 項目不同環境打包配置與Shell腳本部署實踐,太實用了!

Spring boot 項目中如何優雅停止服務的五種方法,值得收藏!

Java 中什麼是 IO 流,位元組流、字元流兩者差別,緩沖流原理代碼剖析

點個贊,就知道你“在看”!