垃圾收集器以及記憶體分配
- 2020 年 3 月 5 日
- 筆記
一、分類
在jvm中,實現了多種垃圾收集器,包括:串列垃圾收集器、並行垃圾收集器、CMS(並發)垃圾收集器、G1垃圾收集器
二、串列垃圾收集器
串列垃圾收集器,是指使用單執行緒進行垃圾回收,垃圾回收時,只有一個執行緒在工作,並且java應用中的所有執行緒都要暫停,等待垃圾回收的完成。這種現象稱之為STW(Stop-The-World)。
對於交互性較強的應用而言,這種垃圾收集器是不能夠接受的。
一般在Javaweb應用中是不會採用該收集器的。
1、編寫測試程式碼
package com.zn; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Random; public class GCTest { public static void main(String[] args) throws Exception { List<Object> list = new ArrayList<Object>(); while (true) { int sleep = new Random().nextInt(100); if (System.currentTimeMillis() % 2 == 0) { list.clear(); } else { for (int i = 0; i < 10000; i++) { Properties properties = new Properties(); properties.put("key_" + i, "value_" + System.currentTimeMillis() + i); list.add(properties); } } // System.out.println("list大小為:" + list.size()); Thread.sleep(sleep); } } }
2、設置垃圾回收為串列收集器
在程式運行參數中添加2個參數,如下:
-XX:+UseSerialGC:指定年輕代和老年代都使用串列垃圾收集器
-XX:+PrintGCDetails:列印垃圾回收的詳細資訊
-XX:+UseSerialGC -XX:+PrintGCDetails -Xms16m -Xmx16m
3、啟動程式資訊列印

GC日誌資訊解讀:
年輕代的記憶體GC前後的大小:
DefNew:表示使用的是串列垃圾收集器。
4928K->512K(4928K):表示,年輕代GC前,佔有4416K記憶體,GC後,佔有512K記憶體,總大小4928K
0.0043929 secs:表示,GC所用的時間,單位為毫秒。
11130K->9711K(15872K):表示,GC前,堆記憶體佔有11130K,GC後,佔有9711K,總大小為15872K
Full GC:表示,記憶體空間全部進行GC
二、並行垃圾收集器
並行垃圾收集器在串列垃圾收集器的基礎之上做了改進,將單執行緒改為了多執行緒進行垃圾回收,這樣可以縮短垃圾回收的時間。(這裡是指,並行能力較強的機器)當然了,並行垃圾收集器在收集的過程中也會暫停應用程式,這個和串列垃圾回收器是一樣的,只是並行執行,速度更快些,暫停的時間更短一些。
2.1 ParNew垃圾收集器
ParNew垃圾收集器是工作在年輕代上的,只是將串列的垃圾收集器改為了並行。
通過-XX:+UseParNewGC參數設置年輕代使用ParNew回收器,老年代使用的依然是串列收集器。
1、編寫測試程式碼
package com.zn; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Random; public class GCTest { public static void main(String[] args) throws Exception { List<Object> list = new ArrayList<Object>(); while (true) { int sleep = new Random().nextInt(100); if (System.currentTimeMillis() % 2 == 0) { list.clear(); } else { for (int i = 0; i < 10000; i++) { Properties properties = new Properties(); properties.put("key_" + i, "value_" + System.currentTimeMillis() + i); list.add(properties); } // System.out.println("list大小為:" + list.size()); Thread.sleep(sleep); } } }
2、設置垃圾回收為ParNew收集器
-XX:+UseParNewGC -XX:+PrintGCDetails -Xms16m -Xmx16m

3、啟動程式資訊列印

由以上資訊可以看出, ParNew: 使用的是ParNew收集器。其他資訊和串列收集器一致。
2.2 ParallelGC垃圾收集器
ParallelGC收集器工作機制和ParNewGC收集器一樣,只是在此基礎之上,新增了兩個和系統吞吐量相關的參數,使得其使用起來更加的靈活和高效。
相關參數如下:
-XX:+UseParallelGC:年輕代使用ParallelGC垃圾回收器,老年代使用串列回收器。
-XX:+UseParallelOldGC:年輕代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。
-XX:MaxGCPauseMillis:設置最大的垃圾收集時的停頓時間,單位為毫秒
需要注意的是,ParallelGC為了達到設置的停頓時間,可能會調整堆大小或其他的參數,如果堆的大小設置的較小,就會導致GC工作變得很頻繁,反而可能會影響到性能。
該參數使用需謹慎。
-XX:GCTimeRatio:設置垃圾回收時間占程式運行時間的百分比,公式為1/(1+n)。
它的值為0~100之間的數字,默認值為99,也就是垃圾回收時間不能超過1%
-XX:UseAdaptiveSizePolicy:自適應GC模式,垃圾回收器將自動調整年輕代、老年代等參數,達到吞吐量、堆大小、停頓時間之間的平衡。
一般用於,手動調整參數比較困難的場景,讓收集器自動進行調整。
1、編寫測試程式碼
package com.zn; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Random; public class GCTest { public static void main(String[] args) throws Exception { List<Object> list = new ArrayList<Object>(); while (true) { int sleep = new Random().nextInt(100); if (System.currentTimeMillis() % 2 == 0) { list.clear(); } else { for (int i = 0; i < 10000; i++) { Properties properties = new Properties(); properties.put("key_" + i, "value_" + System.currentTimeMillis() + i); list.add(properties); } // System.out.println("list大小為:" + list.size()); Thread.sleep(sleep); } } } 2、設置垃圾回收為ParallelGC收集器
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xms16m -Xmx16m

3、啟動程式資訊列印

由以上資訊可以看出,年輕代和老年代都使用了ParallelGC垃圾回收器。
三、CMS垃圾收集器
CMS全稱 Concurrent Mark Sweep,是一款並發的、使用標記-清除演算法的垃圾回收器,該回收器是針對老年代垃圾回收的,通過參數-XX:+UseConcMarkSweepGC進行設置。
CMS垃圾回收器的執行過程如下:

- 初始化標記(CMS-initial-mark) ,標記root,會導致stw;
- 並發標記(CMS-concurrent-mark),與用戶執行緒同時運行;
- 預清理(CMS-concurrent-preclean),與用戶執行緒同時運行;
- 重新標記(CMS-remark) ,會導致stw;
- 並發清除(CMS-concurrent-sweep),與用戶執行緒同時運行;
- 調整堆大小,設置CMS在清理之後進行記憶體壓縮,目的是清理記憶體中的碎片;
- 並發重置狀態等待下次CMS的觸發(CMS-concurrent-reset),與用戶執行緒同時運行;
1、編寫測試程式碼
package com.zn; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Random; public class GCTest { public static void main(String[] args) throws Exception { List<Object> list = new ArrayList<Object>(); while (true) { int sleep = new Random().nextInt(100); if (System.currentTimeMillis() % 2 == 0) { list.clear(); } else { for (int i = 0; i < 10000; i++) { Properties properties = new Properties(); properties.put("key_" + i, "value_" + System.currentTimeMillis() + i); list.add(properties); } } // System.out.println("list大小為:" + list.size()); Thread.sleep(sleep); } } } 2、設置垃圾回收為CMS收集器
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms16m -Xmx16m

3、啟動程式資訊列印
CMS執行的過程:

四、G1垃圾收集器(重點)
G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方計劃在jdk9中將G1變成默認的垃圾收集器,以替代CMS。
G1的設計原則就是簡化JVM性能調優,開發人員只需要簡單的三步即可完成調優:
1. 第一步,開啟G1垃圾收集器
2. 第二步,設置堆的最大記憶體
3. 第三步,設置最大的停頓時間
G1中提供了三種模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的條件下被觸發。
4.1 原理
G1垃圾收集器相對比其他收集器而言,最大的區別在於它取消了年輕代、老年代的物理劃分,取而代之的是將堆劃分為若干個區域(Region),這些區域中包含了有邏輯上的年輕代、老年代區域。
這樣做的好處就是,我們再也不用單獨的空間對每個代進行設置了,不用擔心每個代內存是否足夠。

在G1劃分的區域中,年輕代的垃圾收集依然採用暫停所有應用執行緒的方式,將存活對象拷貝到老年代或者Survivor空間,G1收集器通過將對象從一個區域複製到另外一個區域,完成了清理工作。
這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms記憶體碎片問題的存在了。
在G1中,有一種特殊的區域,叫Humongous區域。
如果一個對象佔用的空間超過了分區容量50%以上,G1收集器就認為這是一個巨型對象。
這些巨型對象,默認直接會被分配在老年代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。
為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。
4.2 Young GC
Young GC主要是對Eden區進行GC,它在Eden空間耗盡時會被觸發。
Eden空間的數據移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分數據會直接晉陞到年老代空間。
Survivor區的數據移動到新的Survivor區中,也有部分數據晉陞到老年代空間中。
最終Eden空間的數據為空,GC停止工作,應用執行緒繼續執行。

Remembered Set(已記憶集合)
在GC年輕代的對象時,我們如何找到年輕代中對象的根對象呢?
根對象可能是在年輕代中,也可以在老年代中,那麼老年代中的所有對象都是根么?
如果全量掃描老年代,那麼這樣掃描下來會耗費大量的時間。
於是,G1引進了RSet的概念。它的全稱是Remembered Set,其作用是跟蹤指向某個堆內的對象引用。

每個Region初始化時,會初始化一個RSet,該集合用來記錄並跟蹤其它Region指向該Region中對象的引用,每個Region默認按照512Kb劃分成多個Card,所以RSet需要記錄的東西應該是 xx Region的 xx Card。
4.3 Mixed GC
當越來越多的對象晉陞到老年代old region時,為了避免堆記憶體被耗盡,虛擬機會觸發一個混合的垃圾收集器,即Mixed GC,該演算法並不是一個Old GC,除了回收整個Young
Region,還會回收一部分的Old Region,這裡需要注意:是一部分老年代,而不是全部
老年代,可以選擇哪些old region進行收集,從而可以對垃圾回收的耗時時間進行控制。
也要注意的是Mixed GC 並不是 Full GC。
MixedGC什麼時候觸發?
由參數 -XX:InitiatingHeapOccupancyPercent=n 決定。默認:45%,該參數的意思是:當老年代大小占整個堆大小百分比達到該閥值時觸發。
它的GC步驟分2步:
①全局並發標記(global concurrent marking)
② 拷貝存活對象(evacuation)
1、全局並發標記
全局並發標記,執行過程分為五個步驟:
初始標記(initial mark,STW)
標記從根節點直接可達的對象,這個階段會執行一次年輕代GC,會產生全局停頓。
根區域掃描(root region scan)
G1 GC 在初始標記的存活區掃描對老年代的引用,並標記被引用的對象。
該階段與應用程式(非 STW)同時運行,並且只有完成該階段後,才能開始下
一次 STW 年輕代垃圾回收。
並發標記(Concurrent Marking)
G1 GC 在整個堆中查找可訪問的(存活的)對象。該階段與應用程式同時運行,可以被 STW 年輕代垃圾回收中斷。
重新標記(Remark,STW)
該階段是 STW 回收,因為程式在運行,針對上一次的標記進行修正。
清除垃圾(Cleanup,STW)
清點和重置標記狀態,該階段會STW,這個階段並不會實際上去做垃圾的收集,等待evacuation階段來回收。
2、拷貝存活對象
Evacuation階段是全暫停的。該階段把一部分Region里的活對象拷貝到另一部分Region中,從而實現垃圾的回收清理。
4.4 G1收集器相關參數
-XX:+UseG1GC:使用 G1 垃圾收集器
-XX:MaxGCPauseMillis:設置期望達到的最大GC停頓時間指標(JVM會儘力實現,但不保證達到),默認值是 200 毫秒。
-XX:G1HeapRegionSize=n:設置的 G1 區域的大小。值是 2 的冪,範圍是 1 MB 到 32 MB 之間。目標是根據最小的 Java 堆大小劃分出約 2048 個區域。默認是堆記憶體的1/2000。
-XX:ParallelGCThreads=n:設置 STW 工作執行緒數的值。將 n 的值設置為邏輯處理器的數量。n 的值與邏輯,處理器的數量相同,最多為 8。
-XX:ConcGCThreads=n:設置並行標記的執行緒數。將 n 設置為並行垃圾回收執行緒數 (ParallelGCThreads)的 1/4 左右。
-XX:InitiatingHeapOccupancyPercent=n:設置觸發標記周期的 Java 堆佔用率閾值。默認佔用率是整個 Java 堆的 45%。
4.5 測試
1、編寫測試程式碼
package com.zn; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.Random; public class GCTest { public static void main(String[] args) throws Exception { List<Object> list = new ArrayList<Object>(); while (true) { int sleep = new Random().nextInt(100); if (System.currentTimeMillis() % 2 == 0) { list.clear(); } else { for (int i = 0; i < 10000; i++) { Properties properties = new Properties(); properties.put("key_" + i, "value_" + System.currentTimeMillis() + i); list.add(properties); } // System.out.println("list大小為:" + list.size()); Thread.sleep(sleep); } } } 2、設置垃圾回收為G1 垃圾收集器
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xmx256m
3、啟動程式資訊列印

4.6 對於G1垃圾收集器優化建議
年輕代大小
避免使用 -Xmn 選項或 -XX:NewRatio 等其他相關選項顯式設置年輕代大小。
固定年輕代的大小會覆蓋暫停時間目標。
暫停時間目標不要太過嚴苛
G1 GC 的吞吐量目標是 90% 的應用程式時間和 10%的垃圾回收時間。
評估 G1 GC 的吞吐量時,暫停時間目標不要太嚴苛。目標太過嚴苛表示您願意承受更多的垃圾回收開銷,而這會直接影響到吞吐量


