java垃圾收集器與內存分配策略詳解

1.經典垃圾收集器

1.1 Serial收集器

這個收集器是一個單線程工作的收集器,但它的單線程的意義並不僅僅是說明他隻會使用一個處理器或一條收集線程去完成垃圾收集工作,更重要對的是強調在它進行垃圾收集時,必須暫停其他所有工作線程,直到它收集結束。

目前已經老無可用,但有著優於其他收集器的地方:簡單而高效

1.2 ParNew收集器

ParNew收集器實質上是Serial收集器的多線程並行版本。因為它是除瞭Serial收集器之外,目前唯一可以與CMS收集器配合工作的收集器,所以在JDK7之前的遺留系統中被作為首選的新生代收集器

CMS收集器是HotSpot虛擬機中第一款真正意義上支持並發的垃圾收集器,首次實現瞭讓垃圾收集線程與用戶線程同時工作。但是當選用CMS作為老年代收集器時,新生代收集器隻能選擇使用Serial收集器或者ParNew收集器

隨著垃圾收集器技術的不斷改進,G1收集器帶著CMS繼承者和代替者的光環登場。G1收集器是一個面向全堆的收集器,不需要其他新生代收集器的配合工作

1.3 Parallel Scavenge 收集器

Parallel Scavenge收集器也是一款新生代收集器,同樣是基於標記-復制算法實現的收集器,也可以並行收集的多線程收集器。它的特點是它的關註點與其他收集器不同。CMS等收集器的關註點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。

$$

吞吐量=\frac{運行用戶代碼時間}{運行用戶代碼時間+運行垃圾收集時間}

$$

提供瞭兩個參數用於精確控制吞吐量:

-XX:MaxGCPauseMillis 參數控制最大垃圾搜集停頓時間,允許的值是一個大於0的毫秒數。收集器將盡力保證內存回收花費的時間不超過用戶的設定值。但是設定過分小的值並不能起到加快回收花費的速度的作用。

-XX:GCTimeRatio 參數直接設置吞吐量大小,允許的值是一個大於0小於100的整數。也就是垃圾收集時間占總時間的比率。相當於吞吐量的倒數。

Parallel Scavenge 收集器還有一個參數:-XX:+UseAdaptiveSizePolicy 這是一個開關參數,當這個參數被激活以後,就不需要人工指定新生代的大小,Eden與Survivor區的比例等等。虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數。

1.4 Serial Old 收集器

Serial Old 是 Serial收集器的老年代版本,同樣是一個單線程收集器,使用標記-整理算法。可能有兩種用途:1. 在JDK5以及之前的版本中與Parallel Scavenge收集器搭配使用 2. 作為CMS收集器發生失敗時的後備預案。

1.5 Parallel Old 收集器

Parallel Old 是 Parallel Scavenge收集器的老年代版本,支持多線程並發收集,基於標記-整理算法實現,從JDK6版本開始提供。在註重吞吐量或者處理器資源較為稀缺的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器這個組合。

1.6 CMS 收集器

CMS收集器是一種以獲取最短回收停頓時間為目標的收集器,基於標記-清除算法實現。整個運作過程分為4步:

步驟名稱 行為
初始標記(CMS initial mark) 標記一下GC Roots能直接關聯到的對象,需要Stop The World
並發標記(CMS concurrent mark) 從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,可以與垃圾收集線程一起並發運行
重新標記(CMS remark) 修正並發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,需要Stop the World
並發清楚(CMS concurrent sweep) 清理刪除掉標記階段判斷的已經死亡的對象,可以與用戶線程同時並發完成

CMS收集器存在三個缺點:

1.CMS收集器對處理器資源非常敏感,默認啟動的回收線程數為(處理器核心數量+3)/ 4。在並發階段會因為占用瞭一部分線程而導致應用程序變慢,降低總吞吐量。

為瞭緩解這種情況虛擬機提供瞭“增量式並發收集器”(Incremental Concurrent Mark Sweep/i-CMS)作用是在並發標記、清理的時候讓收集器線程、用戶線程交替運行,盡量減少垃圾收集器線程的獨占資源的時間,這樣整個垃圾收集的過程會更長,但是對用戶程序的影響就會顯得較少一些,直觀感受是速度變慢的時間更多瞭,但速度下降幅度就沒有那麼明顯。效果一般從jdk7開始被聲明為deprecated ,從JDK9發佈後被完全廢棄

2.由於CMS收集器無法處理“浮動垃圾”(Floating Garbage),有可能出現”Concurrent Mode Failure” 失敗進而導致另一完全”Stop The World”的Full GC的產生。

可以適當調高參數-XX:CMSInitiatingOccu-pancyFraction的值來提高CMS的觸發百分比,降低內存回收頻率,獲得更好的性能。如果設置的太高將會很容易導致大量的並發失敗產生,性能反而降低

