JVM(2):垃圾收集器與gc

1.java堆的基本結構與一個對象在堆中的分配

在這裏插入圖片描述
其中的Eden,Survivor0和Survivor1被稱爲新生代,Tenured被稱爲老年代。
當一個對象被new出來之後,如果他非常的大,那麼直接裝入Tenured中。否則裝入Eden中,在一次GC之後,Eden中存活下來的對象就複製到Survivor0中。接下來的GC中如果Survivor0中存活下來的對象就複製到Survivor1中,接下來存活的對象就在Survivor0和Survivor1中不斷移動,如果經過比較多次的GC之後Survivor0和Survivor1中還有對象存在,則將他移動到Tenured中。
Survivor0和Survivor1使用的是標記-複製算法,Tenured中使用的是標記-整理算法。

2.如何判斷對象已死?

引用計數算法:
我們可以爲每一個對象設置一個count引用計數器,每當一個地方引用它的時候,計數器就加1,當引用失效的時候,計數器就減1;當計數器的值爲0的時候,就代表着這個對象不會再被使用了,這個時候就可以對該對象進行回收。
這個算法十分簡單,但是並沒有被廣泛的使用。因爲這個看似簡單的原理需要考慮很多例外的情況。
可達性算法:
這是當前比較主流的判斷對象是否應該被回收的方式。基本思路就是通過一系列被稱爲“GC Roots”的跟對象作爲起始節點集,如果某個對象到GC Roots之間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象已經不可達的時候,則證明這個對象不可能再被使用了。
在這裏插入圖片描述
由上圖可以出Object A,Object B,Object C和Object D對於GC Roots都是可達的,因此表示這幾個對象還存在對於他們的引用,但是Object E和Object F對於GC Roots都是不可達的,則他們就應該被回收。這個時候就出現了一個問題,什麼樣的東西可以被看作GC Roots?
可以作爲GC Roots的對象:
(1)在虛擬機棧(本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用的參數、局部變量、臨時變量等。
(2)在方法區中類靜態屬性引用的對象,譬如java類的引用類型靜態變量。
(3)在方法區中常量引用的對象。
(4)在本地方法棧中的Native方法引用的對象。
(5)java虛擬機內部的引用。
(6)所有被同步鎖(synchronized關鍵字)持有的對象。
(7)反映java虛擬機內部情況的JMXBean、JVMTI中註冊的回調,本地代碼緩存等。

3.關於引用

jdk1.2之後,java對引用的概念進行了擴充,將引用分爲強引用(Strongly Reference),軟引用(Soft Reference),弱引用(Weak Reference)和虛引用(Phantom Reference)。這四種引用的強度依次減弱。
強引用(Strongly Reference):就是最傳統的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類似“Object obj = new Object()”這種引用關係。無論何時,只要強引用存在,垃圾收集器就永遠不會回收被引用的對象。
軟引用(Soft Reference):用來描述一些還有用,但非必須的對象。只要有被弱引用的對象,在系統將要發出內存溢出異常之前,會把這些對象列入回收範圍之中進行第二次回收,如果這次回收之後還沒有足夠的內存,纔會拋出內存溢出異常。
弱引用(Weak Reference):與軟引用的定義類似,但比軟引用更弱,被弱引用的對象只能生存到下一次垃圾收集爲止,當垃圾收集器開始工作時,無論內存是否足夠,都會回收掉被弱引用的對象。
虛引用(Phantom Reference):也稱“幽靈引用”和“幻影引用”。無法通過虛引用來取得一個對象的實例。它存在的唯一目的是爲了能在這個對象被垃圾收集器收集的時候收到一個系統的通知。

即使在可達性算法中被判定爲不可達的對象,是否就一定救“非死不可”呢?
一個對象在被垃圾收集器回收的過程中總需要經歷兩個階段。第一個階段就是被判定爲不可達的狀態,這時候就進入第二個階段,這個階段會對對象進行篩選,即是否有必要執行finalize()方法進行回收。如果該對象沒有覆蓋finalize()或者已經調用過finalize()方法,則被判定爲沒有必要執行。如果被判定爲有必要執行,則虛擬機會將該對象放置在一個F-Queue的隊列中,並在稍後由一條由虛擬機自動建立的,低優先級的Finalizer線程去執行finalize()方法。但是虛擬機只負責開始這個方法,不負責他完整的運行完,這是因爲如果某個對象的finalize()方法執行緩慢或者發生其他錯誤,則就會影響隊列中的其他對象。finalize()方法是對象逃脫死亡的最後一個機會,稍後收集器將對F-Queue中的對象進行第二次標記,如果對象在finalize()中成功拯救自己——即重新與任意對象建立關係鏈,則可以避免被回收。

方法區的垃圾回收效率要低於堆區的垃圾回收效率。

4.垃圾收集算法

當前的大多是商用虛擬機的垃圾收集器,都遵循“分代收集”的理論。主要是建立在兩個假說之上:
(1)弱分代假說:絕大多數對象都是朝生夕滅的。
(2)強分代假說:熬過越多次垃圾收集過程的對象就越難以消亡。
即虛擬機應該劃分出多個不同的區域,然後將回收對象根據年齡分配到不同的區域之中,對他們採取不同的使用回收策略。

在現在的虛擬機中,設計者至少會把堆劃分爲新生代(Young Generation)和老年代(Old Generation)兩個區域,顧名思義,新生代區域中在每一次垃圾回收的過程中都會有大量的對象死去,存活的少量對象將會逐步晉升到老年代中進行存放。

標記-清除算法:
這是最早出現的垃圾收集算法。算法分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,標記完成後再統一回收點所有被標記的對象。
這個算法存在兩個主要缺陷:
1.對一個對象進行標記和清除兩個階段的操作會隨着對象數量的增多而變得低效。
2.該算法回收之後的區域會變得碎片化,空間難以再次利用。

標記-複製算法:
簡稱爲複製算法。主要是爲了解決標記-清除算法在對象數量較多時執行效率慢的問題。
它將內存分爲大小相同的兩個部分,每次分配的時候只在其中一個部分進行分配,當這個部分滿了之後,就執行垃圾回收過程,並將在垃圾回收中存活的內存塊複製到另一個部分保留下來,然後清空第一個部分用來下一輪的分配。一般經過垃圾回收之後存活的內存塊的數量比被回收的數量少得多,因此複製的開銷並不會很大。而且也解決了碎片化的問題,每次分配只需移動第一個部分的指針,並且清楚也是將第一個部分全部清除,不存在部分清楚部分保留的碎片化情況。但是這個算法也存在着響應的缺陷,就是可利用的麼內存只剩下原來的一半了。
現在的大部分虛擬機都優先使用這種算法,但是內存的分配並不是嚴格的按照1:1進行區分,因爲絕大部分的對象都無法在第一輪垃圾回收之後存活。

標記-整理算法:
標記-複製算法如果在存活的對象比較多的時候就顯得不是那麼合適了,因爲需要進行大量的複製工作。
標記-整理算法其實前面的步驟與標記-清除算法是一樣的,不過它並不是簡單的對對象進行回收,而是先把存活的對象集中移動到內存的一段,然後再對回收對象進行回收。他解決了內存碎片化的問題,但是又引入了一個新的問題,移動後再進行回收真的是百利無害的操作嗎?
顯然他的優點是規避了內存碎片化的問題,但是當在像老年代區域中有大量對象存活的情況下,移動就是一項十分費時費力的操作了,因爲在移動的過程中就必須全程暫停用戶應用程序,即“Stop The World”。
可以簡單的理解爲,如果移動,內存回收就會顯得更復雜;如果不移動,內存分配就會顯得更復雜。

5.HotSpot的算法細節實現

根節點的枚舉:
無論是效率多高的虛擬機,在進行根節點枚舉(查找GC Roots)的時候,都必須暫停用戶線程,即和整理內存碎片一樣需要“Stop The World”。但是因爲現在的虛擬機都是使用的準確式垃圾收集,HotSpot中使用一組成爲OopMap的數據結構來存儲每個對象的信息,這樣收集器在掃描的時候就可以直接得到這些信息,而不需要在GC RootsSet中一個一個的遍歷查找。

安全點:
一個線程並不是在任意時候都可以停止下來進行垃圾回收過程的,因爲這樣會使得整個系統的開銷變得很大而且線程的執行比較混亂。因此我們在每個線程中設置安全點,只有當執行到安全點的時候才允許進行垃圾回收過程。那麼怎麼讓系統中的所有線程都達到安全點以進行垃圾回收呢?有搶佔式中斷和主動式中斷兩種方式。
搶佔式中斷(Preemptive Suspension):系統在垃圾回收發生時,先中斷所有線程,如果發現還有線程沒有到達安全點,就恢復這條線程繼續執行。
主動式中斷(Voluntary Suspension):當系統需要垃圾回收時,不直接對線程進行操作,而是爲每個線程設置一個標誌位,各個線程在執行過程中不斷去輪詢這個標誌位,一旦發現中斷標誌爲真的時候,就自己在最近的安全點上掛起。

安全區域:
安全點保證了線程執行過程中不必等待太久就會遇到可以進入垃圾收集過程的安全點。但是如果程序“不執行”呢?例如線程處於“Block”或者“Sleep”狀態。因此就引入了安全區域進行解決。
安全區域是指能夠確保某一段代碼片段之中,引用關係不會發生變化,因此在這個區域之中任意地方開始垃圾收集都是安全的。也可以看做被拉長了的安全點。
當線程要進入安全區域的時候,就會像虛擬機報告,那麼之後虛擬機在需要執行垃圾回收的一系列操作的時候就不需要管這些處在安全區域的線程;當線程要離開安全區域的時候,需要先檢測虛擬機是否已經完成了根節點枚舉(或其他需要暫停所有用戶線程的操作),如果完成了,就直接走出安全區域即可,否則就要一直等待知道虛擬機完成這些操作。

併發的可達性分析:
由於各種技術的加持(Oop Map),對於GC Roots對象的枚舉已經不再是制約垃圾回收過程效率的瓶頸了,相對的,隨着對象的逐漸增多,從GC Roots延伸出去的對象的數量與複雜度不斷地增加,成爲了影響停頓時間的主要因素。
引入三色圖來分析一下這一過程:
a.白色:表示對象尚未被垃圾收集器訪問過。顯然在可達性分析剛剛開始所有的對象都是白色的;若在分析結束階段,仍然是白色的對象,就是不可達的對象。
b.黑色:表示對象已經被垃圾收集器訪問過了,且這個對象的所有引用都已經掃描過了。黑色的對象代表掃描過的,安全存活的,如果有其他對象的引用指向黑色的對象,則無需對他進行掃描,黑色對象不可能直接指向白色的對象。
c.灰色:表示對象已經被垃圾收集器訪問過,但是還存在至少一個引用還沒有被掃描過。
在這裏插入圖片描述
研究發現,當同時滿足一下兩個條件的時候,就會產生“對象消失”的問題,即原本應該是黑色的對象被誤標成白色:
a.賦值器插入了一條或多條從黑色對象到白色對象的新引用;
b.賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用。
因此我們只需要破壞兩個條件中的任意一個就可以解決“對象消失”的問題。由此產生了兩種解決的方案,增量更新和原始快照。
增量更新破壞的是第一個條件,當黑色對象插入新的指向白色對象的引用關係時,就將這個新插入的引用記錄下來,等併發掃描結束之後再將這些記錄過的引用關係中的黑色對象爲根,重新掃描一遍。可以簡單的理解爲,黑色對象一旦新插入了指向白色對象的引用之後,他就變回黑色對象了。
原始快照破壞的是第二個條件,當灰色對象要刪除指向白色對象的引用關係時,就將這個要刪除的引用記錄下來,在併發掃描結束的時候,再將這些記錄過的引用關係中的灰色對象爲根,重新掃描一遍。可以簡單地理解爲,無論引用關係刪除與否,都會按照剛剛開始的那一刻的對象圖快照進行搜索。

6.經典垃圾收集器

Serial收集器
Serial收集器是歷史最悠久的收集器,他是一個單線程工作的新生代收集器,這裏的單線程指的是它在進行垃圾收集的時候必須暫停其他工作進程,直到收集結束。但是這種“Stop The
World ”的體驗是極度不友好的。於是,從Serial收集器開始,收集器的設計都遵循着減少用戶線程的停頓時間這一基本的目的進行設計,但是仍然無法根本消除這一停頓。
Serial收集器的優點就是佔用的資源是最少的,比較簡單高效,因爲它沒有太多的線程交互的開銷,垃圾收集過程簡單純粹。

ParNew收集器
ParNew收集器其實就是Serial收集器的多線程版本,同時使用多條線程進行垃圾收集,其他的行爲與Serial收集器可用的所有控制參數,收集算法,Stop The World等都完全一致。ParNew收集器的另外一個存在的條件就是它是除了Serial收集器外唯一能與CMS收集器一起工作的收集器,這(在G1收集器出現之前)是一種十分流行的垃圾回收搭配方案。

Parallel Scavenge收集器
Parallel Scavenge收集器是一款基於標記-複製算法的新生代收集器,與ParNew收集器十分類似。Parallel Scavenge收集器的特點是他關注的點與其他的收集器不同,像CMS等收集器關注的是儘可能縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。所謂吞吐量就是處理器用於運行用戶代碼時的事件與處理器總消耗時間的比值。即Parallel Scavenge收集器關注的是處理器用於垃圾回收的時間的長短控制。

Serial Old收集器與Parallel Old收集器
分別是Serial收集器與Parallel Scavenge收集器針對老年代的版本。

CMS收集器
CMS收集器(Concurrent Mark Sweep)是基於標記-清除算法實現的,運行過程主要分爲以下四個步驟:
a.初始標記(CMS initial mark)
b.併發標記(CMS concurrent mark)
c.重新標記(CMS remark)
d.併發清除(CMS concurrent sweep)
其中的初始標記和重新標記都需要Stop The World。初始標記首先遍歷與GC Roots直接相連的對象,速度很快。併發標記就是從GC Roots直接相連的對象開始遍歷整個圖的過程,這個過程耗時長但是不需要停頓其他用戶進程。重新標記則是爲了修正在併發標記過程中因爲用戶操作而產生變動的一些情況。最後併發清除的過程就是清除掉之前標記的對象,因爲不需要進行對象的移動,此過程也可以與其他的用戶線程一起運行。
可以發現,在最消耗時間的併發標記和併發清除兩個環節當中都不需要暫停其他用戶線程,這就使得CMS收集器在處理用戶停頓時間這一問題上有了一次很大的飛躍。
但是CMS收集器也有以下三個缺點:
a.對處理器資源非常敏感。其實所有的併發式收集器都對處理器資源比較敏感,在併發階段,雖然不會導致用戶線程暫停,但是卻因爲佔用了一部分的線程而導致應用程序變慢,降低總吞吐量。爲了緩解這種情況的發生,虛擬機提供了一種稱爲“增量式併發收集器”的CMS收集器變種,它使得用戶線程和收集器線程交替運行,減少收集器線程單獨佔據處理器的時間。但是這種模式因爲體驗不佳已經在JDK 9被完全廢棄。
b.CMS無法處理“浮動垃圾”。因爲CMS的併發標記是與用戶線程一起運行的,在這段時間內產生的新的垃圾並沒有辦法被標記到,只能等到下一次垃圾收集過程中進行處理,這部分垃圾稱爲“浮動垃圾”。
c.因標記-清除算法帶來的內存碎片化的問題。

Garbage First收集器
G1收集器的一個特點就是他不再侷限於在老年代收集,在新生代收集或是在整個java堆中收集這一模式,而是面向堆內任何部分組成回收集進行回收,衡量標準不再是它屬於哪個分代,而是哪塊內存中存放的垃圾數量最多,回收效益最大,這就是G1收集器的Mixed GC模式。G1收集器不再將堆機械得分爲老年代,新生代這種整塊的區域,而是分爲一個一個的Region(區域),然後會通過對各個Region回收價值的分析排序,以及用戶所期望的停頓時間對Region進行有計劃的回收。
G1收集器的運作過程也可以分爲四個步驟:
a.初始標記:僅僅只是標記一下GC Roots能直接關聯到的對象。需要暫停用戶線程。
b.併發標記:從GC Roots開始對堆中的對象進行可達性分析,遞歸掃描整個圖,找到要回收的對象。可與用戶線程併發執行。
c.最終標記:對用戶線程進行暫停,用於處理併發標記遺留下來的最後少量的SATB記錄。
d.篩選回收:負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多的Region構成回收集,然後把存活的Region複製到空的Region中,在清理掉舊Region的全部空間。這裏涉及到存活對象的移動,也需要暫停用戶線程。
從上述過程可以看出,G1只有併發標記不需要暫停用戶線程,因此G1並不只是單純的追求低延遲,而是想成爲一個在延遲可控的情況下將獲得儘可能高的吞吐量的“全功能收集器”。
相比於CMS收集器,G1收集器的最大的優點就是可以指定最大停頓時間,分Region的內存佈局,按受益確定回收集這些創造性設計帶來的紅利。但是G1收集器無論是垃圾收集產生的內存佔用還是程序運行時的額外執行負載都比CMS要高。

7.低延遲垃圾收集器

因爲之前的垃圾收集器無論是CMS還是G1,都還是存在着一定程度上的“Stop The World”的現象,而這個停頓時間的問題日益成爲限制收集器性能瓶頸,因此在後來的發展中出現了兩款幾乎整個處理過程都是併發進行的垃圾收集器。
Shenandoah收集器
Shenandoah收集器是一款只存在於OpenJDK中而不存在於OracleJDK的垃圾收集器。
Shenandoah收集器相對於G1和CMS最主要的改進之處就是他可以併發的進行垃圾收集之後的整理工作,這就大大的減少了停頓時間的產生。
其實Shenandoah收集器在絕大部分的工作原理上與G1十分相似,但是也至少存在着是三個不同。首先就是回收垃圾之後的整理過程可併發,G1的清除過程可以併發,但是整理過程還是需要暫停用戶線程;第二就是Shenandoah沒有分代的概念,所以在權衡清除價值的時候,與對象處於什麼世代無關;最後就是Shenandoah摒棄了G1中耗費大量內存和資源去維護的記憶集,改用“連接矩陣”的全局數據結構來記錄Region的引用關係。“連接矩陣”就是一個二維表,記錄對象之間的連接關係。
Shenandoah收集器的工作過程可以分爲以下九個階段:
(1)初始標記:與G1一樣,標記直接與GC Roots相連的對象,需要暫停用戶線程;
(2)併發標記:遍歷從GC Roots出發的所有對象的可達性,可與用戶線程併發執行;
(3)最終標記:與G1一樣,處理剩餘的SATB掃描,並統計出回收價值最高的Region,在統計階段也會造成用戶線程的暫停;
(4)併發清理:這個階段用於清理那些連一個存活對象都沒有找到的Region;
(5)併發回收:這個階段是Shenandoah收集器與之前的所有收集器有所區別的階段。Shenandoah需要把回收集中的存活對象先複製一份到其他未被使用的Region中(標記複製算法)。複製這一操作如果在用戶線程暫停的情況下進行就顯得十分簡單,但是如果要求與用戶線程並行,就顯得十分複雜了。Shenandoah會通過讀屏障和被稱爲“Brooks Pointers”的轉發指針來解決這一困難。
(6)初始引用更新:併發回收階段複製結束之後,還需要把堆中所有指向舊對象的引用修正到複製後的新地址,這個操作成爲引用更新。這個過程會造成一個短暫的用戶線程暫停。
(7)併發引用更新:這是真正進行引用更新的過程,這個階段與用戶線程併發進行。它與併發標記不同,不需要再遍歷整個圖,只需要按照內存物理地址的順序,線性搜索出引用類型,把舊值修改爲新值即可。
(8)最終引用更新:解決了堆中的引用更新之後,還要修正存在於GC Roots中的引用,這個階段是Shenandoah的最後一次用戶線程暫停過程,停頓時間只與GC Roots的數量有關。
(9)併發清理:經過併發回收和引用更新後,整個回收集中所有的Region已再無存活對象,因此再調用一次併發清理過程來回收這些Region空間。

在這一系列的過程中,Shenandoah的核心就是如何在複製的過程中實現與用戶線程併發。那就是利用了Brooks Pointers(轉發指針)技術。這種技術就是在原有的對象佈局前面加入了一個引用字段,在正常不處於併發移動的情況下,該引用指向對象自己;而併發複製所面臨的問題就是:移動是一瞬間的問題,但是移動之後整個內存所有指向該對象的引用都還是指向舊地址,這是很難一瞬間改變的,而在這個過程中用戶線程又會對被移動的對象進行讀寫訪問,這就造成了前後信息不一致的問題。Brooks Pointer的出現,使得在完成內存區域的複製移動之後,只需要通過修改轉發指針,將指針指向對象存在的新地址上,這樣虛擬機內存中所有通過舊引用地址訪問的代碼便仍可使用,都會被自動轉發到新對象上繼續工作。雖然大大縮短了更改地址的操作耗時,但是如果在虛擬機更改轉發指針之前有一個用戶線程訪問了對象,那麼它仍然會操作到舊對象上,造成錯誤,因此依然需要對這個階段採取同步操作的限制。

ZGC收集器
我們先用一句話來概括ZGC的主要特徵:ZGC是一款基於Region內存佈局的,不設分代的,使用讀屏障,染色指針和內存多重映射等技術來實現可併發的標記-整理算法的,以低延遲爲首要目標的一款垃圾收集器。
ZGC的Region與G1和Shenandoah大致相同,但是ZGC的Region是動態的,即動態創建和銷燬以及動態的區域容量大小。
Shenandoah收集器使用Brooks Pointers和讀屏障來解決併發整理的問題,而ZGC則使用染色指針技術(Colored Pointer)。
染色指針其實就是將之前一般存儲在對象之中的一些對象相關的信息改變爲存儲在對象指向該引用的指針中,這樣使得即使引用存在被移動的可能,虛擬機也可以通過對象指向引用的指針直接的對引用的一些信息進行訪問。
ZGC的可分爲以下工作階段:
(1)初始標記:標記直接與GC Roots相連的對象;
(2)併發標記:遍歷整個圖進行標記;
(3)最終標記:完善標記的結果;
(4)併發預備重分配:根據特定的查詢條件統計得出本次收集過程要清理哪些Region,將這些Region組成重分配集。
(5)併發重分配:這個過程要把重分配集中的存活對象複製到新的Region上,併爲重分配集中的每個Region維護一個轉發表,記錄從舊對象到新對象的轉向關係。得益於染色指針的支持,這一過程能僅僅通過指針實現與用戶線程的併發,這一行爲稱爲指針的“自愈”能力。
(6)併發重映射:修正堆中指向重分配集合中舊對象的所有引用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章