1. 程式人生 > 實用技巧 >Java中當物件不再使用時,不賦值為null會導致什麼後果 ?

Java中當物件不再使用時,不賦值為null會導致什麼後果 ?

前言

許多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的世界中,物件與物件之間是存在關聯的,我們可以從一個物件訪問到另一個物件。如圖所示。

再仔細想想,這些物件與物件之間構成的引用關係,就像是一張大大的圖;更清楚一點,是眾多的樹。

如果我們找到了所有的樹根,那麼從樹根走下去就能找到所有存活的物件,那麼那些沒有找到的物件,就是已經死亡的了!這樣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“時大膽去用,但不應當對其有過多依賴,更不能當作是一個普遍規則來推廣。

歡迎關注我的微信公眾號「碼農突圍」,分享Python、Java、大資料、機器學習、人工智慧等技術,關注碼農技術提升•職場突圍•思維躍遷,20萬+碼農成長充電第一站,陪有夢想的你一起成長