Java對象不再使用時,為什麼要賦值為 null?

點擊上方 "程序員小樂"關注, 星標或置頂一起成長

每天凌晨00點00分, 第一時間與你相約


每日英文

Never expect, never assume, and never demand. Just let it be, because if it's meant to be, it will happen the way you want it to.

永不期待,永不假設,永不強求。順其自然,若是註定發生,必會如你所願。


每日掏心話

其實,許多事從一開始就已預感到了結局,往後所有的折騰,都不過只是為了拖延散場的時間 。


來自:zhantong | 責編:樂樂

鏈接:polarxiong.com/


Java對象不再使用時,為什麼要賦值為 null?

程序員小樂(ID:study_tech)第 812 次推文 圖片來自百度


往日回顧:百度和谷歌到底有什麼區別?看完終於明白了!


00 前言


許多Java開發者都曾聽說過“不使用的對象應手動賦值為null“這句話,而且好多開發者一直信奉著這句話;問其原因,大都是回答“有利於GC更早回收內存,減少內存佔用”,但再往深入問就回答不出來了。


鑑於網上有太多關於此問題的誤導,本文將通過實例,深入JVM剖析“對象不再使用時賦值為null”這一操作存在的意義,供君參考。本文儘量不使用專業術語,但仍需要你對JVM有一些概念。


01 正文


示例代碼


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


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“時大膽去用,但不應當對其有過多依賴,更不能當作是一個普遍規則來推廣。


參考


  • 周志明. 深入理解Java虛擬機:JVM高級特性與最佳實踐[M]. 機械工業出版社, 2013.


    Java對象不再使用時,為什麼要賦值為 null?



歡迎在留言區留下你的觀點,一起討論提高。如果今天的文章讓你有新的啟發,學習能力的提升上有新的認識,歡迎轉發分享給更多人。


猜你還想看


阿里、騰訊、百度、華為、京東最新面試題彙集

統一異常處理介紹及實戰,看這篇就對了!

真正理解Mysql的四種隔離級別,看了都說好!

GitHub宣佈收購npm,微軟或成最大贏家!開源界野蠻競爭影響1200萬開發者

關注訂閱號「程序員小樂」,收看更多精彩內容
嘿,你在看嗎?


分享到:


相關文章: