總結之JVM調優(二)——垃圾的回收

JVM常見垃圾回收算法

摘取:

對象是否“已死”算法——引用計數器算法

對象中添加一個引用計數器,如果引用計數器爲0則表示沒有其它地方在引用它。如果有一個地方引用就+1,引用失效時就-1。看似搞笑且簡單的一個算法,實際上在大部分Java虛擬機中並沒有採用這種算法,因爲它會帶來一個致命的問題——對象循環引用。對象A指向B,對象B反過來指向A,此時它們的引用計數器都不爲0,但它們倆實際上已經沒有意義因爲沒有任何地方指向它們。所以又引出了下面的算法。
  優點
①可即刻回收垃圾
在引用計數法中,每個對象始終都知道自己的被引用數(就是計數器的值)。當被引用數 的值爲 0 時,對象馬上就會把自己作爲空閒空間連接到空閒鏈表。也就是說,各個對象在變成垃圾的同時就會立刻被回收。

另一方面,在其他的 GC 算法中,即使對象變成了垃圾,程序也無法立刻判別。只有當 分塊用盡後 GC 開始執行時,才能知道哪個對象是垃圾,哪個對象不是垃圾。也就是說,直 到 GC 執行之前,都會有一部分內存空間被垃圾佔用。

②最大暫停時間短
在引用計數法中,只有當通過 mutator 更新指針時程序纔會執行垃圾回收。也就是說, 每次通過執行 mutator 生成垃圾時這部分垃圾都會被回收,因而大幅度地削減了 mutator 的最 大暫停時間。

③沒有必要沿指針查找
引用計數法和 GC 標記 - 清除算法不一樣,沒必要由根沿指針查找。減少沿指針查找的次數。

缺點
①計數器值的增減處理繁重
在引用計數法中,每當指針更新時,計數器的值都會隨之更新,因此值的增減處理必然會變得繁重。

②計數器需要佔用很多位
用於引用計數的計數器最大必須能數完堆中所有對象的引用數。

③實現煩瑣複雜
引用計數的算法本身很簡單,但事實上實現起來卻不容易。如果漏掉了某處,內存管理就無法正確 進行,就會產生 BUG。

④循環引用無法回收(最大缺點)
兩個對象互相引用,所以各對象的計數器的值都是 1。但是這些對象 組並沒有被其他任何對象引用。因此想一併回收這兩個對象都不行,只要它們的計數器值都 是 1,就無法回收。

對象是否“已死”算法——可達性分析算法

這種算法可以有效地避免對象循環引用的情況,整個對象實例以一個樹呈現,根節點是一個稱爲“GC Roots”的對象,從這個對象開始向下搜索並作標記,遍歷完這棵樹過後,未被標記的對象就會判斷“已死”,即爲可被回收的對象。

標記-清除算法

等待被回收對象的“標記”過程在上文已經提到過,如果在被標記後直接對對象進行清除,會帶來另一個新的問題——內存碎片化。如果下次有比較大的對象實例需要在堆上分配較大的內存空間時,可能會出現無法找到足夠的連續內存而不得不再次觸發垃圾回收。

複製算法(Java堆中新生代的垃圾回收算法)

此GC算法實際上解決了標記-清除算法帶來的“內存碎片化”問題。首先還是先標記處待回收內存和不用回收的內存,下一步將不用回收的內存複製到新的內存區域,這樣舊的內存區域就可以全部回收,而新的內存區域則是連續的。它的缺點就是會損失掉部分系統內存,因爲你總要騰出一部分內存用於複製。
新的對象實例被創建的時候通常在Eden空間,發生在Eden空間上的GC稱爲Minor GC,當在新生代發生一次GC後,會將Eden和其中一個Survivor空間的內存複製到另外一個Survivor中,如果反覆幾次有對象一直存活,此時內存對象將會被移至老年代。可以看到新生代中Eden佔了大部分,而兩個Survivor實際上佔了很小一部分。這是因爲大部分的對象被創建過後很快就會被GC(這裏也許運用了是二八原則)

