JVM-GC-串行回收器-SerialGC實戰

Java GC發展至今,已經推出了好幾代收集器,包括Serial、ParNew、Parallel、CMS、G1以及Java11中最新的ZGC。每一代GC都對前一代存在的問題做出了很大的改善。

今天介紹一個古董收集器-Serial串行GC。

雖然此收集器的使用場景已經不多,但本文通過這個收集器,說明了如何分配每一塊堆內存的大小,並根據GC日誌,詳細說明了Serial GC在新生代和老年代的GC過程。

Serial GC的名字能很好地概括他的特點:串行。它與應用線程的執行是串行的,也就是說,執行應用線程的時候,不會執行GC,執行GC的時候,不能執行應用線程。

所以,整個Java進程執行起來就行下面的樣子:

JVM-GC-串行回收器-SerialGC實戰

Serial GC特點

Serial GC使用的是分代算法,在新生代上,Serial使用複製算法進行收集,在老年代上,Serial使用標記-壓縮算法進行收集。

分代算法、複製算法、標記-壓縮請移步:

1.Serial存在的問題

如上圖所示,在需要執行GC時,GC線程會阻塞所有用戶線程(Stop-The-world,簡稱STW),等他執行完,才會恢復用戶線程。

這對我們的應用程序來說,每次GC是都會造成不同程度的卡頓,對用戶是極為不友好的。

2.使用場景

個人觀點:

首先,根據其特點,回收算法簡單,所以回收效率高。

其次,它是單線程收集的,不存在GC線程之間的切換。由於Java的線程切換是需要系統內核來調度的,在單線程下,可以很大程度的減少調度帶來的系統開銷。

所以,也許在單核CPU機器上,且業務場景為只對公司內部使用且可以忍受STW帶來的卡頓的情況下,有一些用武之地。

3.實戰

環境:

  • CPU:i7 4核
  • 內存:16G
  • JDK version:8

3.1 先來看一下默認情況下,使用的哪個GC

添加下面JVM參數並運行代碼,觀察GC日誌

/**
* JVM參數:
* -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
*/
public static void main(String[] args) {
System.out.println("Hello SerialGC");
}

程序輸出如下

Hello SerialGC
// 下面是GC日誌
Heap
PSYoungGen total 76288K, used 6554K [0x000000076b180000, 0x0000000770680000, 0x00000007c0000000)
eden space 65536K, 10% used [0x000000076b180000,0x000000076b7e6930,0x000000076f180000)
from space 10752K, 0% used [0x000000076fc00000,0x000000076fc00000,0x0000000770680000)
to space 10752K, 0% used [0x000000076f180000,0x000000076f180000,0x000000076fc00000)
ParOldGen total 175104K, used 0K [0x00000006c1400000, 0x00000006cbf00000, 0x000000076b180000)
object space 175104K, 0% used [0x00000006c1400000,0x00000006c1400000,0x00000006cbf00000)
Metaspace used 3458K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 381K, capacity 388K, committed 512K, reserved 1048576K

PSYoungGen:表示年輕代使用的是ParallelGC

ParOldGen:表示老年代使用的是ParallelGC

Metaspace:元數據區使用情況

可見,在多核情況下,JVM默認選用了支持多線程併發的ParallelGC。

3.2 Serial GC是運行在Client模式下的默認收集器?

周志明老師的書中提到過,SerialGC仍然是-client模式下默認的收集器。

下面來實驗一下,剛才的JVM啟動參數加上-client參數

/**
* JVM參數:
* -client -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
*/
public static void main(String[] args) {
System.out.println("Hello SerialGC");
}

運行結果如下:

Hello SerialGC in client mode
Heap
PSYoungGen total 76288K, used 6554K [0x000000076b180000, 0x0000000770680000, 0x00000007c0000000)
eden space 65536K, 10% used [0x000000076b180000,0x000000076b7e6930,0x000000076f180000)
from space 10752K, 0% used [0x000000076fc00000,0x000000076fc00000,0x0000000770680000)
to space 10752K, 0% used [0x000000076f180000,0x000000076f180000,0x000000076fc00000)
ParOldGen total 175104K, used 0K [0x00000006c1400000, 0x00000006cbf00000, 0x000000076b180000)
object space 175104K, 0% used [0x00000006c1400000,0x00000006c1400000,0x00000006cbf00000)
Metaspace used 3513K, capacity 4498K, committed 4864K, reserved 1056768K
class space used 387K, capacity 390K, committed 512K, reserved 1048576K


可見依然是ParallelGC。

這個原因應該是由於,在JDK1.8下,-client和-server參數默認都是失效的,所以指定-client也無濟於事。

其實筆者也在相同的環境下嘗試了JDK6和JDK7,也同樣不是SerialGC,所以猜想可能是老版本的單核CPU情況下,JVM會默認選擇SerialGC,但這一點筆者尚未查證。

