我看不少資料在介紹GC Root
時,並無說棧幀的操做數棧上引用的對象也是GC Root
,包括我去翻閱《深刻理解Java虛擬機》這本書也是同樣。因此我纔好奇。java
爲何我會以爲操做數棧上引用的對象也應該是GC Root
節點?算法
假設在垃圾回收標記階段,因爲併發標誌(如cms
、g1
),此時若是用戶線程在方法中new
一個對象,執行new
字節碼指令時,new
出來的對象的引用是保存在操做數棧頂的,此時並未保存回本地變量表,也並不必定要保存回本地變量表,那麼這個新new的對象豈不是會被回收。安全
New
一個對象,並不必定會保存回本地變量表,如併發
public class Main {
public void hello(){
System.out.println("wujiuye");
}
public static void main(String[] args) {
new Main().hello();
}
}
複製代碼
建立出來的對象Main是不會存儲到本地變量表的。 字節碼以下:框架
帶着疑問,我翻閱GC
垃圾回收知識點相關資料,纔想起Safepoint
。若是說安全點在方法執行以前或以後,就不須要將操做數棧引用的對象做爲GC Root
。異步
關於安全點Safepoint
:jvm
一、已經掛起的線程處於安全點(安全區域),如WAIT
、BLOCK
狀態。只有處於running
狀態的線程須要等待執行到安全點。 二、方法調用指令是安全點,即被調用的方法執行以前。方法結束出口處是安全點,如ruturn
指令以前。 三、循環體在進入下一次以前是安全點。由於不少循環都是耗時操做,如循環一百萬次,jvm
等待多個線程執行完循環再進行垃圾回收,那麼進程就會處於假死狀態。工具
第三點循環體結束進入下一次以前,也能夠把循環體看成一個方法,不管這個方法作什麼事情,這個方法每次執行結束都不會存在建立對象未保存回本地方法表的狀況。所以,沒有必要考慮將操做數棧引用的對象做爲GC Root
,證實了個人想法是錯誤的。性能
在JIT
執行方式下,JIT
編譯的時候直接把Safepoint
的檢查代碼加入了生成的本地代碼。當JVM
須要讓Java
線程進入Safepoint
時,只須要設置一個標誌位,讓Java
線程運行到Safepoint
時主動檢查這個標誌位,若是標誌被設置,那麼線程停頓,若是沒有被設置,那麼繼續執行。如HotSpot
在x86
中爲輪詢Safepoint
會生成一條相似於test
彙編指令。學習
使用JITWatch
工具,能夠驗證。JITWatch
還提供了一個測試保存點的demo
:SafePointTest
。所以連demo都不須要寫了。關於JITWatch
能夠看下我前面寫的這篇文章《JITWatch查看字節碼被JIT編譯後的彙編代碼》。
能夠看到,incCounter
方法反編譯爲彙編代碼後,在retq
這條返回指令以前插入了一條test
指令,也是在return
字節碼指令處。
既然說到這,咱們也一併分析循環體在每進入下一次循環以前插入Safepoint
的檢查。
從圖中能夠看出,對應字節碼是goto
指令的地方,也就是每一次循環結束的地方,都會判斷是否須要跳轉執行text
指令(彙編)。
這裏還能夠引伸出一個知識點,就是逃逸分析。也是爲了提高性能。JIT
即時編譯器會將屢次被執行的字節碼編譯爲機器碼,同時也會分析方法體內的對象建立。若是方法體內建立的對象沒有逃離出方法體以外,即不會被別的地方引用,沒有別的線程使用,那麼就不須要將對象分配到堆中,而是直接分配到虛擬機棧上。當對象分配在虛擬機棧上,對象的生命週期就是對象被建立到方法執行結束,隨着棧幀的出棧而滅亡。
還記得Jvm
調優參數-Xss
嗎,這是配置虛擬機棧的大小,默認爲1m
大小,在Linux
64
位操做系統下最小爲228kb
,通常設置爲最小256kb
。該值的設置須要考慮線程上方法調用的深度,以及方法調用棧上每一個方法的操做數棧的深度和本地變量表的長度。若是存在因逃逸分析而將對象分配在棧上的可能,還須要將此估算進棧的大小。
圖中的代碼片斷是我從以前我寫的一個異步框架截取的,使用asm
生成字節碼,經過調用Method Visitor
的visitMaxs
方法設置操做數棧的深度和本地變量表的長度。也就是說java
代碼在編譯成字節碼以後,操做數棧的深度和本地變量表的長度就已是肯定的了。
大家在學習垃圾回收的時候,有沒有想過爲何gc
回收時要中止全部用戶線程呢?
若是停留在表面理解,gc
回收後須要爲解決內存碎片爲,須要整理內存,如Parallel Old
的老年代回收,使用標誌-整理算法。整理意味着對象的地址會改變,所以gc
後須要修正對象的引用。再如,對象重新生代進入老年代。
CMS
併發標誌也會有STW
,在初始標誌和重標誌兩個階段。由於被標誌的對象還會有被用戶線程修改引用的可能,而在重標誌階段就不得不從新遍歷那些已經修改過引用的對象。只是能夠下降總的STW
時間。更深層次的還要去了解卡表、寫屏障。