天天看點

深入了解Java虛拟機(三)之垃圾收集算法

深入了解Java虛拟機系列文章

  • 深入了解Java虛拟機(一)之記憶體布局和對象的建立
  • 深入了解Java虛拟機(二)之四種引用和2次标記
  • 深入了解Java虛拟機(四)之JVM調優
  • 深入了解Java虛拟機(五)之Class類檔案結構
  • 深入了解Java虛拟機(六)之類加載機制
  • 深入了解Java虛拟機(七)之Java記憶體模型

垃圾收集算法

标記-清除算法
  • 最基礎的收集算法,包括“标記”和“清除”2個階段
  • 首先标記出所有需要回收的對象,标記過程見前文的2次标記,标記完以後統一回收所有被标記的對象
  • 主要的不足
    • 标記和清除2個階段的效率都不高
    • 标記清除之後會産生大量的不連續的記憶體碎片,記憶體碎片過多會導緻以後為大對象配置設定記憶體時,無法找到足夠的連續記憶體而不得不觸發再一次的GC
複制算法
  • 将記憶體按容量分為大小相等的2塊,每次隻使用其中的一塊。當這一塊記憶體用完了,就将存活的對象複制到另一塊記憶體中,然後一次性清理掉已使用過的記憶體空間。
  • 優點:每次都是對半個記憶體區域進行回收,記憶體配置設定時也不用考慮記憶體碎片等複雜情況,實作簡單,運作高效
  • 不足:将記憶體縮小為原來的一半,代價較高
  • 現在的商業虛拟機一般都采用這種複制算法回收新生代,但不是嚴格按照1:1這樣劃分記憶體。而是分為較大的一塊Eden空間和2塊較小的Survivor空間。HotSpot虛拟機預設Eden和Survivor的比例為8:1。
  • 由于上述Eden和Survivor的劃分,導緻會出現Survivor空間不夠用的情況,這時就需要依賴老年代記憶體進行配置設定擔保(Handle Promotion)。
标記-整理算法
  • 分為“标記”和“整理”2個過程
  • “标記”過程和标記-清除算法一樣
  • 與标記-清除算法不一樣的是,在标記完以後,會讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體。這樣可以避免産生大量的記憶體碎片的問題
  • 一般用于老年代的垃圾收集
分代收集算法
  • 根據對象的存活周期的不同将記憶體分為幾塊,一般把Java堆分為新生代和老年代
  • 新生代用複制算法,老年代一般用标記-清除或是标記-整理算法

算法的實作

可達性分析時如何知道哪些地方存放着對象引用?—OopMap資料結構
  • 在類加載完成的時候, HotSpot就把對象内什麼位置上是什麼類型的資料計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用,這些資訊都使用一組稱為OopMap的資料結構來實作
從哪些位置進入GC?—安全點
  • HotSpot不會為每個指令都生成OopMap,這樣會浪費很多空間。
  • 如上所述,HotSpot會在特定的地方記錄引用的資訊,這些特定的地方就是安全點,也就是可以進入GC的點
  • 安全點不能太少,也不能太多,基本上以“是否具有讓程式長時間執行的特征”為标準進行標明。“長時間執行”的最明顯特征就是指令序列複用,如方法調用、循環跳轉、異常跳轉等。是以具有這些功能的指令才會産生SafePoint。
GC發生時如何讓所有線程停下來?—搶先式中斷和主動式中斷
  • 搶先式中斷:在GC發生時,首先把所有的線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢複線程,讓它“跑”到安全點上
  • 主動式中斷:GC需要中斷線程的時候,不直接對線程操作,而是設定一個标志,每個線程執行時主動去輪詢這個标志,發現中斷标志為真時就自己中斷挂起。輪詢标志的地方和安全點是重合的,另外再加上建立對象需要配置設定記憶體的地方
  • 搶先式中斷已經很少使用
沒有正在執行的線程如何響應JVM的中斷請求?—安全區域(Safe Region)
  • 安全區域是指在一段代碼中,引用關系不會發生變化,在這個區域中開始GC是安全的,可以看作是擴充了的安全點
  • 線程進入安全區域時會辨別自己,這樣GC時就不用管辨別自己進入安全區域的線程
  • 線程在離開安全區域時,會檢查系統是否已經完成了根節點枚舉或是整個GC過程,如果完成就繼續執行下去,如果沒有就必須等待,直到收到可以安全離開安全區域的信号

垃圾收集器

  • 垃圾收集器是記憶體回收的具體實作
  • 新生代垃圾收集器包括:Serial收集器、ParNew收集器、Parallel Scavenge收集器
  • 老年代收集器:CMS收集器、Serial Old收集器、Parallel Old收集器
  • 以及G1收集器
Serial收集器
  • Serial收集器是最基本、發展曆史最悠久的收集器
  • Serial收集器是一個單線程的收集器
  • Serial收集器在進行垃圾收集時,必須暫停所有其他的工作線程,直到收集結束,是以有“Stop The World”的稱号
  • 虛拟機運作在Client模式下的預設新生代收集器
  • 優點:相比于其他單線程的收集器而言,簡單高效。沒有線程切換的開銷,可以獲得最高的單線程收集效率
ParNew收集器
  • 是Serial收集器的多線程版本,多個線程同時進行垃圾收集
  • 除了Serial收集器,目前隻有ParNew收集器能與CMS收集起配合工作
  • 是許多運作在Server模式下的虛拟機的首選的新生代收集器
  • ParNew收集器由于存線上程互動的開銷,在單個CPU環境中不會比Serial收集器有更好的效果