標記-壓縮算法(或稱爲標記-整理算法,Java堆中老年代的垃圾回收算法)

對於新生代,大部分對象都不會存活,所以在新生代中使用複製算法較爲高效,而對於老年代來講,大部分對象可能會繼續存活下去,如果此時還是利用複製算法,效率則會降低。標記-壓縮算法首先還是“標記”,標記過後,將不用回收的內存對象壓縮到內存一端,此時即可直接清除邊界處的內存,這樣就能避免複製算法帶來的效率問題,同時也能避免內存碎片化的問題。老年代的垃圾回收稱爲“Major GC”。

垃圾收集器

Serial(串行)收集器

Serial(串行)收集器收集器是最基本、歷史最悠久的垃圾收集器了(新生代採用複製算法,老生代採用標誌整理算法)。大家看名字就知道這個收集器是一個單線程收集器了。

它的 “單線程” 的意義不僅僅意味着它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( “Stop The World” :將用戶正常工作的線程全部暫停掉),直到它收集結束。
在這裏插入圖片描述
新生代採用複製算法,Stop-The-World
老年代採用標記-整理算法,Stop-The-World
當它進行GC工作的時候,雖然會造成Stop-The-World,正如每種算法都有存在的原因,該串行收集器也有存在的原因:因爲簡單而高效(與其他收集器的單線程比),對於限定單個CPU的環境來說,沒有線程交互的開銷,專心做GC,自然可以獲得最高的單線程效率。所以Serial收集器對於運行在client模式下的應用是一個很好的選擇(到目前爲止,它依然是虛擬機運行在client模式下的默認新生代收集器)
串行收集器的缺點很明顯,虛擬機的開發者當然也是知道這個缺點的,所以一直都在縮減Stop The World的時間。
在後續的垃圾收集器設計中停頓時間在不斷縮短(但是仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)

特點

針對新生代的收集器;
採用複製算法;
單線程收集;
進行垃圾收集時,必須暫停所有工作線程,直到完成;
即會"Stop The World";

應用場景

依然是HotSpot在Client模式下默認的新生代收集器;
也有優於其他收集器的地方:
簡單高效(與其他收集器的單線程相比);
對於限定單個CPU的環境來說,Serial收集器沒有線程交互(切換)開銷,可以獲得最高的單線程收集效率;
在用戶的桌面應用場景中,可用內存一般不大(幾十M至一兩百M),可以在較短時間內完成垃圾收集(幾十MS至一百多MS),只要不頻繁發生,這是可以接受的

設置參數
添加該參數來顯式的使用串行垃圾收集器:
"-XX:+UseSerialGC"

ParNew(並行)收集器(Serial收集器的多線程版本-使用多條線程進行GC)

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行爲(控制參數、收集算法、回收策略等等)和Serial收集器完全一樣。
它是許多運行在Server模式下的虛擬機的首要選擇,除了Serial收集器外,目前只有它能與CMS收集器配合工作。
CMS收集器是一個被認爲具有劃時代意義的併發收集器,因此如果有一個垃圾收集器能和它一起搭配使用讓其更加完美,那這個收集器必然也是一個不可或缺的部分了。
收集器的運行過程如下圖所示:
在這裏插入圖片描述

特點

除了多線程外,其餘的行爲、特點和Serial收集器一樣;
如Serial收集器可用控制參數、收集算法、Stop The World、內存分配規則、回收策略等;
Serial收集器共用了不少代碼;
設置參數

指定使用CMS後,會默認使用ParNew作爲新生代收集:
"-XX:+UseConcMarkSweepGC"
強制指定使用ParNew:   
"-XX:+UseParNewGC"
指定垃圾收集的線程數量,ParNew默認開啓的收集線程與CPU的數量相:
"-XX:ParallelGCThreads"

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。

