前言
許多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技術堆疊,在后臺回復:JVM46,可以獲取一份 46 頁的高清 JVM 調優教程,非常齊全,
運行時堆疊
典型的運行時堆疊
如果你了解過編譯原理,或者程式執行的底層機制,你會知道方法在執行的時候,方法里的變數(區域變數)都是分配在堆疊上的;當然,對于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的堆疊索引,這樣就節約了記憶體空間,
另外,關注公眾號Java技術堆疊,在后臺回復:面試,可以獲取我整理的 JVM 系列面試題和答案,非常齊全,
提醒
上面的“運行時堆疊”和“索引”是為方便引入而故意發明的詞,實際上在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“時大膽去用,但不應當對其有過多依賴,更不能當作是一個普遍規則來推廣,
最后,關注公眾號Java技術堆疊,在后臺回復:JVM46,可以獲取一份 46 頁的高清 JVM 調優教程,非常齊全,
參考:周志明. 深入理解Java虛擬機:JVM高級特性與最佳實踐[M]. 機械工業出版社, 2013.
來源博客:www.polarxiong.com/
近期熱文推薦:
1.600+ 道 Java面試題及答案整理(2021最新版)
2.終于靠開源專案弄到 IntelliJ IDEA 激活碼了,真香!
3.阿里 Mock 工具正式開源,干掉市面上所有 Mock 工具!
4.Spring Cloud 2020.0.0 正式發布,全新顛覆性版本!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/264680.html
標籤:Java
上一篇:Go - 代碼生成工具