PS:-client和-server

-client和-server參數在之前版本的JDK中是用來選擇JVM運行過程中使用的編譯器的。對啟動性能有要求的程序,可使用-client,對應的編譯器為編譯效率較快C1,對峰值性能有要求的程序,可使用-server,對應生成代碼執行效率較快的C2(參考了鄭雨迪老師在極客時間推出的課程)。

Java8會默認使用分層編譯的機制,會自動選擇在何時使用哪個編譯器,所以client和server參數在默認情況下失效。相對之前的JDK版本,JDK8的這種機制很大程度地提升了代碼的編譯執行效率。

3.3 Serial GC實戰 - JVM參數

本小節說明了如何配置堆內存中每一塊內存的大小。

首先我們要明確需要指定哪幾塊內存。因為Serial GC是分代收集,所以要確認新生代和老年代的大小,其中,新生代又需要確認Eden區和Survivor區的大小。

(1)定義整個堆內存的大小

// -Xmx:最大堆內存,-Xms:最小堆內存,這裡設置為一樣的,表示堆內存固定200M
-Xmx200M -Xms200M

(2)定義新生代和老年代的大小

// NewRatio表示老年代和新生代的比例,3表示3:1
// 即把整個堆內存分為4份,老年代佔3份,新生代1份
// 目前堆內存為200M,NewRatio=3時,新生代=50M,老年代=150M
-XX:NewRatio=3

(3)定義Eden區和Survivor區的大小

// SurvivorRatio表示Eden區和兩個Survivor區的比例,3表示3:2(注意是兩個Survivor區)
// 即把新生代分為5份,Eden佔3份,Survivor區佔2份
// 目前新生代為50M,Survivor=3時,Eden=30M,Survivor=20M(from=10M, to=10M)
-XX:SurvivorRatio=3

(4)配置GC日誌打印參數

// -XX:+UseSerialGC:顯示指定使用Serial GC
// -XX:+PrintGCDetails:打印GC詳細日誌
// -XX:+PrintGCTimeStamps:打印GC發生的時間
-XX:+UseSerialGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

(5)實踐

JVM-GC-串行回收器-SerialGC實戰

JVM啟動參數

依然用上面的Hello SerialGC程序,運行結果如下

Hello SerialGC
Heap
// def new generation表明新生代使用SerialGC,total:40M,已使用:4302K
// total少了10M?這是因為新生代使用複製算法,From區和to區實際上每次只能使用1個,所以是eden的30M + from或to的10M = 40M
def new generation total 40960K, used 4302K [0x00000000f3800000, 0x00000000f6a00000, 0x00000000f6a00000)
// eden區30M
eden space 30720K, 14% used [0x00000000f3800000, 0x00000000f3c33b78, 0x00000000f5600000)
// from區10M
from space 10240K, 0% used [0x00000000f5600000, 0x00000000f5600000, 0x00000000f6000000)
// to區10M
to space 10240K, 0% used [0x00000000f6000000, 0x00000000f6000000, 0x00000000f6a00000)
// 老年代使用 SerialGC ,總大小150M,已使用0K
tenured generation total 153600K, used 0K [0x00000000f6a00000, 0x0000000100000000, 0x0000000100000000)
the space 153600K, 0% used [0x00000000f6a00000, 0x00000000f6a00000, 0x00000000f6a00200, 0x0000000100000000)
// 元數據區大小,暫不關注
Metaspace used 3450K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 380K, capacity 388K, committed 512K, reserved 1048576K

關於複製算法,請移步:

3.4 Serial GC實戰 - 通過GC日誌理解新生代老年代的GC過程

此實驗在上述JVM參數配置條件下運行。

下面通過一個實例程序,來觀察一下

public class SerialGCDemo {
/**
* 堆內存:-Xmx200M -Xms200M
* 新生代:-XX:NewRatio=3 -XX:SurvivorRatio=3
* GC參數:-XX:+UseSerialGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
* 堆空間:200M,新生代:50M,老年代:150M,新生代eden區:30M,新生代from區:10M,新生代to區:10M
* -Xmx200M -Xms200M -XX:NewRatio=3 -XX:SurvivorRatio=3 -XX:+UseSerialGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
* @param args
*/
public static void main(String[] args) {
byte[][] useMemory = new byte[1000][];
Random random = new Random();
for (int i = 0; i < useMemory.length; i++) {
useMemory[i] = new byte[1024 * 1024 * 10]; // 創建10M的對象
// 20%的概率將創建出來的對象變為可回收對象
if (random.nextInt(100) < 20) {
System.out.println("created byte[] and set to null: " + i);
useMemory[i] = null;
} else {
System.out.println("created byte[]: " + i);
}
}
}
}

