若是說 JVM 的垃圾回收算法是內存回收的抽象策略,那麼垃圾收集器就是內存回收的具體實現。
JVM 規範對於垃圾收集器應該如何實現沒有任何規定,所以不一樣的廠商、不一樣版本的虛擬機所提供的垃圾收集器差異較大,這裏只看 HotSpot 虛擬機。
就像沒有最好的算法同樣,垃圾收集器也沒有最好的,只有最合適的。咱們能作的就是根據具體的應用場景選擇最合適的垃圾收集器。
複製代碼
-Serial
-ParNew
-Parallel Scavenge
複製代碼
-Serial Old
-Parallel Old
-CMS
複製代碼
-G1
-收集器參數總結
複製代碼
Serial(串行)收集器是最基本、歷史最悠久的垃圾收集器了(新生代採用複製算法,老年代採用標誌整理算法)。看名字就能夠知道這個收集器是一個單線程的收集器。
它的 "單線程" 的意義不只僅意味着它只會使用一條垃圾收集線程去完成垃圾收集工做,更重要的是它在進行垃圾收集工做的時候必須暫停其餘全部的工做線程(Stop The World:將用戶正常工做的線程所有暫停掉),直到它收集結束。
複製代碼
上圖中:
1、新生代採用複製算法,Stop-The-World
2、老年代採用標記-整理算法,Stop-The-World
當它進行 GC 工做的時候,雖然會形成 Stop-The-World,正如每種算法都有存在的緣由,該串行收集器也有存在的緣由:由於簡單而高效(與其餘收集器的單線程相比),對於限定單個 CPU 的環境來講,沒有線程交互的開銷,專心作 GC,天然能夠得到最高額單線程效率。因此 Serial 收集器對於運行在 Client 模式下應用是一個很好的選擇(到目前爲止,它依然是虛擬機運行在 Client 模式下默認的新生代收集器)
串行收集器的缺點很明顯,虛擬機的開發者固然也是知道這個缺點的,因此一直都在縮減 Stop The World 的時間。
在後續的垃圾收集器設計中停頓時間不斷縮短(可是仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。
整理必定下前面關於 Serial 收集器的特色:
1、針對新生代的收集器;
2、採用複製算法;
3、單線程收集;
4、進行垃圾收集時,必須暫停全部工做線程,直到完成,即會 "Stop The World";
應用場景:
1、依然是 HotSpot 在 Client 模式下默認的新生代收集器;
2、也有優於其餘收集器的地方:
-簡單高效(與其餘收集器的單線程相比);
3、對於限定單個 CPU 的環境來講,Serial 收集器沒有線程交互(切換)開銷,能夠得到最高的單線程收集效率;
4、在用戶的桌面應用場景中,可用的內存通常不大(幾十 M 至一兩百 M),能夠在較短期內完成垃圾收集(幾十 MS 至一百多 MS),只要不頻繁發生,這是能夠接受的;
設置參數:
1、添加該參數來顯示使用的串行垃圾收集器
-XX: + UseSerialGC
複製代碼
ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其他行爲(控制參數、手機算法、回收策略等等)和 Serial 收集器徹底同樣。
它是許多在 Server 模式下的虛擬機的首要選擇,除了 Serial 收集器外,目前只有它能與 CMS 收集器配合工做。
CMS 收集器是一個被認爲具備劃時代意義的併發收集器,所以若是有一個垃圾收集器能和它一塊兒搭配使用讓其更加完美,那這麼收集器必然是一個不可或缺的部分了。
複製代碼
特色:
1、除了多線程外,其他的行爲、特色和 Serial 收集器同樣;
2、如 Serial 收集器可控制參數、收集算法、Stop The World、內存分配、回收策略等;
3、Serial 收集器共用了很多代碼;
應用場景:
1、在 Server 場景下,ParNew 收集器是一個很是重要的收集器,所以除 Serial 外,目前只有它能與 CMS 收集器配合工做;
2、在單個 CPU 環境中,不會比 Serial 收集器有更好的效果,所以存在線程交互開銷;
設置參數:
1、指定使用 CMS 後,會默認使用 ParNew 做爲新生代收集:
-XX: + UseConcMarkSweepGC
2、強制使用 ParNew:
-XX:+UseParNewGC
3、指定垃圾收集器的線程數量,ParNew 默認開啓的收集線程與 CPU 的數量相等:
-XX:ParallelGCThreads
複製代碼
1、CMS 是 HotSpot 在 JDK 1.5 推出的第一款真正意義上的併發(Concurrent)收集器,第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工做;
2、CMS 做爲老年代收集器,但卻沒法與 JDK 1.4 已經存在的新生代收集器 Parallel Scavenge 配合工做;
3、由於 Parallel Scavenge(以及 G1)都沒有使用傳統的 GC 收集器代碼框架,而另外獨立實現。而其他幾種收集器則共用了部分的框架代碼。
複製代碼
Parallel Scavenge 收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。
Parallel Scavenge 收集器關注點是吞吐量(如何高效率利用 CPU)。
CMS 等垃圾收集器的關注點更多的是在用戶線程的停頓時間(提升用戶體驗)。
所謂吞吐量就是 CPU 中用於運行用戶代碼的時間與 CPU 總消耗時間的比值。(吞吐量:CPU 用於用戶代碼的時間 / CPU 總消耗時間的比值,即 = 運行用戶代碼的時間 / (運行用戶代碼時間 + 垃圾收集時間)。好比,虛擬機總共運行了 100 分鐘,其中垃圾收集花掉 1 分鐘,那吞吐量就是 99%)
複製代碼
Parallel Scavenge 收集器提供了不少參數供用戶找到最合適的停頓時間或最大吞吐量,若是對於收集器運做不太瞭解的話,不進行手工優化,能夠選擇把內存管理優化交給虛擬機去完成。
特色:
1、新生代收集器;
2、採用複製算法;
3、多線程收集;
4、CMS 等收集器的關注點是儘量的縮短垃圾收集時用戶線程的停頓時間;而 Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量(ThroughPut);
應用場景:
1、高吞吐量爲目標,既減小垃圾收集的時間,讓用戶代碼得到更長的運行時間;
2、當應用程序運行在具備多個 CPU 上,對暫停時間沒有特別高的要求時,即程序主要在後臺進行計算,而不需與用戶進行太多交互;
3、例如,那些執行批量處理、訂單處理(對帳等)、工資支付、科學計算的應用程序。
設置參數:
Parallel Scavenge 收集器提供兩個參數用於精確控制吞吐量:
1、控制最大垃圾收集停頓的時間:
-XX:MaxGCPauseMillis
a.控制最大垃圾收集停頓時間,大於 0 的毫秒數
b.MaxGCPauseMillis 設置的稍小,停頓時間可能會縮短,但也可能會使得吞吐量降低;由於可能致使垃圾收集發生的更頻繁。
2、設置垃圾收集時間佔總時間的比率:
-XX:GCTimeRatio
設置垃圾收集時間佔總時間的比率,0 < n < 100 的整數;
GCTimeRatio 至關於設置吞吐量大小;
垃圾收集執行時間佔應用程序執行時間的比例計算方法是:1 / (1 + n)。例如,選項 -XX:GCTimeRatio = 19,設置了垃圾收集時間佔總時間的 5% = 1 / (1 + 19);默認值是 1% = 1 / (1 + 99),即 n = 99;
垃圾收集所花費的時間是年輕代和老年代收集的總時間;
複製代碼
另外還有一個參數:
-XX:+UseAdptiveSizePolicy
開啓這個參數後,就不用手工指定一些細節參數,好比:
1、新生代的大小(-Xmm)、Eden 與 Survivor 區的比列(-XX:SurvivorRation)、晉升老年代的對象年齡(-XX:PretenureSizeThreshold)等;
2、JVM 會根據當前系統運行的狀況收集性能監控信息,動態調整這些參數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱爲 GC 自適應的調節策略(GC Ergonomics);
3、這是一種值得推薦的方式:
a.只需設置好內存數據大小(如"-Xmx"設置最大堆);
b.而後使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"給 JVM 設置一個優化目標;
c.那些具體細節參數的調節就由 JVM 自適應完成。
這也是 Parallel Scavenge 收集器與 ParNew 收集器一個重要的區別。
複製代碼
Serial 收集器的老年代版本,它一樣是一個單線程收集器。
它主要有兩大用途:一種用途是在 JDK 1.5 及之前的版本中與 Parallel Scavenge 收集器搭配使用;另外一種用途是做爲 CMS 收集器的後備方案。
特色:
1、針對老年代;
2、採用"標記-整理-壓縮"算法(Mark-Sweep-Compact);
3、單線程收集。
Serial/Serial Old收集器運行示意圖在前面有。
應用場景:
1、主要用於 Client 模式;
2、而在 Server 模式有兩大用途:
a.在 JDK 1.5 及之前,與 Parallel Scavenge 收集器搭配使用(JDK 1.6 有 Parallel Old 收集器可搭配 Parallel Scavenge 收集器);
b.做爲 CMS 收集器的後備預案,在併發收集器發生 Concurrent Mode Failure 時使用。
複製代碼
Parallel Scavenge 收集器的老年代版本。
使用多線程和"標記-整整"算法。在注重吞吐量以及 CPU 資源的場合,均可以優先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。
在 JDK 1.6 纔有的。
特色:
1、針對老年代;
2、採用"標記-整理-壓縮"算法;
3、多線程收集。
複製代碼
Parallel Scavenge/Parallel Old 收集器運行示意圖以下:java
應用場景:
1、JDK 1.6 及之後用來代替老年代的 Serial Old 收集器;
2、特別是在 Server 模式,多 CPU 狀況下:
a.這樣在注重吞吐量以及 CPU 資源敏感的場景,就有了 Parallel Scavenge(新生代)加 Parallel Old(老年代)收集器的"給力"應用組合。
設置參數:
1、指定使用 Parallel Old 收集器:
-XX: + UseParallelOldGC
複製代碼
CMS(Concurrent Mark Sweep)收集器是一種以得到最短回收停頓時間爲目標的收集器。它很是適合在注重用戶體驗的應用上使用。
特色:
1、針對老年代;
2、基於"標記-清除"算法(不進行壓縮操做,會產生內存碎片);
3、以獲取最短回收停頓時間爲目標;
4、併發收集、低停頓;
5、須要更多的內存。
CMS 是 HotSpot 在 JDK 1.5 推出的第一款真正意義上的併發(Concurrent)收集器;第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工做。
應用場景:
1、與用戶交互較多的場景(如常見 WEB、B/S-瀏覽器/服務器模式系統的服務器上應用);
2、但願系統停頓時間最短,注重服務響應速度:
a.以給用戶帶來較好的體驗;
設置參數:
1、指定使用 CMS 收集器
-XX:+UseConcMarkSweepGC
複製代碼
從名字中的 Mark Sweep 這兩個詞能夠看出,CMS 收集器是一種"標記-清除"算法實現的,它的運做過程相比於前面幾種垃圾收集器來講更加複雜一些。整個過程分爲四個步驟:
1、初始標記:
暫停全部的其餘線程,初始標記僅僅標記 GC Roots 能直接關聯到的對象,速度很快;
2、併發標記:
a.併發標記就是進行 GC Roots Tracing 的過程
b.同時開啓 GC 和用戶線程,用一個閉包結構去紀錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前全部的可達對象。由於用戶線程可能會不斷的更新引用域,因此 GC 線程沒法保證可達性分析的實時性。因此這個算法裏會跟蹤記錄這些發生引用更新的地方;
3、從新標記:
從新標記階段就是爲了修正併發標記期間由於用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄(採用多線程並行執行來提高效率);須要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;
4、併發清除:
開啓用戶線程,同時 GC 線程開始對標記的區域作清掃,回收全部的垃圾對象。
因爲整個過程耗時最長的併發標記和併發清除過程收集器線程均可以與用戶線程一塊兒工做。
因此整體來講,CMS 的內存回收是與用戶線程一塊兒"併發"執行的。
CMS 收集器運行示意圖以下:
複製代碼
面向併發設計的程序都對 CPU 資源比較敏感(併發程序的特色)。在併發階段,它雖然不會致使用戶線程停頓,但會由於佔用了一部分線程(或者說 CPU 資源)而致使應用程序變慢,總吞吐量會下降。(在對帳系統中,不適合使用 CMS 收集器)。
CMS 的默認收集線程數量是 = (CPU 數量 + 3) / 4;當 CPU 數量越多,回收的線程佔用 CPU 就少。
也就是當 CPU 在 4 個以上時,併發回收時垃圾收集線程很多於 25% 的 CPU 資源,對用戶程序影響可能較大;不足 4 個時,影響更大,可能沒法接受。(好比 CPU = 2 時,那麼就啓動一個線程回收,佔了 50% 的 CPU 資源。)
一個回收線程會在回收期間一直佔用 CPU 資源;
針對這種狀況,曾出現了"增量式併發收集器"(Incremental Concurrent Mark Sweep/i-CMS);
相似使用搶佔式來模擬多任務機制的思想,讓收集線程和用戶線程交替運行,減小收集線程運行時間;
但效果並不理想,JDK 1.6 後就官方再也不提倡用戶使用。
複製代碼
沒法處理浮動垃圾,可能出現"Concurrent Mode Failure"失敗,在併發清除時,用戶線程新產生的垃圾,稱爲浮動垃圾;
解決辦法:
這使得併發清除時須要預留必定的內存空間,不能像其餘收集器在老年代幾乎填滿再進行收集;也能夠認爲CMS所須要的空間比其餘垃圾收集器大;可使用"-XX:CMSInitiatingOccupancyFraction",設置CMS預留老年代內存空間。
複製代碼
因爲 CMS 是基於"標記-清除"算法來回收老年代對象的,所以長時間運行後會產生大量的空間碎片問題,可能致使新生代對象晉升到老生代失敗。
因爲碎片過多,將會給大對象的分配帶來麻煩。所以會出現這樣的狀況,老年代還有不少剩餘的空間,可是找不到連續的空間來分配當前對象,這樣不得不提早觸發一次 Full GC。
解決辦法:
使用"-XX:+UseCMSCompactAtFullCollection"和"-XX:+CMSFullGCsBeforeCompaction",須要結合使用。
複製代碼
因爲合併整理是沒法併發執行的,空間碎片問題沒有了,可是有致使了連續的停頓。所以,可使用另外一個參數−XX:CMSFullGCsBeforeCompaction,表示在多少次不壓縮的 Full GC 以後,對空間碎片進行壓縮整理。
能夠減小合併整理過程的停頓時間;
默認爲 0,也就是說每次都執行 Full GC,不會進行壓縮整理;
因爲空間再也不連續,CMS 須要使用可用"空閒列表"內存分配方式,這比簡單實用"碰撞指針"分配內存消耗大。
複製代碼
上一代的垃圾收集器(串行 serial, 並行 parallel, 以及 CMS)都把堆內存劃分爲固定大小的三個部分:
-年輕代(young generation)
-年老代(old generation)
-持久代(permanent generation)
注:堆內存中均可以認爲是 Java 對象
G1 (Garbage-First)是 JDK 7-u4 才推出商用的收集器;
G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的及其。以極高機率知足 GC 停頓時間要求的同時,還具有高吞吐量性能特徵。被視爲 JDK 1.7 中 HotSpot 虛擬機的一個重要進化特徵。
G1 的使命是在將來替換 CMS,而且在 JDK 1.9 已經成爲默認的收集器。
複製代碼
G1 可以充分利用 CPU 多核環境下的硬件優點,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其餘收集器本來須要停頓 Java 線程執行的 GC 動做,G1 收集器仍然能夠經過併發的方式讓 Java 程序繼續執行。
複製代碼
雖然 G1 能夠不須要其它收集器配合就能獨立管理整個 GC 堆,可是仍是保留了分代的概念。
a.能獨立管理整個 GC 堆(新生代和老年代),而不須要與其餘收集器搭配;
b.可以採用不一樣方式處理不一樣時間的對象;
c.雖然保留分代概念,但 Java 堆的內存佈局有很大差異;
d.將整個堆劃分爲多個大小相等的獨立區域(Region);
e.新生代和老年代再也不是物理隔離,他們都是一部分 Region(不須要連續)的集合。
複製代碼
與 CMS 的"標記-清除"算法不一樣,G1 從總體來看是基於"標記-整理"算法實現的收集器;從局部上來看就是基於"複製"算法實現的。
a.從總體上看,是基於標記-整理算法
b.從局部(兩個 Region 間)看,是基於複製算法
1、這是一種相似火車算法的實現;
2、不會產生內存碎片,有利於長時間運行。
複製代碼
這是 G1 相對於 CMS 的另外一個大優點,下降停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能創建可預測的停頓時間模型。能夠明確指定 M 毫秒時間片內,垃圾收集消耗的時間不超過 N 毫秒。在低停頓的同時實現高吞吐量。
複製代碼
1、能夠有計劃地避免在 Java 堆的進行全區域的垃圾收集;
2、G1 收集器將內存分大小相等的獨立區域(Region),新生代和老年代概念保留,可是已經再也不物理隔離;
3、G1 跟蹤各個 Region 得到其收集價值大小,在後臺維護一個優先列表;
4、每次根據容許的收集時間,優先回收價值最大的 Region(名稱Garbage-First的由來);
這就保證了在有限的時間內能夠獲取儘量高的收集效率。
複製代碼
一個 Region 不多是孤立的,一個 Region 中的對象可能被其餘任意 Region 中對象引用,判斷對象存活時,是否須要掃描整個 Java 堆才能保證準確?
在其餘的分代收集器,也存在這樣的問題(而G1更突出):回收新生代也不得不一樣時掃描老年代?
這樣的話會下降 Minor GC 的效率;
解決辦法:
不管G1仍是其餘分代收集器,JVM都是使用Remembered Set來避免全局掃描;
每一個Region都有一個對應的Remembered Set;
每次Reference類型數據寫操做時,都會產生一個Write Barrier暫時中斷操做;
而後檢查將要寫入的引用指向的對象是否和該Reference類型數據在不一樣的Region(其餘收集器:檢查老年代對象是否引用了新生代對象);
若是不一樣,經過CardTable把相關引用信息記錄到引用指向對象的所在Region對應的Remembered Set中;
當進行垃圾收集時,在GC根節點的枚舉範圍加入Remembered Set;
就能夠保證不進行全局掃描,也不會有遺漏。
複製代碼
1、面向服務端應用,針對具備大內存、多處理器的機器;
2、最主要的應用是爲須要低 GC 延遲,並具備大堆的應用程序提供解決方案
如:在堆大小約 6GB 或更大時,可預測的暫停時間能夠低於 0.5 秒;
(實踐:對帳系統中將 CMS 垃圾收集器修改成 G1,下降對帳時間 20 秒以上)
具體什麼狀況下應用 G1 垃圾收集器比 CMS 好,能夠參考如下幾點(但不是絕對):
1、超過 50% 的 Java 堆被活動數據佔用;
2、對象分配頻率或年代的提高頻率變化很大;
3、GC 停頓時間過長(長於 0.5 至 1 秒);
建議:
1、若是如今採用的收集器沒有出現問題,不用急着去選擇 G1;
2、若是應用程序追求低停頓,能夠嘗試選擇 G1;
3、是否代替 CMS 只有須要實際場景測試才知道。(若是使用 G1 後發現性能尚未使用 CMS 好,那麼仍是選擇 CMS 比較好)
複製代碼
能夠經過下面的參數,來設置一些 G1 相關的配置。
1、指定使用 G1 收集器:
-XX:+UseG1GC
2、當整個 Java 堆的佔用率達到參數值時,開始併發標記階段;默認爲 45:
-XX:InitiatingHeapOccupancyPercent
3、爲G1設置暫停時間目標,默認值爲 200 毫秒:
-XX:MaxGCPauseMillis
4、設置每一個 Region 大小,範圍 1MB 到 32MB;目標是在最小 Java 堆時能夠擁有約 2048 個 Region:
-XX:G1HeapRegionSize
5、新生代最小值,默認值 5%:
-XX:G1NewSizePercent
6、新生代最大值,默認值 60%:
-XX:G1MaxNewSizePercent
7、設置 STW 期間,並行 GC 線程數:
-XX:ParallelGCThreads
8、設置併發標記階段,並行執行的線程數:
-XX:ConcGCThreads
複製代碼
不計算維護 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,而且在此過程當中壓縮和釋放內存;
能夠併發進行,下降停頓時間,並增長吞吐量。
複製代碼
G1 在標記過程當中,每一個區域的對象活性都被計算,在回收時候,就能夠根據用戶設置的停頓時間,選擇活性較低的區域收集,這樣既能保證垃圾回收,又能保證停頓時間,並且也不會下降太多的吞吐量。Remark(從新標記)階段新算法的運用,以及收集過程當中的壓縮,都彌補了 CMS 不足。
引用 Oracle 官網的一句話:"G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS)";
G1 計劃做爲併發標記-清除收集器(CMS)的長期替代品。
複製代碼