天天看點

Shenandoah GC---低延遲垃圾收集器相關概念與G1 GC的比較運作過程Brooks Pointer/轉發指針

相關概念

曆史: Shenandoah GC是由 RedHat公司開發的的新型收集器,14年RedHat把Shenandoah貢獻給了OpenJDK。

設計目标:實作一種能在任何堆記憶體大小下都可以把垃圾收集的停頓時間限制在十毫秒以内的垃圾收集器。

與G1 GC的比較

相同點

  • 同樣基于Region的堆記憶體布局,同樣有着用于存放大對象Humongous Region
  • 預設的回收政策也同樣是優先處理回收價值最大的Region

不同點

堆記憶體管理方面

  • 支援并發的整理算法,G1的回收階段可以是并行的,但卻不能與使用者線程并發
  • 預設不使用分代收集,不會有專門的新生代Region或者老年代Region
  • 去除了G1中耗費記憶體和計算資源去維護的RSet,改為使用名為“連接配接矩陣”的全局資料結構來記錄跨Region的引用,
    • 優點:
      • 降低了處理跨代指針時的記憶集維護消耗
      • 降低了僞共享的發生機率(僞共享相關内容:https://www.yuque.com/lihongjian/fx3n4r/cnhs3h)
    • 連接配接矩陣: 可以了解為一張二維表格,如果Region N有對象指向Region M,就在表格的N行M列上打上一個标記**,**如果Region5中的對象Baz引用了Region3的Foo,Foo又引用了Region1 的Bar,那連接配接矩陣中的5行3列、3行1列就應該被打上标記,回收時通過這張表格就可以得出哪些Region之間産生了跨代引用。

使用者态、核心态相關概念:https://zh.wikipedia.org/wiki/

運作過程

1、2、3階段與G1相同

  1. 初始标記(STW)

标記所有GC Roots能直接關聯到的對象,速度很快,會造成短暫的暫停,時間長短與堆記憶體的大小無關,與對象的多少無關,與存活對象多少也無關

  1. 并發标記

GC線程與使用者線程并發執行,周遊整個對象圖,标記出所有可達的對象,時間長短取決于存活對象的數量以及對象圖的複雜度

  1. 最終标記(STW)

與G1一樣處理剩餘的SATB掃描,并在這個階段統計出回收價值最高的Region,将這些Region構成一組回收集(CSet)

  1. 并發清理

将标記到沒有對象存活的Region清理掉

  1. 并發回收
    1. 在CMS中會删除不再使用的對象,并回收他們占用的記憶體
    2. 在G1中的拷貝存活對象(evacuiation)階段是篩選出部分Region後拷貝到新的Region中去,并将原有的Region清空。
    3. Shenandoah GC要把回收集裡面的存活對象先複制一份到其他未被使用的Region中。
      1. **問題:由于與使用者線程并發執行,使用者線程會對對象不停的進行讀寫操作,但移動對象後整個記憶體中所有指向該對象的引用都需要改變,總不能把所有指向該對象的引用全都修改一遍,那屬實是太麻煩了?**Shenandoah使用讀屏障和被稱為“Brooks Pointer”的轉發指針來解決,下文會進行解釋
  2. 初始引用更新
    1. 前一階段已經複制完對象了,理論上要更新引用了是吧,但事實卻不是這樣的,這個階段其實是建立一個線程的集合點,由于GC是多線程執行的,需要一個集合點來保證所有的GC線程都把自己負責的對象複制完了。
  3. 并發引用更新
    1. 真正開始進行引用更新操作,此階段不需要和并發标記階段一樣周遊整個對象圖來搜尋,隻需要按照記憶體屋裡位址的順序,線性地搜尋出引用類型,把舊值改為新值即可(其實隻是涉及修改被移動對象的對象頭的轉發指針的引用,下文會講到),此階段的時間長短與存活對象的個數有關
  4. 最終引用更新 (STW)
    1. 修正存在于GC Roots中的引用,停頓時間隻與GC Roots數量有關,因為GC Roots不一定參與回收,是以需要更新對應的引用,否則下次GC時對象就不可達了
  5. 并發清理
    1. 經過上邊的階段,整個回收集中所有的Region中已經再無存活對象,這些Region都變成直接垃圾區,是以最後再調用一次并發清理過程來回收這些Region的記憶體空間,供以後新對象配置設定使用。

Brooks Pointer/轉發指針

Brooks Pointer/轉發指針旨在實作對象移動與使用者程式并發的解決方案。

之前的處理方式

在這種概念出現之前,要做類似的并發操作,通常是在被移動對象原有的記憶體上設定保護陷阱(Memory Protection Trap),一旦使用者程式通路到歸屬于舊對象的記憶體空間就會産生自陷中段,進入預設好的異常處理器中,再由其中的代碼邏輯把通路轉發到複制後的新對象上

缺點: 雖然能實作對象移動與使用者線程并發, 但是如果沒有作業系統層面的直接支援,這種方案将導緻使用者态頻繁切換到核心态,代價是非常大的,不能頻繁使用。

Brooks Pointer/轉發指針的處理方案

在原有的對象布局結構的最前面統一增加一個新的引用字段,在正常不處于并發移動的情況下,該引用指向對象自己。

Shenandoah GC---低延遲垃圾收集器相關概念與G1 GC的比較運作過程Brooks Pointer/轉發指針

是不是與對象的通路方式中的“句柄”模式有點像,兩者都是間接性的對象通路方式,差别是句柄通常會統一存儲所在專門的句柄池中,而轉發指針是分散存放在每一個對象頭前面。

優點

當對象擁有了一份新的副本時(複制),隻需要修改舊對象上轉發指針的引用位置,使其指向新對象,便可以将所有對該對象的通路轉發到新的副本上,隻有舊對象的記憶體仍然存在,未被清理掉,虛拟機的記憶體中所有通過舊引用位址通路的代碼便仍然可以使用。

缺點

與其他間接對象通路技術的缺點相同,每次通路對象都會帶來一次額外的轉向開銷(盡管這個開銷已經被優化到隻有一行彙編指令的程度),由于對象通路會被頻繁使用到,仍然是一筆不可忽視的執行成本,與記憶體保護陷阱的方案比起來好了很多。

多線程競争問題

并發讀: 當垃圾收集線程與使用者線程并發讀取對象内容時,無論是讀到舊位址對象還是讀到新位址對象,傳回的内容都是一緻的,是以并發讀沒問題。

并發寫: 對對象的寫操作無疑有如下三種

(1)收集器線程複制出了對象新的副本

(2)使用者線程更新對象的某個字段

(3)收集器更新對象的轉發指針的位址為新位址

想象一下當(1)執行了,(3)執行之前(2)來執行,是不是會出現使用者線程的更新更新到了舊的對象上,此時舊的對象還沒被清理,此時一切還是正常,但是等到舊對象被回收掉後,是不是就有問題了,是以必須保證對轉發指針的同步,Shenandoah采用的是CAS的操作來保證的。

執行頻率問題

Shenandoah通過設定讀寫屏障來處理轉發指針,對象的讀動作是很頻繁的,遠遠大于寫動作(前邊介紹的GC都隻是使用了寫屏障,Shenandoah是第一款介紹的使用的讀屏障的GC)

  • 讀頻率高,自然讀屏障的個數要遠大于寫屏障
  • 在之前垃圾收集器需要的寫屏障的地方又加了轉發操作

計劃在JDK13中将Shenandoah的記憶體屏障模型改進為基于引用通路屏障,即: 指記憶體屏障隻攔截對象中資料類型為引用類型的讀寫操作,不管原生資料類型等其他非引用字段的讀寫,這能夠省去大量對原生對象、對象比較、對象加鎖等場景中設定記憶體屏障帶來的消耗。