3.由於基於標記-清除算法,可能在收集結束時會有大量的空間碎片產生

通過調節:-XX:+UseCMSCompactAtFullCollection開關參數,默認是開啟的,從jdk9開始廢棄

*** -XX:CMSFullGCsBeforeCompaction 默認值是0,表示每次進入Full GC時都進行碎片整理***

1.7 Garbage First 收集器

Garbage First 收集器,簡稱 G1收集器,開創瞭收集器面向局部收集的設計思路和基於Region的內存佈局形式。是一款主要面向服務端應用的垃圾收集器。可以面向堆內存的任何部分來組成回收集,衡量的標準不再是它屬於哪個分代,而是哪塊內存中存放的垃圾數量最多,回收收益最大。G1開創的基於Region的堆內存佈局是它能夠實現這個目標的關鍵,G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間或者老年代空間。

Region中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認為隻要大小超過瞭Region容量一半的對象就可以判定為大對象。每個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值范圍為1MB~32MB。

G1收集器之所以可以建立可預測的停頓時間模型,是因為它將Region作為單次回收的最小單元,即每次收集到的內存空間都是Region大小的整數倍,這樣可以有計劃地避免在整個JAVA堆中進行全區域的垃圾收集。更具體的思路是讓G1收集器區跟蹤各個Region裡面的垃圾堆積的價值大小,價值即回收所獲得的空間大小以及回收所需要的時間的經驗值,然後在後臺 維護一個優先級列表,每次根據用戶設定允許的收集停頓時間(-XX:MaxGCPauseMillis)優先處理回收價值收益最大的那些Region。

G1的記憶集在存儲結構的本質上是一種哈希表,Key是別的Region的起始地址,Value是一個集合,裡面存儲的元素是卡表的索引號。G1收集器通過原始快照(SATB)算法實現瞭保證其不能打破原本的對象圖結構的目的。

G1收集器運作過程大致分為四個步驟:

步驟 行為
初始標記(Initial Marking) 標記一下GC Roots能直接關聯到的對象,並且修改TAMS指針的值。這個階段需要停頓線程,而且是借用進行Minor GC的時候同步完成的
並發標記(Concurrent Marking) 從GC Root開始對堆種對象進行可達性分析,遞歸掃描整個堆裡的對象圖,找出要回收的對象,可以與用戶程序並發執行。對象圖掃描完成以後,還需要重新處理SATB記錄下的在並發時有引用變動的對象
最終標記(Final Marking) 對用戶線程做另一個短暫的暫停,用於處理並發階段結束後仍遺留下來的最後那少量的SATB記錄
篩選回收(Live Data Counting and Evacuation) 負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然後把決定回收的那一部分Region的存活對象復制到空的Region中,再清理整個舊Region的全部空間,必須暫停用戶線程

2低延遲垃圾收集器

2.1 Shenandoah收集器

Shenandoah收集器是一款隻有OpenJDK才會包含的。與G1收集器相比,它們兩者有著相似的堆內存佈局,在初始標記、並發標記等許多階段的處理思路上都高度一致。但是在管理內存堆方面,與G1收集器至少有三個方面的明顯的不同之處:

1.支持並發的整理算法:G1的回收階段是可以多線程並行的,但不能與用戶線程並發。Shenandoah後面會講到。

2.Shenandoah收集器默認不使用分代收集。

3.Shenandoah摒棄瞭在G1中耗費大量內存和計算資源去維護的記憶集,改名為“連接矩陣”(Connection Matrix)的全局數據結構來記錄跨Region的引用關系。降低瞭處理跨代指針的記憶集維護消耗,也降低瞭偽共享問題發生的概率

Shenandoah收集器大致工作流程可以分為9個階段:

步驟名稱 動作
初始標記(Initial Marking) 標記與GC Roots直接關聯的對象,這個階段是Stop The World的,停頓時長與堆大小無關,與GC Roots的數量相關。
並發標記(Concurrent Marking) 遍歷對象圖,標記出全部可達的對象,這個階段與用戶線程一起並發的,時間長短取決於堆中存活對象的數量以及對象圖的結構復雜程度。
最終標記(Final Marking) 處理剩餘的SATB掃描,在這個階段統計出回收價值最高的Region,將這些Region構成一組回收集,最終標記階段也會有一小段短暫的停頓。
並發清理(Concurrent Cleanup) 清理那些整個區域內連一個存活對象都沒有找到的Region(這類Region被稱為Immediate Garbage Region)。
並發回收(Concurrent Evacuation) 核心差異!Shenandoah要把回收集裡面的存活對象先復制一份到其他未被使用的Region之中。Shenandoah會通過讀屏障和被成稱為”Brooks Points”的轉發指針來解決在復制對象時遇到的困難。時間長短取決於回收集的大小。
初始引用更新(Initial Update Reference) 把堆中所有指向舊對象的引用修正到復制後的新地址,這個操作稱為引用更新。在此階段,隻是建立瞭一個線程集合點,確保所有的並發回收階段中進行的收集器線程都已完成分配給它們的對象移動任務而已。時間會很短,有一個十分短暫的停頓。
並發引用更新(Comcurrent Update Reference) 真正開始進行引用更新操作,與用戶線程一起並發的,時間長短取決於內存中涉及的引用數量的多少。隻需要按照內存物理地址的順序,線性搜索出引用類型,把舊值改為新值即可。
最終引用更新(Final Update Reference) 解決瞭堆中的引用更新後,還要修正存在於GC Roots中的引用。這個階段是最後一次停頓,時間與GC Roots的數量有關。
並發清理(Concurrent Cleanup) 此時整個回收集中所有的Region已再無存活對象,都變成瞭Immediate Garbage Regions瞭,最後調用一次並發清理過程來回收這些Region的內存空間,供以後新對象分配使用。