Parallel Scavenge收集器(吞吐量優先收集器)
  • 吞吐量 = 運作使用者代碼的時間/(運作使用者代碼的時間 + 垃圾收集時間)
  • GC停頓時間越短能保證良好的響應速度,适合與使用者互動的程式;高吞吐量可以提高CPU的利用效率,盡快完成運算任務,适合在背景運算不需要太多互動的任務
  • Parallel Scavenge收集器也是多線程的采用複制算法的垃圾收集器
  • Parallel Scavenge收集器的不同之處在于,它的目标是達到一個可控制的吞吐量
  • Parallel Scavenge收集器可以通過打開UseAdaptiveSizePolicy參數來開啟GC的自适應調節政策
Serial Old收集器
  • Serial Old收集器是Serial收集器的老年代版本,是一個單線程收集器,采用标記-整理算法
  • 主要給Client模式下的虛拟機使用
  • 在Server模式下,一般作為JDK1.5以及之前版本中與Parallel Scavenge收集器配合使用;或是作為CMS收集器的備用
Parallel Old收集器
  • Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和标記-整理算法
  • 在注重吞吐量以及CPU資源敏感的場合,與Parallel Scavenge收集器配合使用
CMS收集器
  • 以擷取最短回收停頓時間為目标的收集器
  • 基于标記-清除算法實作
  • 分為四個過程:初始标記、并發标記、重新标記、并發清除
  • 初始标記、重新标記需要暫停所有線程
  • 并發标記和并發清除耗時比較長,但都是和使用者線程同時工作,是以總體上CMS收集器的記憶體回收工作是和使用者線程同時工作的
  • 缺點:
    • 對CPU資源非常敏感
    • 無法處理浮動垃圾,浮動垃圾就是在CMS并發清理垃圾時,使用者線程同時運作産生的垃圾
    • 由于是基于标記-清除算法實作的,會導緻有很多記憶體碎片産生。雖然可以通過開啟UseCMSCompactAtFullCollection參數來在收集器進行FullGC時開啟記憶體碎片整理,但這個碎片整理過程不是并發的,停頓的時間就變長了
G1收集器
  • 是一款面向服務端應用的垃圾收集器
  • 特點:
    • 并行與并發,G1能使用多個CPU來縮短線程暫停的時間,同時通過并發的方式使Java線程不用停下來
    • 分代收集,采用不同的方式處理新生的對象和已經存活了一段時間、熬過多次GC的舊對象
    • 空間整合,G1整體上看是采用标記-整理算法,從局部(2個Region之間)是基于“複制”算法實作的
    • 可預測性的停頓,G1能建立可預測的停頓時間模型
  • G1把Java堆劃分為多個獨立的Region區域,新生代和老年代不再是實體的隔離,它們都是一部分Region的集合
  • G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收獲得的空間大小以及回收所需時間的經驗值),在背景維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的Region
  • G1收集器中每個Region都有一個對應的Rememberer Set用來記錄跨Region的對象引用和跨新生代老年代的引用,在進行記憶體回收時,在GC根節點範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏
  • G1收集器工作過程大緻分為:初始标記、并發标記、最終标記、篩選回收
  • 篩選回收階段首先對各個Region的回收價值進行排序,根據期望的GC停頓時間制定回收計劃。篩選回收階段會停頓使用者線程。

記憶體配置設定政策

對象優先在Eden區配置設定
  • 大部分情況下,對象在新生代的Eden區配置設定,當Eden區沒有足夠的空間時,虛拟機将發生一次MinorGC
  • 通過-Xms和-Xmx參數設定堆的大小,通過-Xmn參數設定新生代的大小,最後通過-XX:SurvivorRatio設定新生代中Eden區和一個Survivor區的空間比例
大對象直接進入老年代
  • 大對象指的是需要大量連續記憶體空間的對象,比如很長的字元串以及數組。經常出現大對象很容易導緻GC
  • 虛拟機提供了參數-XX:PretenureSizeThreshold,大于這個值的對象直接在老年代中配置設定
長期存活的對象将進入老年代
  • 虛拟機為每個對象定義了一個對象年齡計數器
  • 如果對象在Eden區經過一次Minor GC後仍然存活并能被Survivor容納的話,将被移動到Survivor空間,并且對象年齡設為1
  • 對象在Survivor區每熬過一次Minor GC,年齡就增加1歲
  • 當對象的年齡增加到一定程度(預設為15歲),就将會被晉升到老年代
  • 對象晉升到老年代的門檻值可以通過虛拟機參數-XX:MaxTenuringThreshold來設定
動态對象年齡判定
  • 如果在Survivor空間中相同年齡所有對象的大小的總和大于Survivor空間的一半,那年齡大于或等于該年齡的對象就可以直接進入老年代,而不用等到MaxTenuringThreshold中要求的年齡
空間配置設定擔保
  • 在發生Minor GC之前,虛拟機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間。原因是,新生代中的垃圾收集采用的是複制算法,那最壞的情況就是Minor GC之後,新生代中的對象都存活,那Survivor空間中勢必容不下這麼多對象,就要将對象移到老年代,而老年代的空間如果不夠的話就要進行Full GC了
  • 如果上面的條件成立,就會進行Minor GC
  • 如果條件不成立,那就要判斷虛拟機的參數HandlePromotionFailure是否允許擔保失敗,如果不允許,就要進行一次Full GC
  • 如果允許擔保失敗,就會繼續檢查老年代最大可用的連續空間是否大于曆次晉升到老年代對象的平均大小,如果大于,就進行一次Minor GC,否則就進行Full GC
  • 以上的方案說到底是盡量避免不必要的Full GC
  • 需要注意的是JDK6 Update 24之後的規則變為隻要老年代的連續空間大于新生代對象總大小或者曆次晉升的平均大小就會進行Minor GC,否則将進行Full GC。

歡迎關注我的微信公衆号,和我一起學習一起成長!

深入了解Java虛拟機(三)之垃圾收集算法