JVM理論:(二/3)垃圾收集算法、垃圾收集器
掌握三種垃圾算法,七種垃圾收集器,了解每種垃圾收集器使用的是哪種垃圾收集算法,以及關於SafePoint的知識點。
垃圾收集算法
1、標記-清除算法(Mark-Sweep)
先標記(如可達性算法)出所有需要回收的對象,標記完後再統一回收所有被標記的對象。
缺點:標記和清除過程的效率都不高,且清除後會產生大量不連續的內存碎片。
2、復制算法(Copying)
將可用內存劃分為大小相等的兩塊,每次只使用其中的一塊。當其中一塊的內存用完了,就將還存活的對象復制到另一塊內存上,然後再把已使用過的內存空間一次清理掉。
新生代回收的原理就是使用復制算法,HotSpot虛擬機將新生代分為1個Eden和2個Survivor,默認比例是Eden:Survior1:Survior2 = 8:1:1。回收時,將Eden和Survior1中還存活的對象一次性復制到Survior2,然後清理掉Eden和Survior1的空間。由於沒辦法保證每次回收後存活的對象小於10%,因此要對新生代進行分配擔保,即如果另一塊Survivor沒有空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。
優缺點:復制算法雖然不用考慮內存碎片的問題,簡單高效,但也需要浪費一部分的空間,還有需要考慮分配擔保,適用於對象存活率不高的新生代。
3、標記-整理算法(Mark-Compact)
對於對象存活率較高的老年代,不適合使用復制算法,因為不僅會進行較多的復制,且還需要額外的空間進行分配擔保。
老年代中一般使用“標記—整理”算法,先標記出需要回收的對象,接著讓所有存活對象都向一端移動,然後直接清理掉端邊界以外的所有內存。
4、分代收集算法(Generational Collection)
根據Java堆不同年代的區域采用適當的收集算法,新生代中98%的對象朝生夕死,適合用復制算法,只需要復制少量存活對象。老年代中對象存活率高,且沒有額外空間對它進行擔保,適合用“標記—清除”或“標記—整理”算法。
垃圾收集器
以下7種作用於不同分代的收集器,兩收集器間存在連線,表示可以搭配使用。
1、Serial收集器
新生代收集器,單線程,GC時會暫停其他所有的工作線程(stop the world),使用復制算法。
虛擬機在Client模式下默認的新生代收集器,在單CPU環境下,Serial收集器由於沒有線程交互的開銷,簡單高效。
開啟Serial收集器:-XX:+UseSerialGC,與Serial Old搭配使用的運行過程如下圖。
2、ParNew收集器
新生代收集器,多線程並行,GC時會暫停其他所有的工作線程(stop the world),使用復制算法,是Serial收集器的多線程版。
許多在Server模式下的虛擬機首選的新生代收集器,因為只有Serial和ParNew收集器能與CMS(老年代)配合工作。選擇CMS(-XX:+UseConcMarkSweepGC)後默認的新生代收集器是ParNew,也可用-XX:+UseParNewGC命令指定為ParNew收集器。
如果是單CPU的環境,Serial會有更好的效果,但隨著CPU數量的增加,ParNew更適合。ParNew默認開啟的GC線程數與CPU數量相同,通過-XX:ParallelGCThreads可以限制GC線程數。
並行:多條GC線程並行工作,但此時用戶線程仍處於等待狀態, 即GC線程和用戶線程不能同時工作,其中一個線程工作,就需要停止別的線程,一個CPU只執行一條線程。
並發:用戶線程與GC線程同時執行(不一定是並行,可能會交替執行),用戶程序繼續運行,垃圾收集程序運行於另一個CPU。 以一個CPU的角度,廣義上是同時運行的(交替執行)
與Serial Old搭配使用的運行過程如下圖。
3、Serial Old收集器
Serial的老年代版本,單線程,使用“標記-整理”。
一般給Client模式下的虛擬機使用,如果在Server模式下,一種用途是JDK 1.6以前與Parallel Scavenge收集器搭配使用,另一種用途就是作為CMS收集器的後備預案。
4、Parallel Scavenge收集器
新生代收集器,多線程並行、使用復制算法。
看上去與ParNew收集器相似,但不同處是Parallel Scavenge的關註點是能控制吞吐量。CMS的關註點則是盡可能縮短GC時用戶線程的停頓時間,低停頓時間適合需要與用戶頻繁交互的程序,高吞吐量則可以高效率地利用CPU時間,盡快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。吞吐量是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間),例如虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間,大於0的毫秒,收集器會盡可能保證GC時間不超過設置值,但是不是設置小了GC就快了,設置小了,GC就頻繁了,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的,新生代調小一些,收集300MB新生代肯定比收集500MB快,但這也直接導致垃圾收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,現在變成5秒收集一次、每次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了。
-XX:GCTimeRatio:直接設置吞吐量大小,大於0且小於100的整數,吞吐量的倒數,若設置為19,那允許的最大GC時間就占總時間的5%(即1/(1+19)),默認值為99,就是允許最大1%(即1/(1+99))的垃圾收集時間。
-XX:+UseAdaptiveSizePolicy:GC自適應的調節策略,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數以提供最合適的停頓時間或者最大的吞吐量。只要把基本的內存數據設置好(如-Xmx設置最大堆),然後使用MaxGCPauseMillis(更關註最大停頓時間)或GCTimeRatio(更關註吞吐量)給虛擬機設立一個優化目標即可,接著具體細節參數就由虛擬機自適應調節。
與Parallel Old搭配使用的運行過程如下圖。
5、Parallel Old收集器
Parallel Scavenge的老年代版,多線程,“標記-整理”算法。
JDK1.6前,Parallel Scavenge只能與老年代收集器Serial Old(PS MarkSweep)組合,由於Serial Old無法充分利用服務器多CPU的處理能力,會拖累整體性能。
JDK1.6後,Parallel Scavenge可與Parallel Old組合,達到名副其實的“吞吐量優先”,在註重吞吐量以及CPU資源敏感的場合可以優先考慮這個組合。
6、CMS收集器(Concurrent Mark Sweep)
基於“標記—清除”算法,低停頓,並發收集。以獲取最短回收停頓時間、低延遲為目標,適用於重視服務響應速度的應用。
主要過程為以下4步——
(1)初始標記:會經歷Stop The World,標記GC Roots能直接關聯到的對象,速度很快。
(2)並發標記:GC Roots Tracing,從GC Roots進行可達性分析,搜索所關聯的引用鏈,可與用戶線程並發進行。
(3)重新標記:也需要Stop The World,因為並發標記階段程序仍運行,可能會讓部分標記產生變動,這個階段是為了修正並發標記期間因用戶程序繼續運作而產生變動的那一部分標記,停頓時間比初始標記稍長,但遠比並發標記短。
(4)並發清除:GC線程和用戶線程可並發進行。
CMS運行過程如下圖。
因為耗時最長的並發標記和並發清除過程GC線程都可以與用戶線程一起工作,所以整體來說,CMS是與用戶線程並發執行的。
CMS的3個缺點——
(1)對CPU資源非常敏感,CMS默認啟動回收線程數是(CPU數+3)/4,如果CPU數量較多時還好,但如果CPU數量不足4個,GC時將占用近一半的CPU資源,如果本來CPU負載就比較大,還分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低了很多,GC時占用過多CPU資源會讓總吞吐量降低。
(2)無法處理浮動垃圾,由於在並發清理階段,用戶線程還會運行,因此就會產生的新垃圾,而這些新垃圾都是在標記後才產生的,這些垃圾就稱作浮動垃圾,CMS只能等到下次GC才能處理它們。
由於垃圾收集階段用戶線程還需要運行,所以就要預留足夠的內存給用戶線程使用,CMS不能像其他收集器那樣等到老年代幾乎完全被填滿再進行收集,JDK 1.6中,老年代使用了92%的空間後就會激活CMS收集器,通過-XX:CMSInitiatingOccupancyFraction可設置觸發比例,若CMS運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”,這時虛擬機會臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,停頓時間很長。所以-XX:CMSInitiatingOccupancyFraction設置太高容易導致大量“Concurrent Mode Failure”,太低頻繁GC也會影響性能。
(3)會產生大量的空間碎片,因為CMS收集器基於“標記—清除”算法,所以收集結束時會有大量空間碎片產生,對於碎片整理有以下兩個參數可以進行調整。
-XX:+UseCMSCompactAtFullCollection:默認開啟,用於在CMS收集器頂不住要進行Full GC時開啟內存碎片的合並整理過程,內存整理的過程是無法並發的,空間碎片問題沒有了,但停頓時間不得不變長。
-XX:CMSFullGCsBeforeCompaction:用於設置執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的(默認值為0,表示每次進入Full GC時都進行碎片整理)。
7、G1收集器
G1收集器和其他收集器的Java堆布局有很大差別,其他收集器的收集範圍都是整個新生代或老年代,而G1將整個Java堆劃分為多個大小相等的獨立區域Region,新生代和老年代不再是物理隔離的。空間分布如下圖所示。
G1仍然采用分代算法,新生代中依然將存活對象拷貝到老年代或者Survivor空間,老年代也分成很多區域,G1收集器通過將對象從一個區域復制到另外一個區域,完成了清理工作,這樣就不會有CMS內存碎片問題的存在了。圖中H的區域代表Humongous,這表示這些Region存儲的是巨大對象(humongous object,H-obj),即大小大於等於region一半的對象,H-obj會直接被分配到old gen,防止了反復拷貝移動,如果一個H區裝不下一個巨型對象,那麽G1會尋找連續的H分區來存儲,為了能找到連續的H區,有時候不得不啟動Full GC。
G1除了追求低停頓外還有另一個特點,能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,有這個特點是因為G1會跟蹤各個Region裏垃圾堆積的價值大小,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(G1名稱的由來)。
接下來拋出一個問題,也是其他收集器會面臨的問題:整個Java堆任意對象間都可能存在引用關系,那麽在做可達性判定確定對象是否存活時,是否要掃描整個Java堆?
G1及其他收集器都是通過Remembered Set來避免全堆掃描的:G1中每個Region都有一個與之對應的Remembered Set,當虛擬機發現程序在對Reference類型的數據進行寫操作時,會暫時中斷寫操作,檢查Reference引用的對象是否處於不同Region中(分代中則檢查是否老年代中的對象引用了新生代中的對象),如果是,會將引用信息記錄到被引用對象所屬Region的Remembered Set中,當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。
G1是一款面向服務端應用的垃圾收集器,目標是為了未來替代CMS。
除去Remembered Set的維護,G1運作過程大致分為4個步驟:
初始標記:標記GC Roots能直接關聯到的對象,短暫停頓線程。
並發標記:從GC Root開始對堆中對象進行可達性分析,找出存活對象,耗時長,可與用戶線程並發。
最終標記:修正在並發標記期間產生變動的那一部分標記記錄,虛擬機會將這段時間對象的變化記錄在線程的Remembered Set Logs中,最後把Remembered Set Logs的數據合並到Remembered Set中,需要停頓線程,可並行。
篩選回收:先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃,可與用戶線程並發,這個階段只回收一部分Region,時間是用戶可控的。
G1收集器運行圖如下。
綜合以上,G1有如下特點:
* 並行與並發:G1能充分利用多個CPU來縮短Stop-The-World停頓的時間,G1收集器仍然可以通過並發的方式讓Java程序在GC時繼續執行。
* 分代收集:與其他收集器一樣,分代概念在G1中依然得以保留,G1可以不需要其他收集器配合就能獨立管理整個GC堆。
* 空間整合:與CMS的“標記—清理”算法不同,G1從整體來看是基於“標記—整理”算法實現的收集器,從局部(兩個Region之間)上來看是基於“復制”算法實現的,但無論如何,這兩種算法都意味著G1運作期間不會產生內存空間碎片,收集後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次GC。
* 可預測的停頓:降低停頓時間是G1和CMS共同的關註點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型。
G1與Parallel Scavenge/PS Old相比,最大的好處是停頓時間更加可控,可預測。如果追求低停頓,可以嘗試G1,如果追求吞吐量,G1不會有特別的好處。
G1參數配置:
-XX:+UseG1GC:指定使用G1收集器;
-XX:InitiatingHeapOccupancyPercent:當整個Java堆的占用率達到參數值時,開始並發標記階段;默認為45;
-XX:MaxGCPauseMillis:為G1設置暫停時間目標,默認值為200毫秒;
-XX:G1HeapRegionSize:設置每個Region大小,範圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個Region;
參考鏈接:
https://blog.csdn.net/tjiyu/article/details/53983650
https://blog.csdn.net/canot/article/details/51050824
http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#important_defaults
https://www.cnblogs.com/ASPNET2008/p/6496481.html
https://www.cnblogs.com/oldtrafford/p/6883796.html
https://blog.csdn.net/baiye_xing/article/details/73743395
https://www.cnblogs.com/woshimrf/p/jvm-garbage.html
枚舉根節點、安全點、安全區
在總結完垃圾收集算法及垃圾收集器後,還有幾個小細節。
1、枚舉根節點、安全點
先回顧一下,能作為GC Roots的節點主要有常量、靜態類屬性、棧幀中的本地變量表。
在做可達性分析時,對象能被回收的條件是沒有引用來引用它,要判斷這點就需要先得到所有的GC Roots節點,如果要在可能達到數百兆的方法區中逐個檢查引用,必然會消耗很多時間,HotSpot中用了一種很直接的辦法,通過一種叫OopMap的數據結構直接得知哪些地方存著對象引用。
但不是在所有地方都記錄OopMap,只有在安全點處才會記錄OopMap信息,每個方法可能會根據safepoint被分成好幾段代碼,每一段代碼一個oopMap,作用域自然也僅限於這一段代碼,所以每個方法可能會有好幾個oopMap。安全點的選定一般是在那些需長時間執行的地方,如方法調用、循環跳轉、異常跳轉等。
值得一提的是做可達性分析時,為了保證一致性必須在到達安全點時才能停頓所有Java執行線程開始GC,即Stop The World,那麽拋出一個問題:如何在GC發生時讓所有線程(不包括JNI調用的線程)都跑到最近的安全點上再停頓下來?
有搶先式中斷和主動式中斷兩種方案,搶先式中斷是在GC發生時,先把所有線程中斷,發現有線程中斷的地方不在安全點上,就恢復線程,讓它跑到安全點上,現在的虛擬機一般不是用這種方案;主動式中斷是在安全點和創建對象需要分配內存的地方設置一個輪詢標誌,當GC中斷線程時,不直接對線程操作,讓各個線程執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起。
2、OopMap與Remembered Set回顧
結合在G1垃圾收集器裏提到的Remembered Set,簡單區分一下,OopMap記錄的是根節點的引用信息,讓枚舉根節點時快速準確。
而根節點是也有可能在老年代中的,就存在這種情況:位於老年代的某個 GC Root,它引用了新生代的某個對象,這個新生代的對象就不能清除,但如果我們只想回收新生代的對象是不是還要去查找老年代的引用呢?為了讓我們回收新生代時不用掃描整個堆,就通過Remembered Set來記錄位於不同年代對象之間的引用關系。
3、安全區域
使用安全點Safepoint機制會遇到一個問題:處於Sleep或Blocked狀態的線程,無法響應JVM的中斷請求,走到安全的地方去中斷掛起,JVM也不太可能等待線程重新被分配CPU時間,對於這種情況就需要安全區域Safe Region來解決。
安全區域是指在一段代碼片段中,引用關系不會發生變化,在這個區域中的任意地方開始GC都是安全的,
在線程執行到安全區域時,首先標識自己已經進入了安全區域,那樣在這段時間裏JVM要發起GC,就不用管標識自己在安全區域的那些線程了,在線程要離開安全區域時,會檢查系統是否完成GC,如果完成線程就可以繼續執行,否則要等待收到可以離開安全區域的信號。
參考鏈接:
https://blog.csdn.net/ifleetingtime/article/details/78934379
https://www.jianshu.com/p/d0ab167b460d
https://www.cnblogs.com/strinkbug/p/6376525.html?utm_source=itdadao&utm_medium=referral
http://dsxwjhf.iteye.com/blog/2201685
JVM理論:(二/3)垃圾收集算法、垃圾收集器