玩轉Java虛擬機(十二)

打卡學習JVM,第十二天

本人學習過程中所整理的代碼,源碼地址

以下大部分內容摘自《深入理解Java虛擬機》

枚舉根節點

  • 當執行停頓下來後,並不需要一不漏地檢查完所有執行上下文和全局的引用位置,虛擬機應當是有辦法直接得知哪些地方存放着對象引用。在Hotspot的實現中,是使用一組稱爲OopMap的數據結構來達到這個目的的

安全點(Safe Point)

  • 在OopMap的協助下,HotSpot可以快速且準確地完成GC Roots枚舉,但一個很現實的問題隨之而來:可能導致引用關係變化,或者說OopMap內容變化的指令非常多,如果爲每一條指令都生成對應的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得很高
  • 實際上,HotSpot也的確沒有爲每條指令都生成OopMap,前面已經提到,只是在“特定的位置”記錄了這些信息,這些位置稱爲安全點(Safepoint),即程序執行時並非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停
  • Safepoint的選定既不能太少以致於讓GC等待時間太長,也不能過於頻繁以致於過分增大運行時的負荷。所以,安全點的選定基本上是以程序“是否具有讓程序長時間執行的特徵”爲標準進行選定的——因爲每條指令執行的時間都非常短暫,程序不太可能因爲指令流長度太長這個原因而過長時間運行,“長時間執行”的最明顯特徵就是指令序列複用,例如方法調用、循環跳轉、異常跳轉等,所以具有這些功能的指令纔會產生Safepoint
  • 對於Sefepoint,另一個需要考慮的問題是如何在GC發生時讓所有線程(這裏不包括執行JNI調用的線程)都“跑”到最近的安全點上再停頓下來。這裏有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension)

- 搶佔式中斷

  • 不需要線程的執行代碼主動去配合,在GC發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上

- 主動式中斷

  • 當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方

現在幾乎沒有虛擬機實現採用搶先式中斷來暫停線程從而響應GC事件

安全區域(Safe Region)

  • 使用Safe Point似乎已經完美地解決了如何進入GC的問題,但實際情況卻並不一定。Safepoint機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的Safepoint。但是,程序“不執行”的時候呢?所謂的程序不執行就是沒有分配CPU時間,典型的例子就是線程處於Sleep狀態或者Blocked狀態,這時候線程無法響應JVM的中斷請求,“走”到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配CPU時間。對於這種情況,就需要安全區域(Safe Region)來解決
  • 安全區域是指在一段代碼片段之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。我們也可以把Safe Region看做是被擴展了的Safe Point。在線程執行到Safe Region中的代碼時,首先標識自己已經進入了Safe Region,那樣,當在這段時間裏JVM要發起GC時,就不用管標識自己爲Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開Safe Region的信號爲止

CMS收集器

  • 以獲取最短回收停頓時間爲目標,多數應用於互聯網站或者B/S系統的服務器端上
  • CMS是基於標記-清除 算法實現的,整個過程分爲4個步驟:初始標記(只是標記一下GC Roots能直接關聯到的對象),併發標記(進行GC Roots Tracing),重新標記,併發清除
  • 初始標記和重新標記需要STW
  • 重新標記階段是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短
  • 併發收集、低停頓
  • 缺點:
    1、CMS收集器對CPU資源非常敏感
    2、CMS收集器無法處理浮動垃圾(Floating Garbage,就是指在之前判斷該對象不是垃圾,由於用戶線程同時也是在運行過程中的,所以會導致判斷不準確的, 可能在判斷完成之後在清除之前這個對像已經變成了垃圾對象,所以有可能本該此垃圾被回收但是沒有被回收,只能等待下一次GC再將該對象回收,所以這種對像就是浮動垃圾),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。如果在應用中老年代增長不是太快,可能適當調高參數-XX:CMSInitiatingOccupancyFraction的值來提高觸發百分比,以便降低內存回收次數從而獲取更好的性能。要是CMS運行期間預留的內存無法滿足程序需要時,虛擬機將啓動後備預案:臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數-XX:CMSInitiatingOccupancyFraction設置得太高很容易導致大量“Concurrent Mode Failure”失敗,性能反而降低
    3、收集結束時會有大量空間碎片產生,空間碎片過多時,將會給大對象分配帶來很大麻煩,往往出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前進行一次Full GC。CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數(默認就是開啓的),用於在CMS收集器頂不住要進行Full GC時開啓內存碎片的合併整理過程,內存整理的過程是無法併發的,空間碎片問題沒有了,但停頓時間不得不變長
    在這裏插入圖片描述