整體日誌輸入如下:

created byte[]: 0
created byte[]: 1
0.236: [GC (Allocation Failure) 0.236: [DefNew: 24807K->870K(40960K), 0.0132148 secs] 24807K->21350K(194560K), 0.0132618 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
created byte[]: 2
created byte[] and set to null: 3
0.252: [GC (Allocation Failure) 0.252: [DefNew: 21941K->717K(40960K), 0.0060942 secs] 42421K->31437K(194560K), 0.0061231 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
created byte[]: 4
created byte[]: 5
0.259: [GC (Allocation Failure) 0.259: [DefNew: 22408K->717K(40960K), 0.0114560 secs] 53128K->51917K(194560K), 0.0114856 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
created byte[]: 6
created byte[]: 7
0.285: [GC (Allocation Failure) 0.285: [DefNew: 21788K->717K(40960K), 0.0122524 secs] 72988K->72397K(194560K), 0.0122868 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
created byte[]: 8
created byte[]: 9
0.299: [GC (Allocation Failure) 0.299: [DefNew: 21790K->717K(40960K), 0.0115042 secs] 93470K->92877K(194560K), 0.0115397 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]

created byte[]: 10
created byte[]: 11
0.312: [GC (Allocation Failure) 0.312: [DefNew: 21791K->717K(40960K), 0.0120174 secs] 113952K->113357K(194560K), 0.0120525 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
created byte[]: 12
created byte[]: 13
0.328: [GC (Allocation Failure) 0.328: [DefNew: 21792K->717K(40960K), 0.0162437 secs] 134432K->133837K(194560K), 0.0162844 secs] [Times: user=0.00 sys=0.01, real=0.02 secs]
created byte[]: 14
created byte[]: 15
0.347: [GC (Allocation Failure) 0.347: [DefNew: 21793K->21793K(40960K), 0.0000201 secs]0.347: [Tenured: 133120K->143360K(153600K), 0.0103885 secs] 154913K->154316K(194560K), [Metaspace: 3350K->3350K(1056768K)], 0.0104608 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
Exception in thread "main" created byte[]: 16
0.361: [Full GC (Allocation Failure) 0.361: [Tenured: 143360K->143360K(153600K), 0.0028089 secs] 165153K->164556K(194560K), [Metaspace: 3350K->3350K(1056768K)], 0.0028543 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.364: [Full GC (Allocation Failure) 0.364: [Tenured: 143360K->143360K(153600K), 0.0050038 secs] 164556K->164538K(194560K), [Metaspace: 3350K->3350K(1056768K)], 0.0050390 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Disconnected from the target VM, address: '127.0.0.1:57881', transport: 'socket'
java.lang.OutOfMemoryError: Java heap space
Heap
at com.example.demo.gcdemo.SerialGCDemo.main(SerialGCDemo.java:28)
def new generation total 40960K, used 22281K [0x00000000f3800000, 0x00000000f6a00000, 0x00000000f6a00000)
eden space 30720K, 72% used [0x00000000f3800000, 0x00000000f4dc27c0, 0x00000000f5600000)
from space 10240K, 0% used [0x00000000f6000000, 0x00000000f6000000, 0x00000000f6a00000)
to space 10240K, 0% used [0x00000000f5600000, 0x00000000f5600000, 0x00000000f6000000)
tenured generation total 153600K, used 143360K [0x00000000f6a00000, 0x0000000100000000, 0x0000000100000000)
the space 153600K, 93% used [0x00000000f6a00000, 0x00000000ff6000e0, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3381K, capacity 4568K, committed 4864K, reserved 1056768K
class space used 364K, capacity 392K, committed 512K, reserved 1048576K

日誌說明:

0.236: [GC (Allocation Failure) 0.236: [DefNew: 24807K->870K(40960K), 0.0132148 secs] 24807K->21350K(194560K), 0.0132618 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
  • 0.236:GC發生的時間(秒),從程序啟動開始計算
  • [GC:GC類型,另外還有Full GC,GC不會造成STW,Full GC會。
  • (Allocation Failure):GC原因,申請內存失敗
  • [DefNew:說明新生代用Serail GC回收,即default new generation之意。
  • 24087K -> 870K(40960K):GC前該區域內存已使用容量 -> GC後該區域內存已使用容量(該區域內存總容量)
  • 0.0132148 secs:該內存區域GC所佔用的時間(秒)
  • 24807K->21350K(194560K):GC前堆內存已使用容量 -> GC後堆內存已使用容量(堆內存總容量:190M,這裡要減去from或to的10M)
  • 0.0132618 secs:本次回收整體佔用時間(秒)
  • [Times: user=0.02 sys=0.00, real=0.01 secs]:佔用時間具體數據。user:用戶態消耗的CPU時間,sys:內核態消耗的CPU時間,real:從操作開始到操作結束所經歷的牆鍾時間。
0.361: [Full GC (Allocation Failure) 0.361: [Tenured: 143360K->143360K(153600K), 0.0028089 secs] 165153K->164556K(194560K), [Metaspace: 3350K->3350K(1056768K)], 0.0028543 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

這裡只說明一下與上面有區別的地方

  • [Full GC:GC類型,會造成STW
  • [Tenured:老年代回收
  • 143360K->143360K(153600K):老年代GC前已使用內存容量 -> 老年代GC後已使用內存容量(老年代總容量)
  • 165153K->164556K(194560K):堆內存GC前已使用內存容量 -> 堆內存GC後已使用內存容量(堆內存總容量)
  • Metaspace:元數據區內存回收情況

下面分步驟詳細看一下從程序開始到結束,對內存的變化過程

整個內存初始狀態如下:

JVM-GC-串行回收器-SerialGC實戰

created byte[]: 0
created byte[]: 1
0.236: [GC (Allocation Failure) 0.236: [DefNew: 24807K->870K(40960K), 0.0132148 secs] 24807K->21350K(194560K), 0.0132618 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

創建了兩個10M的對象(記為ID:0,ID:1),並且沒有設置成可回收對象,由於Eden區目前最起碼還有一個Random對象,所以在給第三個對象申請內存時,發現Eden區內存不足,觸發了GC。

JVM-GC-串行回收器-SerialGC實戰

新生代在GC後變為870K,說明Random對象被複制到from區,而兩個10M的對象都直接晉升到了老年代。

JVM-GC-串行回收器-SerialGC實戰

created byte[]: 2
created byte[] and set to null: 3
0.252: [GC (Allocation Failure) 0.252: [DefNew: 21941K->717K(40960K), 0.0060942 secs] 42421K->31437K(194560K), 0.0061231 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

創建了ID:2和ID:3對象,並把ID:3設置為可回收對象

JVM-GC-串行回收器-SerialGC實戰

GC會將Eden區的對象和from區的對象嘗試複製到to區,ID:3對象直接回收(通過堆空間的容量變化可以看出:42421K->31437K),ID:2對象在to區中放不下,晉升老年代

JVM-GC-串行回收器-SerialGC實戰

一直到創建ID:12,ID:13,都與上述過程類似,並且沒有產生過垃圾對象,但創建完ID:13對象後,老年代的已使用內存達到了130M+,如下:

created byte[]: 12
created byte[]: 13
0.328: [GC (Allocation Failure) 0.328: [DefNew: 21792K->717K(40960K), 0.0162437 secs] 134432K->133837K(194560K), 0.0162844 secs] [Times: user=0.00 sys=0.01, real=0.02 secs]

再創建ID:14,ID:15對象後,又需要新生代GC

created byte[]: 14
created byte[]: 15
0.347: [GC (Allocation Failure) 0.347: [DefNew: 21793K->21793K(40960K), 0.0000201 secs]0.347: [Tenured: 133120K->143360K(153600K), 0.0103885 secs] 154913K->154316K(194560K), [Metaspace: 3350K->3350K(1056768K)], 0.0104608 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

GC前如下所示

JVM-GC-串行回收器-SerialGC實戰

在新生代GC時,要把ID:14,ID:15的對象複製到老年代,但此時老年代已經不足以容納這兩個對象,此時會觸發老年代的GC。

即日誌中的Tenured部分。但發現沒有任何對象可以回收,然後嘗試複製了Eden區的一個對象到老年代

JVM-GC-串行回收器-SerialGC實戰

然後繼續創建對象,會繼續嘗試Full GC,Full GC無果,最終發生內存溢出。

Exception in thread "main" created byte[]: 16
0.361: [Full GC (Allocation Failure) 0.361: [Tenured: 143360K->143360K(153600K), 0.0028089 secs] 165153K->164556K(194560K), [Metaspace: 3350K->3350K(1056768K)], 0.0028543 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.364: [Full GC (Allocation Failure) 0.364: [Tenured: 143360K->143360K(153600K), 0.0050038 secs] 164556K->164538K(194560K), [Metaspace: 3350K->3350K(1056768K)], 0.0050390 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Disconnected from the target VM, address: '127.0.0.1:57881', transport: 'socket'
java.lang.OutOfMemoryError: Java heap space
Heap
at com.example.demo.gcdemo.SerialGCDemo.main(SerialGCDemo.java:28)

4 總結

首先介紹了Serial的特點以及存在的問題,SerialGC是串行收集器,在收集時會產生STW,停頓時間較長導致用戶體驗差。

然後通過實戰,介紹瞭如何指定JVM的每一塊堆內存。

最後通過一個案例,詳細描述了SerialGC的整個過程以及內存變化。


分享到:


相關文章: