天天看點

JVM垃圾回收算法與垃圾收集器

垃圾回收算法

垃圾回收器都是在不同垃圾回收場景使用合理的垃圾回收算算法進行實作的。具體垃圾回收算法有以下幾種。

1、标記-清除:标記所有需要被回收的對象,然後回收。次算法效率低,并且産生記憶體碎片,由于老年代中存活的對象多,在老年代中進行使用。

2、 複制算法:将記憶體劃分為等大小的兩塊,每次使用其中的一塊,回收時,将存活的對象複制到沒有使用的一塊記憶體中,然後對使用的記憶體一次性就行清理。實作簡單,運作高效,但是存在大量記憶體的浪費。由于新生代中存活的對象少,新生代中使用這種算法将E區存活的對象複制到S區。

3、标記整理算法:讓所有存活的對象往一側移動,然後清楚另一側。老年代中使用這種算法,避免産生記憶體碎片。

java中的垃圾回收器的實作及其使用場景。

Serial收集器

屬于單線程垃圾收集器,在進行垃圾收集的時候,必須停止其他所有的使用者正常工作線程。這對很多的應用來說都是難以接受的。從jdk1.3-1.7,hotspot虛拟機開發團隊一直緻力于開發能夠消除或者減少使用者線程停頓的收集器,至今優秀的收集器不斷出現,使用者線程停頓的時間不斷縮短,但是仍不能夠進行消除。

在實際的開發中,Serial依然是虛拟機運作在Client模式下的預設的新生代收集器。在于單個CPU的環境下,Serial收集器由于沒有線程互動的開銷,可以獲得最高的垃圾收集效率。并且在使用者桌面應用場景中,由于配置設定給虛拟機管理的記憶體一般不會太大,是以垃圾收集的停頓時間完全可以控制在一百多毫秒以内,隻要垃圾手機不過于頻繁,對使用者來說是可以接受的。

Serial old收集器

serial old是serial的老年代版本,同樣是一個單線程的收集器,采用的是标記整理算法,此收集器的主要意義是給在client模式下的虛拟機使用。在server中,他的用途為:在jdk1.5之前和parallel scavenge收集器搭配使用。二是作為Cms收集器的後備方案,在并發收集器發生Concurrent Mode Failure時候使用。

Serial/Serial old示意圖:

JVM垃圾回收算法與垃圾收集器

ParNew收集器

parNew是Serial收集器的多線程版本,parnew除了采用多線程之外,其餘的和serial沒有太大的差別,他是運作在server模式下的首選新生代垃圾收集器,其中一個和性能無關的原因是目前除了parnew,隻有serial能配合cms工作。

在單核CPU下,Serial的性能是超過parnew的,但是CPU數量大于等于二的情況下,parnew的效率必定超過serial。

ParNew收集器示意圖:

JVM垃圾回收算法與垃圾收集器

parallel scavenge收集器

此收集器是一個新生代采用複制算法的并行多線程收集器。此收集器看上去和parnew收集器一樣,但是他的關注點和其他收集器不同。

CMS等其他收集器關注點是盡可能的減少垃圾收集過程中使用者線程的停頓時間,而此收集器的目标是達到一個可控制的吞吐量。吞吐量=使用者線程執行的時間/(使用者線程執行時間+垃圾收集時間)。

停頓時間短适合于使用者進行互動的程式,使用好的響應速度提升使用者的體驗。而高的吞吐量可以高效的利用CPU,盡快的完成程式的計算,适合于背景運算而不需要太多的互動任務。

parallel scavenge通過參數最大垃圾收集停頓時間參數-XX:MaxGCPauseMillis和設定吞吐量參數-XX:GCTimeRatio進行控制吞吐量。

MaxGCPauseMillis參數是一個大于0的毫秒值,收集器保證垃圾收集時間不會超過這個時間值,但是不要以為将這個值設定的盡可能小就能夠垃圾回收速度加快,因為停頓時間的縮短是通過犧牲吞吐量和新生代的空間換來的,縮短時間會導緻垃圾收集的次數更頻繁。

GCTimeRatio是一個大于0小于100的整數,就是垃圾收集時間占總是間的比率,相當于吞吐量的倒數。此數值設定的越大,表示吞吐量越大。

parallel old收集器

parallel old是parallel scavenge收集器的老年代版本,使用多線程和标記整理算法。此收集器是在jdk1.6中開始提供。在此之前,如果新生代使用parallel scavenge收集器,老年代除了使用serial old收集器别無選擇,由于serial old在伺服器端性能上的拖累使得parallel scavenge無法獲得吞吐量最大化效果。

parallel old出現後吞吐量優先才有了名副其實的組合,是以在注重吞吐量和CPU資源敏感的場合中優先考慮parallel old和parallel scavenge的搭配使用。

parallel scavenge或parallel old示意圖:

JVM垃圾回收算法與垃圾收集器

CMS收集器

以獲得最短停頓時間為目标,基于标記清除算法,是server模式下提高使用者請求響應速度,給使用者帶來良好體驗的不錯選擇。

他的運作過程分為4個步驟:

1、初始标記:需要停止使用者線程,初始标記隻是标記一下GC Root能夠直接關聯到的對象,速度很快。

2、并發标記:進行周遊GC Root關聯的對象,此過程可以和使用者線程并行。

3、重新标記:修正并發比标記期間因使用者線程繼續執行而産生的變動,此過程需要停止使用者的線程,并且此過程停頓的時間比初始标記停頓的時間稍長,但是小于并發标記所需要的時間。

4、并發清除:清除線程和使用者線程并行

由于整個過程中耗時最長的并發标記和并發清除過程都可以和使用者線程并行,是以總體上可以說垃圾收集過程和使用者的線程是并行的。

CMS屬于低停頓,但是并不是完美的,缺點如下:

1、 CMS收集器對CPU的資源非常的敏感。

在和使用者線程并行執行的階段,雖然不會停止使用者的線程,但是或占用cpu的資源,導緻使用者線程的執行速度變慢,總體的吞吐量降低。CMS預設啟動的回收線程的數量是(CPU數量 + 3)/4,占用CPU的資源=垃圾收集線程數量/CPU數量,不難計算出占CPU資源=(1/4 + 3/(4 × CPU數量))(此為估計值),是以回收線程占用CPU的資源至少為25%,随着CPU數量的增加而減少。并且當CPU的數量為2時,占用CPU的資源為50%,這時無法接受的。

為了解決這種問題,虛拟機提供了一種增量式并發收集器,這種收集方式采用的就是和單CPU執行搶占CPU資源的思想:在并發标記,清理的時候,讓GC線程和使用者線程交替執行,盡量減少GC線程占用CPU的時間,這樣使得垃圾收集的總是間變得更長。但是實踐證明,這種方式效果一般,在目前的版本中已經不提倡使用。

2、CMS收集器無法收集浮動垃圾,并且在收集過程中如果提供給使用者線程使用的空間不夠用将會導緻FULL GC。

由于CMS進行并發清理的時候,使用者線程同時也在運作,此時還會産生新的垃圾,這一部分垃圾發生在并發标記之後,CMS無法對其進行回收,需要留到下次GC時進行回收。也是由于垃圾收集階段,使用者線程需要運作,是以CMS收集器不能和其他垃圾收集器一樣等到老年代被完全填滿之後才進行收集,而是需要預留一部分空間給垃圾回收過程中的使用者線程進行使用。在jdk1.5中,CMS收集器當老年代被使用超過68%時被激活,如果老年代中垃圾增長速度不是太快,可以适當将此值調高(-XX:CMSInitiatingOccupancyFraction),進而降低垃圾回收的次數來提高性能。在jdk1.6中,啟動閥值提升至92%。

如果CMS運作期間預留的記憶體無法滿足使用者程式需要,會出現“Concurrent Mode Failure”失敗,此時虛拟機将會啟動後備方案:臨時啟用Serial Old收集器重新進行老年代垃圾的回收,這樣使得GC停頓的時間變得很長,反而減低了性能。

3、CMS是基于标記清除算法實作,在垃圾收集的過程中會産生大量空間碎片。

空間碎片的過多将會導緻老年代即時有很大的空間但是無法找到足夠大的連續的空間來配置設定對象,進而導緻FULL GC,為了解決這個問題,CMS收集器提供參數-XX:+UseCMSCompact-AtFullCollection開關參數(預設開啟),用于當CMS收集器頂不住要進行FullGc時開啟記憶體碎片的整合過程,整合過程是無法和使用者線程進行并行的,記憶體碎片問題得到了解決,但是停頓時間不得不變長。另外提供參數-XX:CMSFullGCsBeforeCompaction,用于設定執行多少次不壓縮的Full GC之後執行一次壓縮,預設值為0。

CMS示意圖:

JVM垃圾回收算法與垃圾收集器

G1收集器​

是一款面向服務端的垃圾收集器,與其他收集相比他主要有以下特點:

1、并發與并行:G1充分利用多CPU,多核環境下額硬體優勢,使用多個CPU來縮短GC停頓時間,部分其他CPU原本需要停頓使用者線程執行GC動作,G1收集器仍然可以和使用者線程并行執行。

2、 分代收集:G1收集器仍保留了分代收集的概念,并且不與其他收集器配合的情況下也能夠完成整個GC堆的收集和整理。但是G1收集器也可以采用不同的方式去處理新建立的對象,已經存活一段時間的對象,熬過多少次GC的對象以擷取更好的收集效果。

3、空間整合:G1整體采用标記整理算法實作,局部來看(兩個Region之間)基于複制算法實作,兩個算法在垃圾收集過程中不會産生記憶體碎片,不會有頻繁的Full GC出現。

4、可預測停頓:降低停頓時間是CMS和G1的共同關注點,但是G1追求低停頓同時還建立了停頓時間模型,能夠讓使用者明确指定在一個長度為M的時間片段,消耗在垃圾收集上的時間不能超過N秒。

在G1之前的所有收集器,都是針對整個新生代和老年代。而使用G1收集器時,java堆的記憶體布局與其他收集器明顯不同,他整個java堆規劃為多個大小相等的獨立區域(Region),雖保留新生代和老年代的概念,但是新生代和老年代不再是實體隔離的,他們都是一部分不連續的Region的集合。

G1能夠建立可預測的時間停頓模型的原因就是它能夠有計劃的避免在整個java堆進行垃圾收集。G1能夠跟蹤各個Region裡面的垃圾堆積的價值大小(回收所需要的時間,回收所能夠獲得空間的大小),在背景維護一個優先清單,在每次垃圾收集時,根據允許的收集時間,優先回收價值最大的Region。這種方式保證了在有限的時間内擷取盡可能高的收集效率。

G1把記憶體化整為零實踐過程中的問題:Region不可能是孤立存在的。一個對象配置設定在某個Region中,他可以被整個java堆中的任意對象引用,是以在做可達性算法進行判斷對象是否存活的時候,需要進行整個java堆掃面才能夠保證垃圾回收的準确定。這個問題在其他收集器中也有,隻是在G1中表現的更為突出。在之前的垃圾回收器中,新生代的規模一般比老年代小很多,新生代的收集也比老年代頻繁很多,那時回收新生代的時候也面臨着同樣的問題——在回收新生代的時候不得不掃面老年代。那收集器是怎樣對這種問題進行解決的呢。

在G1收集器中Region之間的對象引用問題以及其他收集器中新生代和老年代之間的對象引用問題,虛拟機都是使用Remembered Set來避免進行全堆進行掃描的。G1中的每個Region都有一個與之對應的Remembered Set,當虛拟機發現程式對Reference類型的資料進行寫操作的時候,會産生一個Write Barrier暫時中斷寫操作,檢查Reference是否處于不同的Region之中(在分代的收集器中就檢查是否存在老年代引用了新生代的對象),如果是,就通過CardTable把相關的引用資訊記錄到被引用對象所屬的Region的Remembered Set中。在進行垃圾回收的時候,在GC根節點的枚舉範圍中加入Remembered Set就可以保證不進行全堆進行掃面。

G1收集器垃圾回收步驟:

1、初始标記:僅僅是标記一下GC Root能夠直接關聯到的對象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式在并發運作時,能在正确可用的Region中建立對象,此階段需要停頓使用者線程,但是停頓時間短。

2、并發标記:此階段是通過GC Root引用判斷對象是否存活,用時較長,但是和使用者線程并發執行。

3、最終标記:标記并發标記階段使用者線程運作時候産生的對象變動,在并發标記階段,虛拟機将對象的變動記錄線上程Remembered Set Logs中,最終标記階段将Remembered Set Logs中的資料合并到Remembered Set中,這階段需要停止使用者線程,但是标記線程可以并行執行。

4、 篩選回收:先對各個Region的回收價值進行排序,根據使用者期望的停頓時間制定回收計劃,這階段可以做到和使用者線程并行執行,但是由于隻是收集部分區域,并且時間由使用者可控,是以這階段選擇停止使用者線程,讓多個垃圾收集線程并行執行,提高垃圾回收的效率。

G1示意圖:

JVM垃圾回收算法與垃圾收集器