Brooks Points:Brooks是一個人的名字,它提出使用瞭轉發指針(Forwarding Pointer)的技術來實現對象移動與用戶程序並發的一種解決方案。不需要用到內存保護陷阱,而是在原有對象佈局結構的最前面統一增加一個新的引用字段,在正常不處於並發移動的情況下,該引用指向對象自己。實際上Shenandoah收集器是通過比較並交換(Compare And Swap, CAS)操作來保證並發時對象的訪問正確性的。

JDK13中Shenandoah的內存屏障模型改進為基於引用訪問屏障(Load Reference Barrier)的實現,所謂“引用訪問屏障”是指內存屏障隻攔截對象中數據類型為引用類型的讀寫操作,而不去管原生數據類型等其他非引用字段的讀寫。這能省去大量對原生類型、對象比較、對象加鎖等場景中設置內存屏障所帶來的消耗。

2.2 ZGC收集器

ZGC收集器是一款基於Region內存佈局的,暫時不設分代的,使用瞭讀屏障、染色指針和內存多重映射等技術來實現可並發的標記-整理算法的,以低延遲為首要目標的一款垃圾收集器。

ZGC的Region具有動態性-動態創建和銷毀,以及動態的區域容量大小。

染色指針(Colored Pointer):一種直接將少量額外的信息存儲在指針上的技術。盡管在linux下64位指針的高18位不能用來尋址,但是剩餘的46位所能支持的64TB內存仍然能夠充分滿足需要。鑒於此,將其高4位提取出來存儲四個標記信息。通過這些標志位,虛擬機可以直接從指針中看到其引用對象的三色標記狀態、是否進入瞭重分配集、是否隻能通過finalize( )方法才能被訪問到。也使得ZGC能夠管理的內存不可以超過4TB。使用染色指針的三大優勢:

1.可以使得一旦某個Region的存活對象被移走之後,這個Region立即就能被釋放和重用掉,不必等待整個堆中所有指向該Region的引用都被修正後才能清理。

2.可以大幅度減少在垃圾收集過程中的內存屏障的使用數量。到目前為止,ZGC都未使用寫屏障,隻使用瞭讀屏障。

3.可以作為一種可擴展的存儲結構用來記錄更多與對象標記、重定位過程相關的數據,以便日後進一步提高性能。

Linux/x86-64平臺上的ZGC使用瞭多重映射將多個不同的虛擬內存地址映射到同一個物理內存地址上,意味著ZGC在虛擬內存中看到的地址空間要比實際的堆內存容量來得更大。把染色指針中的標志位看作是地址的分段符,隻要將這些不同的地址段都映射到同一個物理內存空間,經過多重映射轉換後,就可以使用染色指針正常進行尋址瞭。

ZGC的運作過程(省略部分與之前介紹的G1和Shenandoah相同的小階段部分):

步驟 動作
並發標記(Concurrent Mark) 遍歷對象圖做可達性分析的階段,前後也要經過初始標記、最終標記的短暫停頓。ZGC的標記是在指針上而不是在對象上進行的,標記階段會更新染色指針中的Marked 0、Marked 1 標志位。
並發預備重分配(Concurrent Prepare for Relocate) 根據特定的查詢條件統計得出本次收集過程要清理那些Region,將這些Region組成重分配集。與G1收集器的回收集還是有區別的,ZGC的重分配集隻是決定瞭裡面的存活對象會被重新分配復制到其他的Region中,裡面的Region會被釋放,而並不能說回收行為就隻是針對這個集合裡面的Region進行,因為標記過程是針對全堆的。
並發重分配(Concurrent Reolcate) 核心階段!把重分配集中的存活對象復制到新的Region上,並未重分配集中的每個Region維護一個轉發表,記錄從舊對象到新對象的轉向關系。
並發重映射(Concurrent Remap) 修正整個堆中指向重分配集中舊對象的所有引用。

總結

本篇文章就到這裡瞭,希望能給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: