HotSpot的演算法實現 垃圾回收機制(GC)
上一篇文章垃圾回收機制(GC)從理論上介紹了物件存活判定演算法和垃圾收集演算法,而在HotSpot虛擬機器上實現這些演算法時,必須對演算法的執行效率有嚴格的考量,才能保證虛擬機器高效執行。
列舉根節點
以可達性分析中從GC Roots 節點找引用鏈這個操作為例,可作為GC Roots 的節點主要在全域性性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,現在的很多應用僅僅方法區就有數百兆,如果要逐個檢查這裡面的引用,那麼必然會消耗很多時間。
另外,可達性分析對執行時間的敏感還體現在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中進行—這裡的一致性的意思是指在整個分析期間整個執行系統看起來像被凍結在某個時間點上,不可以出現在分析過程中物件引用關係還在不斷的變化,該點不滿足的話分析結果的準確性就無法得到保證。這點導致GC進行時必須停頓所有Java執行執行緒(Sun稱這件事情為“Stop The World”)的其中一個重要的原因,即使在號稱(幾乎)不會發生停頓的CMS收集器中,列舉根節點時也是必須要停頓的。
目前主流的Java虛擬機器使用的都是準確式GC,所以當執行系統停頓下來後,並不需要一個不漏的檢查完所有的執行上下文和全域性的引用位置,虛擬機器應當是有辦法直接得知哪些地方存在著物件引用。在HotSpot的實現中,是使用一組稱為OopMap的資料結構來達到這個目的的,在類載入完成的時候,HotSpot就把物件內什麼偏移量上是什麼型別的資料計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和暫存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些資訊了。
安全點(Safepoint)
在OopMap的協助下,HotSpot可以快速且準確的完成GC Roots列舉,但一個很現實的問題隨之而來:可能導致引用關係變化,或者說OopMap內容變化的指令非常多,如果為每一條指令都生成對應的OopMap,那麼將需要大量的額外空間,這樣GC 的空間成本將會變得很高。
實際上,HotSpot也的確沒有為每條指令都生成OopMap,前面已經提到,只是在“特定的位置”記錄這些資訊,這些問題稱為(Safepoint),即程式執行時並非所有地方都能停頓下來開始GC,只有到達安全點才能暫停。Safepoint的選定既不能太少以至於讓GC等待時間太長,也不能過於頻繁以至於過分增大執行時負荷。所以,安全點的選定基本上是以程式“是否具有讓程式長時間執行的特徵”為標準進行選定的—因為每條指令執行的時間都非常短暫,程式不太可能因為指令流長度太長這個原因而過長時間執行,“長時間執行”的最明顯特徵就是指令序列複用。例如:方法呼叫、迴圈跳轉、異常跳轉等,所以具有這些功能的指令才會產生Safepoint。
對於Safepoint,另外一個需要考慮的問題是如何在GC發生時讓所有執行緒(這裡不包括執行JNI呼叫的執行緒)都跑到最近的安全點再停頓下來,有兩種方案:
1. 搶先式中斷(Preemptive Suspension):在GC發生時,首先把所有執行緒全部中斷,如果發現有執行緒中斷的地方不在安全點上,就恢復執行緒,讓其跑到安全點上;(幾乎沒有虛擬機器使用這種方式)
2. 主動式中斷(Voluntary Suspension):當GC需要中斷執行緒時,不直接對執行緒操作,僅僅簡單設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起。其中輪詢標誌的地方和安全點是重合的,另外再加上建立物件需要分配記憶體的地方。
安全區域(Safe Region)
上面講述的Safepoint似乎已經完美的解決了如何進入GC的問題,但實際情況卻不一定。Safepoint機制保證了程式執行時,在不長的時間內就會遇到可進入GC的Safepoint。但是,程式不執行的時候呢?所謂的程式不執行就是沒有分配CPU時間,典型的例子就是執行緒處於sleep狀態或者Blocked狀態,這時候執行緒無法響應JVM的中斷請求,“走”到安全的地方去中斷掛起,JVM顯然也不太可能等待執行緒重新被分配CPU時間。這種情況,就需要安全區域來解決了。
安全區域是指在一段程式碼片段之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。可以把安全區域看做是擴充套件了的安全點。
線上程執行到安全區域的程式碼時,首先標識自己進入到了安全區域,這樣,當這段時間內JVM要發起GC時,就不用管標識自己為安全區域狀態的執行緒了;線上程要離開安全區域時,它要檢查是否系統已經完成了列舉根節點(或整個GC過程),如果完成了,那麼執行緒就繼續執行,否則就必須等待到直到收到可以安全離開安全區域的訊號為止。
以上就是HotSpot虛擬機器如何發起記憶體回收的問題。
垃圾收集器
垃圾收集器就是上面講的理論知識的具體實現了。不同虛擬機器所提供的垃圾收集器可能會有很大差別,我們使用的是HotSpot,HotSpot這個虛擬機器所包含的所有收集器如圖:
上圖展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,那說明它們可以搭配使用。虛擬機器所處的區域說明它是屬於新生代收集器還是老年代收集器。多說一句,我們必須要明白一個道理:沒有最好的垃圾收集器,更加沒有萬能的收集器,只能選擇對具體應用最合適的收集器。這也是HotSpot為什麼要實現這麼多收集器的原因。OK,下面一個一個看一下收集器:
1、Serial收集器
最基本、發展歷史最久的收集器,這個收集器是一個採用複製演算法的單執行緒的收集器,單執行緒一方面意味著它只會使用一個CPU或一條執行緒去完成垃圾收集工作,另一方面也意味著它進行垃圾收集時必須暫停其他執行緒的所有工作,直到它收集結束為止。後者意味著,在使用者不可見的情況下要把使用者正常工作的執行緒全部停掉,這對很多應用是難以接受的。不過實際上到目前為止,Serial收集器依然是虛擬機器執行在Client模式下的預設新生代收集器,因為它簡單而高效。使用者桌面應用場景中,分配給虛擬機器管理的記憶體一般來說不會很大,收集幾十兆甚至一兩百兆的新生代停頓時間在幾十毫秒最多一百毫秒,只要不是頻繁發生,這點停頓是完全可以接受的。
2、ParNew收集器
ParNew收集器其實就是Serial收集器的多執行緒版本,除了使用多條執行緒進行垃圾收集外,其餘行為和Serial收集器完全一樣,包括使用的也是複製演算法。ParNew收集器除了多執行緒以外和Serial收集器並沒有太多創新的地方,但是它卻是Server模式下的虛擬機器首選的新生代收集器,其中有一個很重要的和效能無關的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作(看圖)。CMS收集器是一款幾乎可以認為有劃時代意義的垃圾收集器,因為它第一次實現了讓垃圾收集執行緒與使用者執行緒基本上同時工作。ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於執行緒互動的開銷,該收集器在兩個CPU的環境中都不能百分之百保證可以超越Serial收集器。當然,隨著可用CPU數量的增加,它對於GC時系統資源的有效利用還是很有好處的。它預設開啟的收集執行緒數與CPU數量相同,在CPU數量非常多的情況下,可以使用-XX:ParallelGCThreads引數來限制垃圾收集的執行緒數。
3、Parallel收集器
Parallel收集器也是一個新生代收集器,也是用複製演算法的收集器,也是並行的多執行緒收集器,但是它的特點是它的關注點和其他收集器不同。介紹這個收集器主要還是介紹吞吐量的概念。CMS等收集器的關注點是儘可能縮短垃圾收集時使用者執行緒的停頓時間,而Parallel收集器的目標則是打到一個可控制的吞吐量。所謂吞吐量的意思就是CPU用於執行使用者程式碼時間與CPU總消耗時間的比值,即吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間),虛擬機器總執行100分鐘,垃圾收集1分鐘,那吞吐量就是99%。另外,Parallel收集器是虛擬機器執行在Server模式下的預設垃圾收集器。
停頓時間短適合需要與使用者互動的程式,良好的響應速度能提升使用者體驗;高吞吐量則可以高效率利用CPU時間,儘快完成運算任務,主要適合在後臺運算而不需要太多互動的任務。
虛擬機器提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio兩個引數來精確控制最大垃圾收集停頓時間和吞吐量大小。不過不要以為前者越小越好,GC停頓時間的縮短是以犧牲吞吐量和新生代空間換取的。由於與吞吐量關係密切,Parallel收集器也被稱為“吞吐量優先收集器”。Parallel收集器有一個-XX:+UseAdaptiveSizePolicy引數,這是一個開關引數,這個引數開啟之後,就不需要手動指定新生代大小、Eden區和Survivor引數等細節引數了,虛擬機器會根據當親系統的執行情況手機效能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量。如果對於垃圾收集器運作原理不太瞭解,以至於在優化比較困難的時候,使用Parallel收集器配合自適應調節策略,把記憶體管理的調優任務交給虛擬機器去完成將是一個不錯的選擇。
4、Serial Old收集器
Serial收集器的老年代版本,同樣是一個單執行緒收集器,使用“標記-整理演算法”,這個收集器的主要意義也是在於給Client模式下的虛擬機器使用。
5、Parallel Old收集器
Parallel收集器的老年代版本,使用多執行緒和“標記-整理”演算法。這個收集器在JDK 1.6之後的出現,“吞吐量優先收集器”終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel收集器+Parallel Old收集器的組合。
6、CMS收集器
CMS收集器是一種以獲取最短回收停頓時間為目標的老年代收集器。目前很大一部分Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其注重服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗,CMS收集器就非常符合這類應用的需求。CMS收集器從名字就能看出是基於“標記-清除”演算法實現的。
7、G1收集器
G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一,JDK 7 Update 4後開始進入商用。在G1收集器之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1收集器不再是這樣,使用G1收集器時,Java堆的記憶體佈局就與其他收集器有很大差別,它將整個Java堆分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region的集合。G1收集器跟蹤各個Region裡面的垃圾堆積的價值大小,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也是Garbage-First名稱的由來)。這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。
參考:《深入理解Java虛擬機器》 周志明 編著: