Serial收集器
介紹
? Serial收集器是最基礎(chǔ)、歷史最悠久的收集器,曾經(jīng)(在JDK 1.3.1之前)是HotSpot虛擬機新生代收集器的唯一選擇。大家只看名字就能夠猜到,這個收集器是一個單線程工作的收集器,但它的“單線程”的意義并不僅僅是說明它只會使用一個處理器或一條收集線程去完成垃圾收集工作,更重要的是強調(diào)在它進(jìn)行垃圾收集時,必須暫停其他所有工作線程,直到它收集結(jié)束。
? 事實上,迄今為止,它依然是HotSpot虛擬機運行在客戶端模式下的默認(rèn)新生代收集器,有著優(yōu)于其他收集器的地方,那就是簡單而高效(與其他收集器的單線程相比),對于內(nèi)存資源受限的環(huán)境,它是所有收集器里額外內(nèi)存消耗(Memory Footprint)最小的;對于單核處理器或處理器核心數(shù)較少的環(huán)境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。在用戶桌面的應(yīng)用場景以及近年來流行的部分微服務(wù)應(yīng)用中,分配給虛擬機管理的內(nèi)存一般來說并不會特別大,收集幾十兆甚至一兩百兆的新生代(僅僅是指新生代使用的內(nèi)存,桌面應(yīng)用甚少超過這個容量),垃圾收集的停頓時間完全可以控制在十幾、幾十毫秒,最多一百多毫秒以內(nèi),只要不是頻繁發(fā)生收集,這點停頓時間對許多用戶來說是完全可以接受的。所以,Serial收集器對于運行在客戶端模式下的虛擬機來說是一個很好的選擇。
記憶
serial收集器
- 介紹
- 一個單線程的收集器,在進(jìn)行垃圾收集時候,必須暫停其他所有的工作線程直到它收集結(jié)束。
- 在新生代使用采取復(fù)制算法。
- 優(yōu)點:簡單而高效
- <font color=green>對于內(nèi)存資源受限的環(huán)境</font>,它是所有收集器里<font color=apple green>額外內(nèi)存消耗(Memory Footprint)最小的</font>。
- <font color=green>對于單核處理器或處理器核心數(shù)較少的環(huán)境</font>來說,Serial收集器由于<font color=red>沒有線程交互的開銷</font>,<font color=apple green>專心做垃圾收集自然可以獲得最高的單線程收集效率</font>。
- 例子
- 在用戶桌面的應(yīng)用場景以及近年來流行的部分微服務(wù)應(yīng)用中,分配給虛擬機管理的內(nèi)存一般來說并不會特別大,收集幾十兆甚至一兩百兆的新生代(僅僅是指新生代使用的內(nèi)存,桌面應(yīng)用甚少超過這個容量),垃圾收集的停頓時間完全可以控制在十幾、幾十毫秒,最多一百多毫秒以內(nèi),只要不是頻繁發(fā)生收集,這點停頓時間對許多用戶來說是完全可以接受的。
- 例子
- 缺點
- STW,這項工作是由虛擬機在后臺自動發(fā)起和自動完成的,在用戶不可知、不可控的情況下把用戶的正常工作的線程全部停掉,這對很多應(yīng)用來說都是不能接受的。
- 補充
- 從JDK 1.3開始,一直到現(xiàn)在最新的JDK 13,HotSpot虛擬機開發(fā)團隊為消除或者降低用戶線程因垃圾收集而導(dǎo)致停頓的努力一直持續(xù)進(jìn)行著,從Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)和Garbage First(G1)收集器,最終至現(xiàn)在垃圾收集器的最前沿成果Shenandoah和ZGC等,我們看到了一個個越來越構(gòu)思精巧,越來越優(yōu)秀,也越來越復(fù)雜的垃圾收集器不斷涌現(xiàn),用戶線程的停頓時間在持續(xù)縮短,但是仍然沒有辦法徹底消除(這里不去討論RTSJ中的收集器),探索更優(yōu)秀垃圾收集器的工作仍在繼續(xù)。
- 補充
- STW,這項工作是由虛擬機在后臺自動發(fā)起和自動完成的,在用戶不可知、不可控的情況下把用戶的正常工作的線程全部停掉,這對很多應(yīng)用來說都是不能接受的。
- 對應(yīng)jvm參數(shù)
- -XX:+UseSerialGC
- 開啟后的收集器的算法組合
- <font color=red>Serial(Young區(qū)用) + Serial Old(Old區(qū)用)的收集器組合</font>,新生代、老年代都會使用串行回收收集器,<font color=red>新生代使用復(fù)制算法,老年代使用標(biāo)記-整理算法</font>。
Serial/Serial Old收集器的運行過程
例子
public class GCDemo {
public static void main(String[] args) throws InterruptedException {
Random rand = new Random(System.nanoTime());
try {
String str = "Hello, World";
while(true) {
str += str + rand.nextInt(Integer.MAX_VALUE) + rand.nextInt(Integer.MAX_VALUE);
}
}catch (Throwable e) {
e.printStackTrace();
}
}
}
jvm參數(shù)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
輸出
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation
-XX:+UseSerialGC /注意這里 DefNew Default New Generation/
[GC (Allocation Failure) [DefNew: 2346K->320K(3072K), 0.0012956 secs] 2346K->1030K(9920K), 0.0013536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2888K->0K(3072K), 0.0013692 secs] 3598K->2539K(9920K), 0.0014059 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2065K->0K(3072K), 0.0011613 secs] 4604K->4550K(9920K), 0.0011946 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2056K->0K(3072K), 0.0010394 secs] 6606K->6562K(9920K), 0.0010808 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2011K->2011K(3072K), 0.0000124 secs][Tenured /注意這里old(老年代)是Tenured/: 6562K->2537K(6848K), 0.0021691 secs] 8574K->2537K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0024399 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2059K->2059K(3072K), 0.0000291 secs][Tenured: 6561K->6561K(6848K), 0.0012330 secs] 8620K->6561K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0012888 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 6561K->6547K(6848K), 0.0017784 secs] 6561K->6547K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0018111 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOfRange(Arrays.java:3664)
at java.lang.String.<init>(String.java:207)
at java.lang.StringBuilder.toString(StringBuilder.java:407)
at com.lun.jvm.GCDemo.main(GCDemo.java:23)
Heap
def new generation total 3072K, used 105K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
eden space 2752K, 3% used [0x00000000ff600000, 0x00000000ff61a7c8, 0x00000000ff8b0000)
from space 320K, 0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
to space 320K, 0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
tenured generation total 6848K, used 6547K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
the space 6848K, 95% used [0x00000000ff950000, 0x00000000fffb4c30, 0x00000000fffb4e00, 0x0000000100000000)
Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
日志解讀:
1.使用SerialGC時,年輕代是表示為:DefNew(DefNew Default New Generation),老年代表示為:Tenured;
2.Allocation Failure:
表明本次引起GC的原因是因為在年輕代中沒有足夠的空間能夠存儲新的數(shù)據(jù)了。
3.日志對照表
ParNew收集器
介紹
? ParNew收集器實質(zhì)上是Serial收集器的多線程并行版本,除了同時使用多條線程進(jìn)行垃圾收集之外,其余的行為包括Serial收集器可用的所有控制參數(shù)(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規(guī)則、回收策略等都與Serial收集器完全一致,在實現(xiàn)上這兩種收集器也共用了相當(dāng)多的代碼。
? ParNew收集器除了支持多線程并行收集之外,其他與Serial收集器相比并沒有太多創(chuàng)新之處,但它卻是不少運行在服務(wù)端模式下的HotSpot虛擬機,尤其是JDK 7之前的遺留系統(tǒng)中首選的新生代收集器,其中有一個與功能、性能無關(guān)但其實很重要的原因是:除了Serial收集器外,目前只有它能與CMS收集器配合工作。
在JDK 5發(fā)布時,HotSpot推出了一款在強交互應(yīng)用中幾乎可稱為具有劃時代意義的垃圾收集器——CMS收集器。這款收集器是HotSpot虛擬機中第一款真正意義上支持并發(fā)的垃圾收集器,它首次實現(xiàn)了讓垃圾收集線程與用戶線程(基本上)同時工作。
記憶
ParNew
-
介紹
- Serial收集器的多線程并行版本,使用多線程進(jìn)行垃圾回收,在垃圾收集時,會Stop-The-World暫停其他所有的工作線程直到它收集結(jié)束。
- 在新生代使用復(fù)制算法。
-
特點:
-
除了Serial收集器外,目前只有它能與CMS收集器配合工作
這是JDK 7之前運行在服務(wù)端模式下的HotSpot虛擬機首選ParNew作為垃圾收集器的原因。
CMS作為老年代的收集器,卻無法與JDK 1.4.0中已經(jīng)存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個。
ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC選項)的默認(rèn)新生代收集器,也可以使用-XX:+/-UseParNewGC選項來強制指定或者禁用它。
-
ParNew可以說是HotSpot虛擬機中第一款退出歷史舞臺的垃圾收集器
- 可以說直到CMS的出現(xiàn)才鞏固了ParNew的地位,但成也蕭何敗也蕭何,隨著垃圾收集器技術(shù)的不斷改進(jìn),更先進(jìn)的G1收集器帶著CMS繼承者和替代者的光環(huán)登場。G1是一個面向全堆的收集器,不再需要其他新生代收集器的配合工作。
- 所以自JDK 9開始,ParNew加CMS收集器的組合就不再是官方推薦的服務(wù)端模式下的收集器解決方案了。官方希望它能完全被G1所取代,甚至還取消了ParNew加Serial Old以及Serial加CMS這兩組收集器組合的支持(其實原本也很少人這樣使用),并直接取消了-XX:+UseParNewGC參數(shù),這意味著ParNew和CMS從此只能互相搭配使用,再也沒有其他收集器能夠和它們配合了。
- 也可以這么理解:自JDK1.9之后,ParNew合并入CMS,成為它專門處理新生代的組成部分。
-
-
性能
- 單核情況下不會比Serial收集器有更好的效果
- ParNew收集器在單核心處理器的環(huán)境中絕對不會有比Serial收集器更好的效果,甚至由于存在線程交互的開銷,該收集器在通過超線程(Hyper-Threading)技術(shù)實現(xiàn)的偽雙核處理器環(huán)境中都不能百分之百保證超越Serial收集器。
- ParNew更適合在多核情況下使用
- 多核情況下,ParNew對于垃圾收集時系統(tǒng)資源的高效利用還是很有好處的。
- 它默認(rèn)開啟的收集線程數(shù)與處理器核心數(shù)量相同
- 在處理器核心非常多(譬如32個,現(xiàn)在CPU都是多核加超線程設(shè)計,服務(wù)器達(dá)到或超過32個邏輯核心的情況非常普遍)的環(huán)境中,可以使用-XX:ParallelGCThreads參數(shù)來限制垃圾收集的線程數(shù)。
- 單核情況下不會比Serial收集器有更好的效果
-
開啟參數(shù)
- -XX:+UseParNewGC
- 開啟上述參數(shù)后,會使用:ParNew(Young區(qū))+ Serial Old的收集器組合,新生代使用復(fù)制算法,老年代采用標(biāo)記-整理算法
- 配合-XX:ParallelGCThreads參數(shù),限制線程數(shù)量,默認(rèn)開啟和CPU數(shù)目相同的線程數(shù)
- -XX:+UseParNewGC
ParNew運行過程
例子
public class GCDemo {
public static void main(String[] args) throws InterruptedException {
Random rand = new Random(System.nanoTime());
try {
String str = "Hello, World";
while(true) {
str += str + rand.nextInt(Integer.MAX_VALUE) + rand.nextInt(Integer.MAX_VALUE);
}
}catch (Throwable e) {
e.printStackTrace();
}
}
}
jvm參數(shù)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC
輸出
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC
[GC (Allocation Failure) [ParNew: 2702K->320K(3072K), 0.0007029 secs] 2702K->1272K(9920K), 0.0007396 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 2292K->37K(3072K), 0.0010829 secs] 3244K->2774K(9920K), 0.0011000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 2005K->9K(3072K), 0.0008401 secs] 4742K->5624K(9920K), 0.0008605 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1974K->1974K(3072K), 0.0000136 secs][Tenured: 5615K->3404K(6848K), 0.0021646 secs] 7589K->3404K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0022520 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1918K->2K(3072K), 0.0008094 secs] 5322K->5324K(9920K), 0.0008273 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [ParNew: 1970K->1970K(3072K), 0.0000282 secs][Tenured: 5322K->4363K(6848K), 0.0018652 secs] 7292K->4363K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0019205 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 4363K->4348K(6848K), 0.0023131 secs] 4363K->4348K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0023358 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
par new generation total 3072K, used 106K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
eden space 2752K, 3% used [0x00000000ff600000, 0x00000000ff61a938, 0x00000000ff8b0000)
from space 320K, 0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
to space 320K, 0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
tenured generation total 6848K, used 4348K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
the space 6848K, 63% used [0x00000000ff950000, 0x00000000ffd8f3a0, 0x00000000ffd8f400, 0x0000000100000000)
Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release
日志解讀:
1.使用ParNew時,年輕代是表示為ParNew(Parallel New Generation)老年代是表示為Tenured。
Parallel Scavenge 收集器
記憶
Parallel Scavenge
- 介紹
- Parallel Scavenge收集器也是一款新生代收集器,它同樣是基于標(biāo)記-復(fù)制算法實現(xiàn)的收集器,也是能夠并行收集的多線程收集器,Parallel Scavenge的諸多特性從表面上看和ParNew非常相似。
- 在新生代使用復(fù)制算法。
- 吞吐量有限收集器:該收集器的更注重的是達(dá)到一個<font color=red>可控的吞吐量</font>。
-
吞吐量:處理器用于運行用戶代碼的時間與處理器總消耗時間的比值。
image.png - 解釋
- 如果虛擬機完成某個任務(wù),用戶代碼加上垃圾收集總共耗費了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
- 停頓時間越短就越適合<font color=apple green>需要與用戶交互</font>或需要<font color=apple green>保證服務(wù)響應(yīng)質(zhì)量</font>的程序,良好的響應(yīng)速度能提升用戶體驗;
- 而高吞吐量則可以最高效率地利用處理器資源,盡快完成程序的運算任務(wù),主要適合在后臺運算而不需要太多交互的分析任務(wù)。
-
- jvm參數(shù)
- 激活參數(shù)
- -XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)
- 配合使用
- -XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間
- 允許的值是一個大于0的毫秒數(shù),收集器將盡力保證內(nèi)存回收花費的時間不超過用戶設(shè)定值。
- 不過大家不要異想天開地認(rèn)為如果把這個參數(shù)的值設(shè)置得更小一點就能使得系統(tǒng)的垃圾收集速度變得更快,<font color=apple green>垃圾收集停頓時間縮短</font>是以<font color=red>犧牲</font><font color=green>吞吐量</font>和<font color=green>新生代空間</font>為代價換取的。
- 系統(tǒng)把新生代調(diào)得小一些,收集300MB新生代肯定比收集500MB快,但這也直接導(dǎo)致垃圾收集發(fā)生得更頻繁,原來10秒收集一次、每次停頓100毫秒,現(xiàn)在變成5秒收集一次、每次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了(<font color=red>吞吐量越低,用戶代碼執(zhí)行時間越短</font>)。
- -XX:GCTimeRatio:直接設(shè)置吞吐量大小
- 參數(shù)的值則應(yīng)當(dāng)是一個大于0小于100的整數(shù),也就是<font color=red>垃圾收集時間占總時間的比率</font>,相當(dāng)于<font color=red>吞吐量的倒數(shù)</font>。
- 譬如把此參數(shù)設(shè)置為19,那允許的最大垃圾收集時間就占總時間的5%(即1/(1+19)),默認(rèn)值為99,即允許最大1%(即1/(1+99))的垃圾收集時間。
- -XX:+UseAdaptiveSizePolicy
- 自適應(yīng)的調(diào)節(jié)策略
- 這是一個開關(guān)參數(shù),當(dāng)這個參數(shù)被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden與Survivor區(qū)的比例(-XX:SurvivorRatio)、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細(xì)節(jié)參數(shù)了,虛擬機會根據(jù)當(dāng)前系統(tǒng)的運行情況收集性能監(jiān)控信息,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量。
- 如果操作者對于收集器運作不太了解,手工優(yōu)化存在困難的話,使用Parallel Scavenge收集器配合自適應(yīng)調(diào)節(jié)策略,把內(nèi)存管理的調(diào)優(yōu)任務(wù)交給虛擬機去完成也許是一個很不錯的選擇。
- 只需要把基本的內(nèi)存數(shù)據(jù)設(shè)置好(如-Xmx設(shè)置最大堆),然后使用-XX:MaxGCPauseMillis參數(shù)(更關(guān)注最大停頓時間)或-XX:GCTimeRatio(更關(guān)注吞吐量)參數(shù)給虛擬機設(shè)立一個優(yōu)化目標(biāo),那具體細(xì)節(jié)參數(shù)的調(diào)節(jié)工作就由虛擬機完成了。
- 自適應(yīng)調(diào)節(jié)策略也是Parallel Scavenge收集器區(qū)別于ParNew收集器的一個重要特性。
- 自適應(yīng)的調(diào)節(jié)策略
- -XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間
- 激活參數(shù)
例子
public class GCDemo {
public static void main(String[] args) throws InterruptedException {
Random rand = new Random(System.nanoTime());
try {
String str = "Hello, World";
while(true) {
str += str + rand.nextInt(Integer.MAX_VALUE) + rand.nextInt(Integer.MAX_VALUE);
}
}catch (Throwable e) {
e.printStackTrace();
}
}
}
jvm參數(shù)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
日志
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
[GC (Allocation Failure) [PSYoungGen: 2009K->503K(2560K)] 2009K->803K(9728K), 0.7943182 secs] [Times: user=0.00 sys=0.00, real=0.79 secs]
[GC (Allocation Failure) [PSYoungGen: 2272K->432K(2560K)] 2572K->2214K(9728K), 0.0020218 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2448K->352K(2560K)] 4230K->3122K(9728K), 0.0017173 secs] [Times: user=0.11 sys=0.02, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1380K->0K(2560K)] [ParOldGen: 6722K->2502K(7168K)] 8102K->2502K(9728K), [Metaspace: 2657K->2657K(1056768K)], 0.0039763 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 2016K->0K(2560K)] [ParOldGen: 6454K->6454K(7168K)] 8471K->6454K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0049598 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 6454K->6454K(9728K), 0.0008614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] /注意這里:PSYoungGen/[ParOldGen: 6454K->6440K(7168K)]/注意這里 ParOldGen/ 6454K->6440K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0055542 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOfRange(Arrays.java:3664)
at java.lang.String.<init>(String.java:207)
at java.lang.StringBuilder.toString(StringBuilder.java:407)
at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
PSYoungGen total 2560K, used 82K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd14810,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 6440K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 89% used [0x00000000ff600000,0x00000000ffc4a1c8,0x00000000ffd00000)
Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
日志解讀:
1.使用Parallel Scavenge時,年輕代是表示為PSYoungGen,老年代是表示為ParOldGen。
Serial Old 收集器
記憶
- 介紹
- Serial Old是Serial收集器的<font color=red>老年代版本</font>,它同樣是一個<font color=red>單線程收集器</font>,使用<font color=red>標(biāo)記-整理算法</font>。
- 使用場景
- 這個收集器的主要意義也是供客戶端模式下的HotSpot虛擬機使用。
- 如果在服務(wù)端模式,,它也可能有兩種用途:
- 一種是在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用。
- 需要說明一下,Parallel Scavenge收集器架構(gòu)中本身有PS MarkSweep收集器來進(jìn)行老年代收集,并非直接調(diào)用Serial Old收集器,但是這個PS MarkSweep收集器與Serial Old的實現(xiàn)幾乎是一樣的,所以在官方的許多資料中都是直接以Serial Old代替PS MarkSweep進(jìn)行講解,這里筆者也采用這種方式。
- 另外一種就是作為CMS收集器發(fā)生失敗時的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時使用。
- 一種是在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用。
-
運行過程
image.png
例子
public class GCDemo {
public static void main(String[] args) throws InterruptedException {
Random rand = new Random(System.nanoTime());
try {
String str = "Hello, World";
while(true) {
str += str + rand.nextInt(Integer.MAX_VALUE) + rand.nextInt(Integer.MAX_VALUE);
}
}catch (Throwable e) {
e.printStackTrace();
}
}
}
jvm參數(shù)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC
日志
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation
-XX:+UseSerialGC /注意這里 DefNew Default New Generation/
[GC (Allocation Failure) [DefNew: 2346K->320K(3072K), 0.0012956 secs] 2346K->1030K(9920K), 0.0013536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2888K->0K(3072K), 0.0013692 secs] 3598K->2539K(9920K), 0.0014059 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2065K->0K(3072K), 0.0011613 secs] 4604K->4550K(9920K), 0.0011946 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2056K->0K(3072K), 0.0010394 secs] 6606K->6562K(9920K), 0.0010808 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2011K->2011K(3072K), 0.0000124 secs][Tenured /注意這里old(老年代)是Tenured/: 6562K->2537K(6848K), 0.0021691 secs] 8574K->2537K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0024399 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew: 2059K->2059K(3072K), 0.0000291 secs][Tenured: 6561K->6561K(6848K), 0.0012330 secs] 8620K->6561K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0012888 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 6561K->6547K(6848K), 0.0017784 secs] 6561K->6547K(9920K), [Metaspace: 2658K->2658K(1056768K)], 0.0018111 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOfRange(Arrays.java:3664)
at java.lang.String.<init>(String.java:207)
at java.lang.StringBuilder.toString(StringBuilder.java:407)
at com.lun.jvm.GCDemo.main(GCDemo.java:23)
Heap
def new generation total 3072K, used 105K [0x00000000ff600000, 0x00000000ff950000, 0x00000000ff950000)
eden space 2752K, 3% used [0x00000000ff600000, 0x00000000ff61a7c8, 0x00000000ff8b0000)
from space 320K, 0% used [0x00000000ff8b0000, 0x00000000ff8b0000, 0x00000000ff900000)
to space 320K, 0% used [0x00000000ff900000, 0x00000000ff900000, 0x00000000ff950000)
tenured generation total 6848K, used 6547K [0x00000000ff950000, 0x0000000100000000, 0x0000000100000000)
the space 6848K, 95% used [0x00000000ff950000, 0x00000000fffb4c30, 0x00000000fffb4e00, 0x0000000100000000)
Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
日志解讀:
1.使用SerialGC時,年輕代是表示為:DefNew(DefNew Default New Generation),老年代表示為:Tenured;
Parallel Old收集器
記憶
- 介紹
- Parallel Old是<font color=red>Parallel Scavenge收集器的老年代版本</font>,支持<font color=red>多線程并發(fā)收集</font>,基于<font color=red>標(biāo)記-整理算法</font>實現(xiàn)。
- 配合Parallel Scavenge收集器使用
- 這個收集器是直到JDK 6時才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處于相當(dāng)尷尬的狀態(tài),原因是如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外別無選擇,其他表現(xiàn)良好的老年代收集器,如CMS無法與它配合工作。
- 由于老年代Serial Old收集器在服務(wù)端應(yīng)用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整體上獲得吞吐量最大化的效果。
- 同樣,由于單線程的老年代收集中無法充分利用服務(wù)器多處理器的并行處理能力,在老年代內(nèi)存空間很大而且硬件規(guī)格比較高級的運行環(huán)境中,這種組合的總吞吐量甚至不一定比ParNew加CMS的組合來得優(yōu)秀。
- 適用場合
- 在注重吞吐量或者處理器資源較為稀缺的場合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器這個組合。
-
運行過程
image.png
例子
public class GCDemo {
public static void main(String[] args) throws InterruptedException {
Random rand = new Random(System.nanoTime());
try {
String str = "Hello, World";
while(true) {
str += str + rand.nextInt(Integer.MAX_VALUE) + rand.nextInt(Integer.MAX_VALUE);
}
}catch (Throwable e) {
e.printStackTrace();
}
}
}
jvm參數(shù)
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC
日志
-XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelOldGC
[GC (Allocation Failure) [PSYoungGen: 1979K->480K(2560K)] 1979K->848K(9728K), 0.0007724 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2205K->480K(2560K)] 2574K->2317K(9728K), 0.0008700 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2446K->496K(2560K)] 4284K->3312K(9728K), 0.0010374 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1499K->0K(2560K)] [ParOldGen: 6669K->2451K(7168K)] 8168K->2451K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0043327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1966K->0K(2560K)] [ParOldGen: 6304K->6304K(7168K)] 8270K->6304K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0021269 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 6304K->6304K(9728K), 0.0004841 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 6304K->6290K(7168K)] 6304K->6290K(9728K), [Metaspace: 2658K->2658K(1056768K)], 0.0058149 secs] [Times: user=0.11 sys=0.00, real=0.01 secs]
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOfRange(Arrays.java:3664)
at java.lang.String.<init>(String.java:207)
at java.lang.StringBuilder.toString(StringBuilder.java:407)
at com.lun.jvm.GCDemo.main(GCDemo.java:22)
Heap
PSYoungGen total 2560K, used 81K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 2048K, 3% used [0x00000000ffd00000,0x00000000ffd14768,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen total 7168K, used 6290K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 87% used [0x00000000ff600000,0x00000000ffc24b70,0x00000000ffd00000)
Metaspace used 2689K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 286K, capacity 386K, committed 512K, reserved 1048576K
日志解讀
1.使用Parallel Old GC時,年輕代是表示為: PSYoungGen,老年代表示為:ParOldGen。
CMS收集器
記憶
- 介紹
- CMS(Concurrent Mark Sweep)收集器是一種以<font color=apple green>獲取最短回收停頓時間</font>為目標(biāo)的收集器。
- 使用場景
- 較為關(guān)注服務(wù)的響應(yīng)速度應(yīng)用。
- 目前很大一部分的Java應(yīng)用集中在互聯(lián)網(wǎng)網(wǎng)站或者基于瀏覽器的B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用通常都會較為關(guān)注服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時間盡可能短,以給用戶帶來良好的交互體驗。CMS收集器就非常符合這類應(yīng)用的需求。
- 較為關(guān)注服務(wù)的響應(yīng)速度應(yīng)用。
- 運作過程
- 從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于<font color=red>標(biāo)記-清除算法</font>實現(xiàn)的,它的運作過程相對于前面幾種收集器來說要更復(fù)雜一些,整個過程分為四個步驟,包括:
- 初始標(biāo)記(CMS initial mark)
- 只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)的對象,速度很快,仍然<font color=red>需要暫停所有的工作線程</font>。
- 并發(fā)標(biāo)記(CMS concurrent mark)
- 并發(fā)標(biāo)記階段就是從<font color=green>GC Roots的直接關(guān)聯(lián)對象</font>開始<font color=red>遍歷整個對象圖的過程</font>,這個過程耗時較長但是<font color=red>不需要停頓用戶線程</font>,可以與<font color=red>垃圾收集線程一起并發(fā)運行</font>。
- 重新標(biāo)記(CMS remark)
- 為了修正在并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運行而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄
- 仍然<font color=red>需要暫停所有的工作線程</font>。
- 由于并發(fā)標(biāo)記時,用戶線程依然運行,因此在正式清理前,再做修正。
- 這個階段的停頓時間通常會比初始標(biāo)記階段稍長一些,但也遠(yuǎn)比并發(fā)標(biāo)記階段的時間短。
- 并發(fā)清除(CMS concurrent sweep)
- 清除GCRoots不可達(dá)對象,和用戶線程一起工作,<font color=red>不需要暫停工作線程</font>。
- 基于標(biāo)記結(jié)果,直接清理對象。
- 初始標(biāo)記(CMS initial mark)
- 從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于<font color=red>標(biāo)記-清除算法</font>實現(xiàn)的,它的運作過程相對于前面幾種收集器來說要更復(fù)雜一些,整個過程分為四個步驟,包括:
- 優(yōu)點
- 并發(fā)收集、低停頓
- 由于耗時最長的并發(fā)標(biāo)記和并發(fā)清除過程中,垃圾收集線程可以和用戶現(xiàn)在一起并發(fā)工作,所以總體上來看CMS 收集器的內(nèi)存回收和用戶線程是一起并發(fā)地執(zhí)行。
- 并發(fā)收集、低停頓
- 缺點
- 并發(fā)執(zhí)行,對CPU資源壓力大,所以對處理器非常敏感。
- 在并發(fā)階段,它雖然不會導(dǎo)致用戶線程停頓,但卻會因為占用了一部分線程(或者說處理器的計算能力)而導(dǎo)致應(yīng)用程序變慢,降低總吞吐量。
- CMS默認(rèn)啟動的回收線程數(shù)是(處理器核心數(shù)量+3)/4
- 也就是說,如果處理器核心數(shù)在四個或以上,并發(fā)回收時垃圾收集線程只占用不超過25%的處理器運算資源,并且會隨著處理器核心數(shù)量的增加而下降。
- 但是當(dāng)處理器核心數(shù)量不足四個時,CMS對用戶程序的影響就可能變得很大。
- 如果應(yīng)用本來的處理器負(fù)載就很高,還要分出一半的運算能力去執(zhí)行收集器線程,就可能導(dǎo)致用戶程序的執(zhí)行速度忽然大幅降低。
- 為了緩解這種情況,虛擬機提供了一種稱為“增量式并發(fā)收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器變種,所做的事情和以前單核處理器年代PC機操作系統(tǒng)靠搶占式多任務(wù)來模擬多核并行多任務(wù)的思想一樣,是在并發(fā)標(biāo)記、清理的時候讓收集器線程、用戶線程交替運行,盡量減少垃圾收集線程的獨占資源的時間,這樣整個垃圾收集的過程會更長,但對用戶程序的影響就會顯得較少一些,直觀感受是速度變慢的時間更多了,但速度下降幅度就沒有那么明顯。實踐證明增量式的CMS收集器效果很一般,從JDK 7開始,i-CMS模式已經(jīng)被聲明為“deprecated”,即已過時不再提倡用戶使用,到JDK 9發(fā)布后i-CMS模式被完全廢棄。
- CMS收集器無法處理“浮動垃圾”(Floating Garbage),有可能出現(xiàn)“Con-current Mode Failure”失敗進(jìn)而導(dǎo)致另一次完全“Stop The World”的Full GC的產(chǎn)生。
- 在CMS的并發(fā)標(biāo)記和并發(fā)清理階段,用戶線程是還在繼續(xù)運行的,程序在運行自然就還會伴隨有新的垃圾對象不斷產(chǎn)生,但這一部分垃圾對象是出現(xiàn)在標(biāo)記過程結(jié)束以后,CMS無法在當(dāng)次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉。這一部分垃圾就稱為“浮動垃圾”。
- 同樣也是由于在垃圾收集階段用戶線程還需要持續(xù)運行,那就還需要預(yù)留足夠內(nèi)存空間提供給用戶線程使用,因此CMS收集器不能像其他收集器那樣等待到老年代幾乎完全被填滿了再進(jìn)行收集,必須預(yù)留一部分空間供并發(fā)收集時的程序運作使用。
- 在JDK 5的默認(rèn)設(shè)置下,CMS收集器當(dāng)老年代使用了68%的空間后就會被激活,這是一個偏保守的設(shè)置,如果在實際應(yīng)用中老年代增長并不是太快,可以適當(dāng)調(diào)高參數(shù)-XX:CMSInitiatingOccu-pancyFraction的值來提高CMS的觸發(fā)百分比,降低內(nèi)存回收頻率,獲取更好的性能。
- 到了JDK 6時,CMS收集器的啟動閾值就已經(jīng)默認(rèn)提升至92%。但這又會更容易面臨另一種風(fēng)險:
- 要是CMS運行期間預(yù)留的內(nèi)存無法滿足程序分配新對象的需要,就會出現(xiàn)一次“并發(fā)失敗”(Concurrent Mode Failure),這時候虛擬機將不得不啟動后備預(yù)案:凍結(jié)用戶線程的執(zhí)行,臨時啟用Serial Old收集器來重新進(jìn)行老年代的垃圾收集,但這樣停頓時間就很長了。
- 所以參數(shù)-XX:CMSInitiatingOccupancyFraction設(shè)置得太高將會很容易導(dǎo)致大量的并發(fā)失敗產(chǎn)生,性能反而降低,用戶應(yīng)在生產(chǎn)環(huán)境中根據(jù)實際應(yīng)用情況來權(quán)衡設(shè)置。
- CMS是一款基于“標(biāo)記-清除”算法實現(xiàn)的收集器,這意味著收集結(jié)束時會有大量空間碎片產(chǎn)生
- 空間碎片過多時,將會給大對象分配帶來很大麻煩
- 往往會出現(xiàn)老年代還有很多剩余空間,但就是無法找到足夠大的連續(xù)空間來分配當(dāng)前對象,而不得不提前觸發(fā)一次Full GC的情況。
- 為了解決這個問題,CMS收集器提供了一個-XX:+UseCMS-CompactAtFullCollection開關(guān)參數(shù)(默認(rèn)是開啟的,此參數(shù)從JDK 9開始廢棄),用于在CMS收集器不得不進(jìn)行Full GC時開啟內(nèi)存碎片的合并整理過程。
- 由于這個內(nèi)存整理必須移動存活對象,(在Shenandoah和ZGC出現(xiàn)前)是無法并發(fā)的。這樣空間碎片問題是解決了,但停頓時間又會變長。
- 因此虛擬機設(shè)計者們還提供了另外一個參數(shù)-XX:CMSFullGCsBefore-Compaction(此參數(shù)從JDK 9開始廢棄),這個參數(shù)的作用是要求CMS收集器在執(zhí)行過若干次(數(shù)量由參數(shù)值決定)不整理空間的Full GC之后,下一次進(jìn)入Full GC前會先進(jìn)行碎片整理(默認(rèn)值為0,表示每次進(jìn)入Full GC時都進(jìn)行碎片整理)。
- 空間碎片過多時,將會給大對象分配帶來很大麻煩
- 并發(fā)執(zhí)行,對CPU資源壓力大,所以對處理器非常敏感。
CMS收集器運行示意圖
Garbage First 收集器
歷史
? Garbage First(簡稱G1)收集器是垃圾收集器技術(shù)發(fā)展歷史上的里程碑式的成果,它開創(chuàng)了收集器面向局部收集的設(shè)計思路和基于Region的內(nèi)存布局形式。早在JDK 7剛剛確立項目目標(biāo)、Oracle公司制定的JDK 7 RoadMap里面,G1收集器就被視作JDK 7中HotSpot虛擬機的一項重要進(jìn)化特征。從JDK 6 Update 14開始就有Early Access版本的G1收集器供開發(fā)人員實驗和試用,但由此開始G1收集器的“實驗狀態(tài)”(Experimental)持續(xù)了數(shù)年時間,直至JDK 7 Update 4,Oracle才認(rèn)為它達(dá)到足夠成熟的商用程度,移除了“Experimental”的標(biāo)識;到了JDK 8 Update 40的時候,G1提供并發(fā)的類卸載的支持,補全了其計劃功能的最后一塊拼圖。這個版本以后的G1收集器才被Oracle官方稱為“全功能的垃圾收集器”(Fully-Featured Garbage Collector)。
? G1是一款主要面向服務(wù)端應(yīng)用的垃圾收集器。HotSpot開發(fā)團隊最初賦予它的期望是(在比較長期的)未來可以替換掉JDK 5中發(fā)布的CMS收集器。現(xiàn)在這個期望目標(biāo)已經(jīng)實現(xiàn)過半了,JDK 9發(fā)布之日,G1宣告取代Parallel Scavenge加Parallel Old組合,成為服務(wù)端模式下的默認(rèn)垃圾收集器,而CMS則淪落至被聲明為不推薦使用(Deprecate)的收集器[1]。如果對JDK 9及以上版本的HotSpot虛擬機使用參數(shù)-XX:+UseConcMarkSweepGC來開啟CMS收集器的話,用戶會收到一個警告信息,提示CMS未來將會被廢棄:
Java HotSpot(TM) 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0 and will likely be removed in a future release.
? 但作為一款曾被廣泛運用過的收集器,經(jīng)過多個版本的開發(fā)迭代后,CMS(以及之前幾款收集器)的代碼與HotSpot的內(nèi)存管理、執(zhí)行、編譯、監(jiān)控等子系統(tǒng)都有千絲萬縷的聯(lián)系,這是歷史原因?qū)е碌模⒉环下氊?zé)分離的設(shè)計原則。為此,規(guī)劃JDK 10功能目標(biāo)時,HotSpot虛擬機提出了“統(tǒng)一垃圾收集器接口”[2],將內(nèi)存回收的“行為”與“實現(xiàn)”進(jìn)行分離,CMS以及其他收集器都重構(gòu)成基于這套接口的一種實現(xiàn)。以此為基礎(chǔ),日后要移除或者加入某一款收集器,都會變得容易許多,風(fēng)險也可以控制,這算是在為CMS退出歷史舞臺鋪下最后的道路了。
? 作為CMS收集器的替代者和繼承人,設(shè)計者們希望做出一款能夠建立起“停頓時間模型”(Pause Prediction Model)的收集器,停頓時間模型的意思是能夠支持指定在一個長度為M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間大概率不超過N毫秒這樣的目標(biāo),這幾乎已經(jīng)是實時Java(RTSJ)的中軟實時垃圾收集器特征了。
? 那具體要怎么做才能實現(xiàn)這個目標(biāo)呢?首先要有一個思想上的改變,在G1收集器出現(xiàn)之前的所有其他收集器,包括CMS在內(nèi),垃圾收集的目標(biāo)范圍要么是整個新生代(Minor GC),要么就是整個老年代(Major GC),再要么就是整個Java堆(Full GC)。而G1跳出了這個樊籠,它可以面向堆內(nèi)存任何部分來組成回收集(Collection Set,一般簡稱CSet)進(jìn)行回收,衡量標(biāo)準(zhǔn)不再是它屬于哪個分代,而是哪塊內(nèi)存中存放的垃圾數(shù)量最多,回收收益最大,這就是G1收集器的Mixed GC模式。
記憶
- 介紹
- G1是一款主要面向服務(wù)端應(yīng)用的垃圾收集器,應(yīng)用在多處理器和大容量內(nèi)存環(huán)境中,在實現(xiàn)高吞吐量的同時,盡可能的滿足垃圾收集暫停時間的要求。
- G1不再堅持固定大小以及固定數(shù)量的分代區(qū)域劃分,而是把連續(xù)的Java堆<font color=red>劃分</font>為<font color=apple green>多個大小相等的獨立區(qū)域(Region)</font>,<font color=red>每一個Region都可以根據(jù)需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間</font>。收集器能夠<font color=apple green>對扮演不同角色的Region采用不同的策略去處理</font>,這樣無論是新創(chuàng)建的對象還是已經(jīng)存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果。
- 如何存儲大對象
- Humongous區(qū)域,專門用來存儲大對象。
- G1認(rèn)為只要大小超過了一個Region容量一半的對象即可判定為大對象。每個Region的大小可以通過參數(shù)-XX:G1HeapRegionSize設(shè)定,取值范圍為1MB~32MB,且應(yīng)為2的N次冪。
- 而對于那些超過了整個Region容量的超級大對象,將會被存放在N個連續(xù)的Humongous Region之中,G1的大多數(shù)行為都把Humongous Region作為老年代的一部分來進(jìn)行看待。
- 特點
- Eden,Survivor和Tenured等內(nèi)存區(qū)域不再是連續(xù)的了,而是變成了一個個大小一樣的region,宏觀上看G1之中不再區(qū)分年輕代和老年代。
- 雖然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它們都是一系列區(qū)域(不需要連續(xù))的動態(tài)集合。
- G1收集器之所以能建立可預(yù)測的停頓時間模型,是因為它<font color=red>將Region作為單次回收的最小單元,即每次收集到的內(nèi)存空間都是Region大小的整數(shù)倍</font>,這樣可以<font color=apple green>有計劃地避免在整個Java堆中進(jìn)行全區(qū)域的垃圾收集</font>。
- 更具體的處理思路是讓G1收集器去跟蹤各個Region里面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經(jīng)驗值,然后在后臺維護一個優(yōu)先級列表,每次根據(jù)用戶設(shè)定允許的收集停頓時間(使用參數(shù)-XX:MaxGCPauseMillis指定,默認(rèn)值是200毫秒),優(yōu)先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來。這種使用Region劃分內(nèi)存空間,以及具有優(yōu)先級的區(qū)域回收方式,保證了G1收集器在有限的時間內(nèi)獲取盡可能高的收集效率。
- 可以由用戶指定期望的停頓時間是G1收集器很強大的一個功能,設(shè)置不同的期望停頓時間,可使得G1在不同應(yīng)用場景中取得關(guān)注吞吐量和關(guān)注延遲之間的最佳平衡。
- 這里設(shè)置的“期望值”必須是符合實際的,不能異想天開,畢竟G1是要凍結(jié)用戶線程來復(fù)制對象的,這個停頓時間再怎么低也得有個限度。
- 它默認(rèn)的停頓目標(biāo)為兩百毫秒,一般來說,回收階段占到幾十到一百甚至接近兩百毫秒都很正常。
- 但如果我們把停頓時間調(diào)得非常低,譬如設(shè)置為二十毫秒,很可能出現(xiàn)的結(jié)果就是由于停頓目標(biāo)時間太短,導(dǎo)致每次選出來的回收集只占堆內(nèi)存很小的一部分,收集器收集的速度逐漸跟不上分配器分配的速度,導(dǎo)致垃圾慢慢堆積。很可能一開始收集器還能從空閑的堆內(nèi)存中獲得一些喘息的時間,但應(yīng)用運行時間一長就不行了,最終占滿堆引發(fā)Full GC反而降低性能,所以通常把期望停頓時間設(shè)置為一兩百毫秒或者兩三百毫秒會是比較合理的。
- 從G1開始,最先進(jìn)的垃圾收集器的設(shè)計導(dǎo)向都不約而同地變?yōu)樽非竽軌驊?yīng)付應(yīng)用的內(nèi)存分配速率(Allocation Rate),而不追求一次把整個Java堆全部清理干凈。
- 這樣,應(yīng)用在分配,同時收集器在收集,只要收集的速度能跟得上對象分配的速度,那一切就能運作得很完美。這種新的收集器設(shè)計思路從工程實現(xiàn)上看是從G1開始興起的,所以說G1是收集器技術(shù)發(fā)展的一個里程碑。
- G1能充分利用多CPU、多核環(huán)境硬件優(yōu)勢,盡量縮短STW
- G1雖然也是分代收集器,但整個內(nèi)存分區(qū)不存在物理上的年輕代與老年代的區(qū)別,也不需要完全獨立的survivor(to space)堆做復(fù)制準(zhǔn)備。G1只有邏輯上的分代概念,或者說每個分區(qū)都可能隨G1的運行在不同代之間前后切換。
- Eden,Survivor和Tenured等內(nèi)存區(qū)域不再是連續(xù)的了,而是變成了一個個大小一樣的region,宏觀上看G1之中不再區(qū)分年輕代和老年代。
- G1將堆內(nèi)存“化整為零”的“解題思路”,看起來似乎沒有太多令人驚訝之處,也完全不難理解,但其中的實現(xiàn)細(xì)節(jié)可是遠(yuǎn)遠(yuǎn)沒有想象中那么簡單,否則就不會從2004年Sun實驗室發(fā)表第一篇關(guān)于G1的論文后一直拖到2012年4月JDK 7 Update 4發(fā)布,用將近10年時間才倒騰出能夠商用的G1收集器來。G1收集器至少有(不限于)以下這些關(guān)鍵的細(xì)節(jié)問題需要妥善解決:
- 譬如,將Java堆分成多個獨立Region后,Region里面存在的跨Region引用對象如何解決?
- 使用記憶集避免全堆作為GC Roots掃描,但在G1收集器上記憶集的應(yīng)用其實要復(fù)雜很多,它的每個Region都維護有自己的記憶集,這些記憶集會記錄下別的Region指向自己的指針,并標(biāo)記這些指針分別在哪些卡頁的范圍之內(nèi)。G1的記憶集在存儲結(jié)構(gòu)的本質(zhì)上是一種哈希表,Key是別的Region的起始地址,Value是一個集合,里面存儲的元素是卡表的索引號。這種“雙向”的卡表結(jié)構(gòu)(卡表是“我指向誰”,這種結(jié)構(gòu)還記錄了“誰指向我”)比原來的卡表實現(xiàn)起來更復(fù)雜,同時由于Region數(shù)量比傳統(tǒng)收集器的分代數(shù)量明顯要多得多,因此G1收集器要比其他的傳統(tǒng)垃圾收集器有著更高的內(nèi)存占用負(fù)擔(dān)。根據(jù)經(jīng)驗,G1至少要耗費大約相當(dāng)于Java堆容量10%至20%的額外內(nèi)存來維持收集器工作。
- 譬如,在并發(fā)標(biāo)記階段如何保證收集線程與用戶線程互不干擾地運行?
- 這里首先要解決的是用戶線程改變對象引用關(guān)系時,必須保證其不能打破原本的對象圖結(jié)構(gòu),導(dǎo)致標(biāo)記結(jié)果出現(xiàn)錯誤,該問題的解決辦法筆者已經(jīng)抽出獨立小節(jié)來講解過(見3.4.6節(jié)):CMS收集器采用增量更新算法實現(xiàn),而G1收集器則是通過原始快照(SATB)算法來實現(xiàn)的。此外,垃圾收集對用戶線程的影響還體現(xiàn)在回收過程中新創(chuàng)建對象的內(nèi)存分配上,程序要繼續(xù)運行就肯定會持續(xù)有新對象被創(chuàng)建,G1為每一個Region設(shè)計了兩個名為TAMS(Top at Mark Start)的指針,把Region中的一部分空間劃分出來用于并發(fā)回收過程中的新對象分配,并發(fā)回收時新分配的對象地址都必須要在這兩個指針位置以上。G1收集器默認(rèn)在這個地址以上的對象是被隱式標(biāo)記過的,即默認(rèn)它們是存活的,不納入回收范圍。與CMS中的“Concurrent Mode Failure”失敗會導(dǎo)致Full GC類似,如果內(nèi)存回收的速度趕不上內(nèi)存分配的速度,G1收集器也要被迫凍結(jié)用戶線程執(zhí)行,導(dǎo)致Full GC而產(chǎn)生長時間“Stop The World”。
- 譬如,怎樣建立起可靠的停頓預(yù)測模型?
- 用戶通過-XX:MaxGCPauseMillis參數(shù)指定的停頓時間只意味著垃圾收集發(fā)生之前的期望值,但G1收集器要怎么做才能滿足用戶的期望呢?G1收集器的停頓預(yù)測模型是以衰減均值(Decaying Average)為理論基礎(chǔ)來實現(xiàn)的,在垃圾收集過程中,G1收集器會記錄每個Region的回收耗時、每個Region記憶集里的臟卡數(shù)量等各個可測量的步驟花費的成本,并分析得出平均值、標(biāo)準(zhǔn)偏差、置信度等統(tǒng)計信息。這里強調(diào)的“衰減平均值”是指它會比普通的平均值更容易受到新數(shù)據(jù)的影響,平均值代表整體平均狀態(tài),但衰減平均值更準(zhǔn)確地代表“最近的”平均狀態(tài)。換句話說,Region的統(tǒng)計狀態(tài)越新越能決定其回收的價值。然后通過這些信息預(yù)測現(xiàn)在開始回收的話,由哪些Region組成回收集才可以在不超過期望停頓時間的約束下獲得最高的收益。
- 譬如,將Java堆分成多個獨立Region后,Region里面存在的跨Region引用對象如何解決?
- 如果我們不去計算用戶線程運行過程中的動作(如使用寫屏障維護記憶集的操作),G1收集器的運作過程大致可劃分為以下四個步驟:
- 初始標(biāo)記(Initial Marking):
- 僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象,并且修改TAMS指針的值,讓下一階段用戶線程并發(fā)運行時,能正確地在可用的Region中分配新對象。這個階段需要停頓線程,但耗時很短,而且是借用進(jìn)行Minor GC的時候同步完成的,所以G1收集器在這個階段實際并沒有額外的停頓。
- 并發(fā)標(biāo)記(Concurrent Marking):
- 從GC Root開始對堆中對象進(jìn)行可達(dá)性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序并發(fā)執(zhí)行。當(dāng)對象圖掃描完成以后,還要重新處理SATB記錄下的在并發(fā)時有引用變動的對象。
- 最終標(biāo)記(Final Marking):
- 對用戶線程做另一個短暫的暫停,用于處理并發(fā)階段結(jié)束后仍遺留下來的最后那少量的SATB記錄。
- 篩選回收(Live Data Counting and Evacuation):
- 負(fù)責(zé)更新Region的統(tǒng)計數(shù)據(jù),對各個Region的回收價值和成本進(jìn)行排序,根據(jù)用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構(gòu)成回收集,然后把決定回收的那一部分Region的存活對象復(fù)制到空的Region中,再清理掉整個舊Region的全部空間。這里的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程并行完成的。
- 從上述階段的描述可以看出,G1收集器除了并發(fā)標(biāo)記外,其余階段也是要完全暫停用戶線程的,換言之,它并非純粹地追求低延遲,官方給它設(shè)定的目標(biāo)是在延遲可控的情況下獲得盡可能高的吞吐量,所以才能擔(dān)當(dāng)起“全功能收集器”的重任與期望。
- 初始標(biāo)記(Initial Marking):
- G1收集器的設(shè)計目標(biāo)是取代CMS收集器,它同CMS相比,在以下方面表現(xiàn)的更出色:
- G1是一個有整理內(nèi)存過程的垃圾收集器,不會產(chǎn)生很多內(nèi)存碎片。
- G1的Stop The World(STW)更可控,G1在停頓時間上添加了預(yù)測機制,用戶可以指定期望停頓時間。
- CMS垃圾收集器雖然減少了暫停應(yīng)用程序的運行時間,但是它還是存在著內(nèi)存碎片問題。于是,為了去除內(nèi)存碎片問題,同時又保留CMS垃圾收集器低暫停時間的優(yōu)點,JAVA7發(fā)布了一個新的垃圾收集器-G1垃圾收集器。
? G1是在2012年才在jdk1.7u4中可用。oracle官方計劃在JDK9中將G1變成默認(rèn)的垃圾收集器以替代CMS。它是一款面向服務(wù)端應(yīng)用的收集器,主要應(yīng)用在多CPU和大內(nèi)存服務(wù)器環(huán)境下,極大的減少垃圾收集的停頓時間,全面提升服務(wù)器的性能,逐步替換java8以前的CMS收集器。
? 現(xiàn)在這個期望目標(biāo)已經(jīng)實現(xiàn)過半了,JDK 9發(fā)布之日,G1宣告取代Parallel Scavenge加Parallel Old組合,成為服務(wù)端模式下的默認(rèn)垃圾收集器,而CMS則淪落至被聲明為不推薦使用(Deprecate)的收集器。
? 但是G1相對于CMS仍然不是占全方位、壓倒性優(yōu)勢的,從它出現(xiàn)幾年仍不能在所有應(yīng)用場景中代替CMS就可以得知這個結(jié)論。比起CMS,G1的弱項也可以列舉出不少:
- 如在用戶程序運行過程中,G1無論是為了垃圾收集產(chǎn)生的內(nèi)存占用(Footprint)還是程序運行時的額外執(zhí)行負(fù)載(Overload)都要比CMS要高。
- 就內(nèi)存占用來說,雖然G1和CMS都使用卡表來處理跨代指針,但G1的卡表實現(xiàn)更為復(fù)雜,而且堆中每個Region,無論扮演的是新生代還是老年代角色,都必須有一份卡表,這導(dǎo)致G1的記憶集(和其他內(nèi)存消耗)可能會占整個堆容量的20%乃至更多的內(nèi)存空間;相比起來CMS的卡表就相當(dāng)簡單,只有唯一一份,而且只需要處理老年代到新生代的引用,反過來則不需要,由于新生代的對象具有朝生夕滅的不穩(wěn)定性,引用變化頻繁,能省下這個區(qū)域的維護開銷是很劃算的。
- 在執(zhí)行負(fù)載的角度上,同樣由于兩個收集器各自的細(xì)節(jié)實現(xiàn)特點導(dǎo)致了用戶程序運行時的負(fù)載會有不同,譬如它們都使用到寫屏障,CMS用寫后屏障來更新維護卡表;而G1除了使用寫后屏障來進(jìn)行同樣的(由于G1的卡表結(jié)構(gòu)復(fù)雜,其實是更煩瑣的)卡表維護操作外,為了實現(xiàn)原始快照搜索(SATB)算法,還需要使用寫前屏障來跟蹤并發(fā)時的指針變化情況。相比起增量更新算法,原始快照搜索能夠減少并發(fā)標(biāo)記和重新標(biāo)記階段的消耗,避免CMS那樣在最終標(biāo)記階段停頓時間過長的缺點,但是在用戶程序運行過程中確實會產(chǎn)生由跟蹤引用變化帶來的額外負(fù)擔(dān)。由于G1對寫屏障的復(fù)雜操作要比CMS消耗更多的運算資源,所以CMS的寫屏障實現(xiàn)是直接的同步操作,而G1就不得不將其實現(xiàn)為類似于消息隊列的結(jié)構(gòu),把寫前屏障和寫后屏障中要做的事情都放到隊列里,然后再異步處理。
? 以上的優(yōu)缺點對比僅僅是針對G1和CMS兩款垃圾收集器單獨某方面的實現(xiàn)細(xì)節(jié)的定性分析,通常我們說哪款收集器要更好、要好上多少,往往是針對具體場景才能做的定量比較。按照筆者的實踐經(jīng)驗,目前在小內(nèi)存應(yīng)用上CMS的表現(xiàn)大概率仍然要會優(yōu)于G1,而在大內(nèi)存應(yīng)用上G1則大多能發(fā)揮其優(yōu)勢,這個優(yōu)劣勢的Java堆容量平衡點通常在6GB至8GB之間,當(dāng)然,以上這些也僅是經(jīng)驗之談,不同應(yīng)用需要量體裁衣地實際測試才能得出最合適的結(jié)論,隨著HotSpot的開發(fā)者對G1的不斷優(yōu)化,也會讓對比結(jié)果繼續(xù)向G1傾斜。
G1收集器下的Young GC
針對Eden區(qū)進(jìn)行收集,Eden區(qū)耗盡后會被觸發(fā),主要是小區(qū)域收集+形成連續(xù)的內(nèi)存塊,避免內(nèi)存碎片
- Eden區(qū)的數(shù)據(jù)移動到Survivor區(qū),假如出現(xiàn)Survivor區(qū)空間不夠,Eden區(qū)數(shù)據(jù)會部會晉升到Old區(qū)。
- Survivor區(qū)的數(shù)據(jù)移動到新的Survivor區(qū),部會數(shù)據(jù)晉升到Old區(qū)。
- 最后Eden區(qū)收拾干凈了,GC結(jié)束,用戶的應(yīng)用程序繼續(xù)執(zhí)行。
GC之G1參數(shù)配置及和CMS的比較
-XX:+UseG1GC
-XX:G1HeapRegionSize=n:設(shè)置的G1區(qū)域的大小。值是2的冪,范圍是1MB到32MB。目標(biāo)是根據(jù)最小的Java堆大小劃分出約2048個區(qū)域。
-XX:MaxGCPauseMillis=n:最大GC停頓時間,這是個軟目標(biāo),JVM將盡可能(但不保證)停頓小于這個時間。
-XX:InitiatingHeapOccupancyPercent=n:堆占用了多少的時候就觸發(fā)GC,默認(rèn)為45。
-XX:ConcGCThreads=n:并發(fā)GC使用的線程數(shù)。
-XX:G1ReservePercent=n:設(shè)置作為空閑空間的預(yù)留內(nèi)存百分比,以降低目標(biāo)空間溢出的風(fēng)險,默認(rèn)值是10%。
開發(fā)人員僅僅需要聲明以下參數(shù)即可:
三步歸納:開始G1+設(shè)置最大內(nèi)存+設(shè)置最大停頓時間
- -XX:+UseG1GC
- -Xmx32g
- -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=n:最大GC停頓時間單位毫秒,這是個軟目標(biāo),JVM將盡可能(但不保證)停頓小于這個時間
G1和CMS比較
- G1不會產(chǎn)生內(nèi)碎片
- 是可以精準(zhǔn)控制停頓。該收集器是把整個堆(新生代、老年代)劃分成多個固定大小的區(qū)域,每次根據(jù)允許停頓的時間去收集垃圾最多的區(qū)域。