Parallel Scavenge收集器關注點是吞吐量(如何高效率的利用CPU)。
CMS等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。
所謂吞吐量就是CPU中用於運行用戶代碼的時間與CPU總消耗時間的比值。(吞吐量:CPU用於用戶代碼的時間/CPU總消耗時間的比值,即=運行用戶代碼的時間/(運行用戶代碼時間+垃圾收集時間)。比如,虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。)
在這裏插入圖片描述

特點

新生代收集器;
採用複製算法;
多線程收集;
CMS等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間;而Parallel Scavenge收集器的目標則是達一個可控制的吞吐量(Throughput);

應用場景

高吞吐量爲目標,即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間;
當應用程序運行在具有多個CPU上,對暫停時間沒有特別高的要求時,即程序主要在後臺進行計算,而不需要與用戶進行太多交互;
例如,那些執行批量處理、訂單處理(對賬等)、工資支付、科學計算的應用程序;

設置參數

Parallel Scavenge收集器提供兩個參數用於精確控制吞吐量:
控制最大垃圾收集停頓時間

"-XX:MaxGCPauseMillis"

控制最大垃圾收集停頓時間,大於0的毫秒數;
MaxGCPauseMillis設置得稍小,停頓時間可能會縮短,但也可能會使得吞吐量下降;因爲可能導致垃圾收集發生得更頻繁;
設置垃圾收集時間佔總時間的比率

"-XX:GCTimeRatio"

設置垃圾收集時間佔總時間的比率,0 < n < 100的整數;
GCTimeRatio相當於設置吞吐量大小;
垃圾收集執行時間佔應用程序執行時間的比例的計算方法是: 1 / (1 + n) 。
例如,選項-XX:GCTimeRatio=19,設置了垃圾收集時間佔總時間的5% = 1/(1+19);默認值是1% = 1/(1+99),即n=99;

垃圾收集所花費的時間是年輕一代和老年代收集的總時間;
如果沒有滿足吞吐量目標,則增加代的內存大小以儘量增加用戶程序運行的時間;

CMS(Concurrent Mark Sweep)收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它非常適合在注重用戶體驗的應用上使用。

特點

針對老年代
基於"標記-清除"算法(不進行壓縮操作,會產生內存碎片)
以獲取最短回收停頓時間爲目標
併發收集、低停頓
需要更多的內存
CMS是HotSpot在JDK1.5推出的第一款真正意義上的併發(Concurrent)收集器;
第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作;

應用場景

與用戶交互較多的場景;(如常見WEB、B/S-瀏覽器/服務器模式系統的服務器上的應用)
希望系統停頓時間最短,注重服務的響應速度;
以給用戶帶來較好的體驗;

CMS收集器運作過程

從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 “標記-清除”算法實現的,它的運作過程相比於前面幾種垃圾收集器來說更加複雜一些。整個過程可分爲四個步驟:

初始標記: 暫停所有的其他線程,初始標記僅僅標記GC Roots能直接關聯到的對象,速度很快;
併發標記
併發標記就是進行GC Roots Tracing的過程;
同時開啓GC和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達對象。因爲用戶線程可能會不斷的更新引用域,所以GC線程無法保證可達性分析的實時性。所以這個算法裏會跟蹤記錄這些發生引用更新的地方;
重新標記: 重新標記階段就是爲了修正併發標記期間因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄(採用多線程並行執行來提升效率);需要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;
併發清除: 開啓用戶線程,同時GC線程開始對爲標記的區域做清掃,回收所有的垃圾對象;
由於整個過程耗時最長的併發標記和併發清除過程收集器線程都可以與用戶線程一起工作。
所以總體來說,CMS的內存回收是與用戶線程一起“併發”執行的。
在這裏插入圖片描述
設置參數

指定使用CMS收集器
"-XX:+UseConcMarkSweepGC"

在這裏插入圖片描述

G1收集器(重點)

能獨立管理整個GC堆(新生代和老年代),而不需要與其他收集器搭配;
能夠採用不同方式處理不同時期的對象;
雖然保留分代概念,但Java堆的內存佈局有很大差別;
將整個堆劃分爲多個大小相等的獨立區域(Region)
新生代和老年代不再是物理隔離,它們都是一部分Region(不需要連續)的集合;
在這裏插入圖片描述
在這裏插入圖片描述
yonth GC Remembered Set的操作
在這裏插入圖片描述
不計算維護Remembered Set的操作,可以分爲4個步驟(與CMS較爲相似)。

1.初始標記(Initial Marking)

僅標記一下GC Roots能直接關聯到的對象;

且修改TAMS(Next Top at Mark Start),讓下一階段併發運行時,用戶程序能在正確可用的Region中創建新對象;

需要"Stop The World",但速度很快;

2.併發標記(Concurrent Marking)

從GC Roots開始進行可達性分析,找出存活對象,耗時長,可與用戶線程併發執行
並不能保證可以標記出所有的存活對象;(在分析過程中會產生新的存活對象)

3.最終標記(Final Marking)

修正併發標記階段因用戶線程繼續運行而導致標記發生變化的那部分對象的標記記錄。

上一階段對象的變化記錄在線程的Remembered Set Log;

這裏把Remembered Set Log合併到Remembered Set中;

需要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;

G1採用多線程並行執行來提升效率;且採用了比CMS更快的初始快照算法:Snapshot-At-The-Beginning (SATB)。

4.篩選回收(Live Data Counting and Evacuation)

首先排序各個Region的回收價值和成本;

然後根據用戶期望的GC停頓時間來制定回收計劃;

最後按計劃回收一些價值高的Region中垃圾對象;

回收時採用"複製"算法,從一個或多個Region複製存活對象到堆上的另一個空的Region,並且在此過程中壓縮和釋放內存;

可以併發進行,降低停頓時間,並增加吞吐量;
與CMS的“標記–清理”算法不同,G1從整體來看是基於“標記整理”算法實現的收集器;從局部上來看是基於“複製”算法實現的。
在這裏插入圖片描述

可預測的停頓

這是G1相對於CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型。可以明確指定M毫秒時間片內,垃圾收集消耗的時間不超過N毫秒。在低停頓的同時實現高吞吐量。

問題

爲什麼G1可以實現可預測停頓

可以有計劃地避免在Java堆的進行全區域的垃圾收集;
G1收集器將內存分大小相等的獨立區域(Region),新生代和老年代概念保留,但是已經不再物理隔離。
G1跟蹤各個Region獲得其收集價值大小,在後臺維護一個優先列表;
每次根據允許的收集時間,優先回收價值最大的Region(名稱Garbage-First的由來);
這就保證了在有限的時間內可以獲取儘可能高的收集效率;

相關參數
指定使用G1收集器:
"-XX:+UseG1GC"

當整個Java堆的佔用率達到參數值時,開始併發標記階段;默認爲45:
"-XX:InitiatingHeapOccupancyPercent"

爲G1設置暫停時間目標,默認值爲200毫秒:
"-XX:MaxGCPauseMillis"

設置每個Region大小,範圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個Region:
"-XX:G1HeapRegionSize"

新生代最小值,默認值5%:
"-XX:G1NewSizePercent"

新生代最大值,默認值60%:
"-XX:G1MaxNewSizePercent"

設置STW期間,並行GC線程數:
"-XX:ParallelGCThreads"

設置併發標記階段,並行執行的線程數:
"-XX:ConcGCThreads"

觸發mixed GC
在這裏插入圖片描述

對於G1垃圾收集器優化建議在這裏插入圖片描述

JVM的GC日誌的主要參數包括如下幾個:

-XX:+PrintGC 輸出GC日誌

-XX:+PrintGCDetails 輸出GC的詳細日誌

-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)

-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)

-XX:+PrintHeapAtGC 在進行GC的前後打印出堆的信息

-XX:+PrintGCApplicationStoppedTime // 輸出GC造成應用暫停的時間

-Xloggc:…/logs/gc.log 日誌文件的輸出路徑

GC Easy 可視化工具

GC Easy是一款在線的可視化工具,易用、功能強大,網站:http://gceasy.io/
打開導出的日誌
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

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