前言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的世界中,對象與對象之間是存在關聯的,咱們能夠從一個對象訪問到另外一個對象。如圖所示。
再仔細想一想,這些對象與對象之間構成的引用關係,就像是一張大大的圖;更清楚一點,是衆多的樹。
若是咱們找到了全部的樹根,那麼從樹根走下去就能找到全部存活的對象,那麼那些沒有找到的對象,就是已經死亡的了!這樣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萬+碼農成長充電第一站,陪有夢想的你一塊兒成長