1. 程式人生 > >深入理解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。

歡迎關注我的微信公眾號,和我一起學習一起成長! AntDream