空間分配擔保

  • 在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。當大量對象在Minor GC後仍然存活,就需要老年代進行空間分配擔保,把Survivor無法容納的對象直接進入老年代。如果老年代判斷到剩餘空間不足(根據以往每一次回收晉升到老年代對象空間的平均值作爲經驗值),則進行一次Full GC。

CMS收集器收集步驟

  • Phase1 :Initial Mark(初始標記)

需要STW,這個階段的目標是:標記那些直接被GC Root引用或者被年輕代存活對象所引用的所有對象
在這裏插入圖片描述

  • Phase2 : Concurrent Mark (併發標記)

遍歷老年代,然後標記所有存活的對象,它會根據上個階段找到的GC Roots遍歷查找,它會與用戶的應用程序併發運行,並不是老年代所有的存活對象都會被標記,因爲在標記期間用戶的程序可能會改變一些引用
在這裏插入圖片描述

  • Phase3 : Concurrent Preclean(併發預先清除)

與應用的線程併發運行,一些對象的引用可能會發生變化,但是這種情況發生時,JVM會將包含這個對象的區域(Card)標記爲Dirty,這也就是Card Marking
在pre-clean階段,那些能夠從Dirty對象到達的對象也會被標記,這個標記做完後,dirty card標記就會被清除了
在這裏插入圖片描述

  • Phase4 : Concurrent Abortable Preclean(併發可能失敗的預先清除)

併發階段,這個階段是爲了儘量承擔STW中最終標記階段的工作。這個階段持續時間依賴於很多的因素,由於是在重複做很多相同的工作,直接滿足一些條件(如重複迭代的次數、完成的工作量或者時鐘時間等)

  • Phase5 : Final Remark(最終重新標記)

第二個STW階段,這個階段的目標是標記老年代所有的存活對象,由於之前的階段是併發執行的,GC線程可能跟不上應用程序的變化,爲了完成標記老年代所有存活對象的目標,STW就非常有必要了
通常CMS的Final Remarj階段會在年輕代儘可能乾淨的時候運行,目的是爲了減少連續STW發生的可能性(年輕代存活對象過多也會導致老年代涉及的存活對象過多),這個階段會比前面幾個階段更復雜一些

至此爲止,標記階段已經完成,開始通過清除算法清除

  • Phase6 : Concurrent Sweep(併發清除)

與用戶的應用程序併發運行,這個階段是爲了清除那些不再使用的對象,回收它們的佔用空間爲將來使用
在這裏插入圖片描述

  • Phase7 : Concurrent Reset(併發重置)

併發執行,它會重設CMS內部的數據結構,爲下次的GC做準備

總結:CMS通過將大量工作分散到併發處理階段來減少STW時間,在這塊做得非常優秀,但CMS收集器無法處理浮動垃圾,並且會導致空間碎片,導致無法分配大對象;對於堆比較大的應用,GC的時間難以預估

測試用例:

VM Option:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC
public class MyTest5 {
    public static void main(String[] args) throws InterruptedException {
        int size = 1024 * 1024;
        byte[] myAlloc1 = new byte[4 * size];
        System.out.println("111111");
        byte[] myAlloc2 = new byte[4 * size];
        System.out.println("222222");
        byte[] myAlloc3 = new byte[4 * size];
        System.out.println("333333");
        Thread.sleep(1000);
        byte[] myAlloc4 = new byte[2 * size];
        System.out.println("444444");
    }
}

打印日誌:

111111
[GC (Allocation Failure) [ParNew: 6322K->685K(9216K), 0.0023811 secs] 6322K->4783K(19456K), 0.0024408 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
222222
[GC (Allocation Failure) [ParNew: 5020K->153K(9216K), 0.0036918 secs] 9119K->9002K(19456K), 0.0037384 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8848K(10240K)] 13098K(19456K), 0.0002609 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-mark-start]
333333
[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (CMS Final Remark) [YG occupancy: 4405 K (9216 K)][Rescan (parallel) , 0.0001423 secs][weak refs processing, 0.0000232 secs][class unloading, 0.0004092 secs][scrub symbol table, 0.0007216 secs][scrub string table, 0.0001729 secs][1 CMS-remark: 8848K(10240K)] 13253K(19456K), 0.0015660 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
444444
Heap
 par new generation   total 9216K, used 7798K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  93% used [0x00000000fec00000, 0x00000000ff3771a8, 0x00000000ff400000)
  from space 1024K,  15% used [0x00000000ff400000, 0x00000000ff426710, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 concurrent mark-sweep generation total 10240K, used 8847K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 3945K, capacity 4568K, committed 4864K, reserved 1056768K
  class space    used 434K, capacity 460K, committed 512K, reserved 1048576K
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章