這是我參與8月更文挑戰的第13天,活動詳情查看:8月更文挑戰java
牛很累,牛快要寫不動了算法
zgc和shenadoah的收集器是面向將來的收集器,目前還處於不斷完善的階段,雖然咱們平時可能不太用的上,可是瞭解和基本掌握它是必須的,關於這一塊網上的內容確實比較少,因此我的仍是使用了書本里面的內容進行總結。數組
另外這兩個垃圾收集器是徹底捨棄分代這個概念的,注意是徹底捨棄,並非相似G1收集器雖然使用了分區可是本質上仍是分代收集的收集器。markdown
因爲這兩個收集器的內容較多,這裏分開進行講解,本篇講解Shenadoah收集器。數據結構
不想看文字的,能夠查看思惟導圖:www.mubucm.com/doc/7L4W-FA…併發
在正式介紹以前,有必要說明一下整個背景,現代的垃圾收集器考慮的點主要爲下面這三個條件:內存佔用,吞吐量,延遲,經過以前的收集器介紹,咱們知道了雖然主流的g1收集器在標記階段實現了併發,可是在初始標記和篩選回收階段仍是須要進行階段性的stop world的,這個垃圾收集器並無作到真正意義上的併發,而且因爲分區+region分代的設計限制,必然會產生垃圾收集的停頓。因此將來的垃圾收集器主要目標將會是面向極低延遲進軍,也就是努力實現用戶線程和垃圾收集器線程的徹底併發運行。oracle
值得一提的是雖然新生的低延遲垃圾收集器拋棄了分代的概念,可是G1的Region分塊以及垃圾停頓模型保留了下來,咱們也能夠看到幾乎全部的垃圾收集器都是基於前人的努力成果進行改進,因此不須要十分恐懼內容很難或者是徹底顛覆想法。eclipse
以前的文章提到了增量更新和原始快照,cms使用的是增量更新,g1使用的是原始快照,另外cms使用標記-清除的算法,免不了內存碎片,而g1雖然使用標記-整理,可是終究仍是須要進行暫停的,因此這是一個很是棘手的問題。jvm
這款收集器是首款非jdk官方開發的垃圾收集器,由redhat公司開發,後續被捐贈給eclipse基金會,目前由eclipse基金會進行維護和管理。雖然Shenadoah從設計的細節來看有不少須要完善的地方,可是確實已經具有了獨立做爲垃圾收集器使用的條件。oop
比較惋惜的是oracle由於商業競爭的問題會把shenandoah經過條件編譯的手段進行排除使用比較麻煩,因此shenadoah只能存在於openJDK沒法在OracleJdk上進行部署,可是這款垃圾收集器依然值得咱們學習。
下面來講一下shenadoah的特色:
Region
和G1收集器的設計原理同樣使用的是region進行分塊,一樣有着大對象的概念,默認的策略也是根據算法回收最有價值的region。
沒有分代和鏈接矩陣
注意是沒有分代的概念,默認不使用分代收集,換言之就是沒有新生代和老年代的說法。那要怎麼設計?Shenandoah的解決方案是使用獨立構建的「鏈接矩陣」全局數據結構來維護region的引用關係,也不要被鏈接矩陣這種名詞給嚇到了,其實本質上是一個二維數組結構,好比咱們在Region N引用了Region M,那麼就會在對應的N行M列上打上一個標記,也就是說全局的對象引用都會經過這個表來維護,這也意味着鏈接矩陣會隨着對象的增加不斷膨脹。
G1收集器是放棄固定分代而是使用分區的設計,然而分區本質上仍是分代的,只不過能夠自由決定屬於哪個分代。
下面直接從書裏面拷了一張圖來顯示鏈接矩陣的設計:
不得不說的是這個鏈接矩陣在設計上是仁者見仁智者見智了,維護一個矩陣雖然很方便可是隨着對象的增多會呈現出指數性的表膨脹,這樣來看仍是一個值得商榷的設計,這一點在後續的垃圾收集器zgc介紹中會提到,zgc發現了鏈接矩陣的問題,採用了一些改進手段來解決表膨脹的問題。
支持併發收集和整理
支持併發收集和整理,能夠實現標記和整理階段徹底和用戶線程併發執行。
那麼這個收集器是如何作到這些事情的呢,在介紹工做流程以前,咱們來聊一下算法的實現細節。
歷史緣由不過多介紹,這裏說明一下這個值的含義:轉發指針。轉發指針是什麼呢?它是用來解決對象移動和用戶程序併發的一種解決方案。
Brooks pointer的工做原理:就是在對象的結構佈局上增長一個新的引用字段,這個引用一般狀況下指向本身,當對象發生轉移的時候,brooks pointer會指向新引用的地址,這樣指向舊引用的對象就能夠修復引用指向新對象,這種結構在形式上和JVM的句柄定位相似,都是使用一種間接的訪問形式,差異是轉發指針會分散存在對象頭內部。(以前咱們討論過對象頭是動態擴展的格式)
這種設計形式也有點相似於鏈表的設計形式。
補充:以前如何解決對象引用問題?
使用的是一種在原有的對象內存之上設置保護陷阱+異常處理的方式,一旦出現訪問舊對象的行爲,就會進入到保護陷阱當中,而且進入異常處理器進行代碼邏輯和引用的修復。這種方式看起來十分的有效,可是若是沒有操做系統的支持,就須要經過不斷的用戶態到內核態的切換,須要耗費更多的上下文切換資源,也是一種很是耗費性能的妥協辦法。
缺點:
雖然轉發指針被優化到只有一行彙編指令的程度,可是依然要消耗對象訪問的效率,固然這個方案毫無疑問是比內存陷阱要好,
併發問題:
轉發指針的設計意味着他必然有併發的問題,若是發生併發操做,就須要保證寫操做必須是在新複製的對象下面,不妨考慮下面的問題:
若是不防範這三個問題,就會致使用戶線程的對象變動都是操做舊對象,因此必須針對指針的訪問操做採起同步的措施。解決辦法和對象的引用分配方式也是相似的也是使用CAS+更新失敗重試的操做機制。
最後,還須要注意的是Shenadoah必須使用讀寫屏障去維護brooks pointer(併發問題決定了要時刻保持同步),這個代價是很是大的。下面咱們接着來說講讀寫屏障的問題。
shenandoah不只使用了寫屏障還使用了讀屏障,讀屏障也是相似對象引用操做的一個AOP的切面,咱們都知道對象的讀操做確定是要多於寫操做的,因此使用讀屏障的代價要大不少。
寫屏障的概念能夠看專欄以前的文章:深刻理解JVM - Hotspot算法細節#寫屏障
固然Shenandoah開發者也意識到這個問題,在JDK13的版本中,改用了基於「引用訪問屏障」的方式解決讀屏障的問題,「引用訪問屏障」指的是隻攔截對象相似是引用類型的數據進行訪問屏障的攔截,這樣就能夠省去一些原生類型併發修改訪問的操做,減小龐大的讀屏障維護開銷。
從這裏也能夠看出來Redhat的開發團隊在設計jvm垃圾收集器上的經驗缺少,可是能夠及時調整解決問題。
shenandoah的工做步驟能夠劃分爲9個步驟,最新版本的shenandoah還在初始標記的步驟前面增長了三個步驟,簡單理解爲分代收集當中的Minor GC操做便可。
接下來講一下具體的步驟:
併發標記、併發回收、併發引用更新這三個階段是最重要的,重點記憶便可。
下面的圖是從官方的wiki扒過來的:
Init Mark:初始標記
Final Mark:最終標記
Init-UR:初始引用更新
Final-UR:最終引用更新
官方有一張對比圖來顯示Shenandoah的垃圾收集耗時對比,從圖中能夠看到作到了幾乎無延遲的垃圾收集:
Shenadoah收集器是收款非JDK官方開發的收集器,然而很遺憾的是,由於商業競爭關係,他只存在於OpenJDK,沒有被商用,而且後續因爲更加ZGC的開發,Shenadoah的做用也在逐漸減小,可是不得不認可的做爲沒有JVM垃圾收集器開發經驗的開發者們開發的收集器,這款收集器知足了要求而且十分值得借鑑和學習。
另外能夠看到即便是簡化工做原理,現代的垃圾收集器也已經十分複雜了,因爲目前大部分開發者仍是使用JDK8和G1等垃圾收集器,因此這些垃圾收集器在目前看來仍是屬於面向將來的收集器,可是毫無疑問咱們須要不斷的學習。
Shenandoah收集器的JVM參數案例:
java -XX:+UseShenandoahGC -XX:ShenandoahGCHeuristics=passive -Xlog:gc
這本書講述這款垃圾收集器的內容算是比較粗淺,可是對於咱們瞭解這款收集器來講算是足夠了。想要了解更多內容,我的建議直接找上面提供的官方WIKI入手,畢竟開發出來的人對這個東西纔是最瞭解的。