天天看點

閑談JVM(二):淺析新老生代參數配置前言Heap區新老生代結語

文章目錄

  • 前言
  • Heap區新老生代
    • 新生代參數配置
      • NewSize
      • MaxNewSize
      • Xmn
      • NewRatio
      • SurvivorRatio
      • 新生代的GC
    • 老生代參數配置
      • 對象何時進入老生代?
      • 老生代GC
  • 結語

前言

在上一篇中,我們介紹了JVM中Heap區的基本參數設定,可以參見:閑談JVM(一):淺析JVM Heap參數配置

在Heap區中,又被劃分為幾個區域,分别為新生代與老生代,而我們知道,絕大多數的對象都是“朝生夕死”的,在新生代階段就會被回收掉,由此可以看出,新生代是非常重要的一個區域,而經過多次回收後,仍存活的對象,将進入老生代,本篇,我們就來了解一下新生代與老生代相關的參數配置。

Heap區新老生代

在最開始,我們還是來看一下JVM的記憶體模型:

閑談JVM(二):淺析新老生代參數配置前言Heap區新老生代結語

從上圖中可以看到,Heap區由新生代與老生代組成,而新生代的比重也是比較大的,而進一步細分,新生代中又可以分為三個區域:Eden、Survivor1(From)、Survivor2(To)。

新生代參數配置

新生代的大小配置,主要由幾個參數控制:Xmn、NewSize、MaxNewSize、NewRatio、SurvivorRatio。

我們逐個來分說明參數的作用。

NewSize

NewSize是設定新生代有效記憶體的初始化大小,也可以說是新生代有效記憶體的最小值,當新生代回收之後有效記憶體可能會進行縮容,這個參數就指定了能縮小到的最小值。

該參數的使用方式如下:

-XX:NewSize=20m
           

在Linux環境下,JDK8中該參數的預設大小即為20M。

需要注意的是,該參數僅當Xms與Xmx不一緻時,才會在JVM初始化時配置設定這麼大的記憶體,當Xms與Xmx設定為同一個值時,該參數無效,初始化的新生代大小将會使用MaxNewSize配置的大小。

MaxNewSize

MaxNewSize顧名思義,就是設定新生代有效記憶體的最大值,當對新生代進行回收之後可能會對新生代的有效記憶體進行擴容,那到底能擴容到多大,這就是最大值。

NewSize與MaxNewSize是一組參數,分别對應新生代有效記憶體的最小值與最大值。

該參數的使用方式如下:

-XX:MaxNewSize=100m
           

在Linux環境下,JDK8中該參數的預設大小為318.5M。

Xmn

Xmn同樣是設定新生代的大小,等同于同時設定了NewSize和MaxNewSize,并且值都相等,例如

-Xmn128m
           

等同于

-XX:NewSize=128m 
-XX:MaxNewSize=128m
           

在絕大多數場景下,對象都是朝生暮死的,在新生代階段就會被回收掉,在高并發的場景下,會高頻建立大量的對象在新生代中,對于這種場景下,可以合理分析堆區記憶體的情況,适當的調大新生代的大小,避免新生代迅速堆滿頻繁觸發YGC,也可以一定程度的避免對象快速進入老生代。

NewRatio

NewRatio參數表示目前老生代可用記憶體/目前新生代可用記憶體的比值,即Old/New,在JDK8中,預設是2,參數使用的方式如下:

-XX:NewRatio=2
           

我們來驗證一下是否是這樣的。

寫一個簡單的Demo:

public class HelloWorld {
	public static void main(String[] args) {
		try {
			Thread.sleep(1000 * 60);
		} catch(Exception e) {
			System.out.println("Error");
		}
		System.out.println("hello world");
	}
}
           

我們設定堆區的大小為30M:

閑談JVM(二):淺析新老生代參數配置前言Heap區新老生代結語

然後來看一下堆區的配置設定情況:

閑談JVM(二):淺析新老生代參數配置前言Heap區新老生代結語

可以看到,新生代與老生代的比值的确為2,不過這是當JVM初始化時,堆區的一個比例,當運作一段時間,發生GC之後,新生代與老生代的比例不一定遵守這個比例,而是進行動态計算的。

同時需要注意的是,如果設定了Xmn或者NewSize/MaxNewSize,JVM初始化時,那麼NewRatio将會被覆寫,即不會生效。

我們來驗證一下這個說法是否成立。

閑談JVM(二):淺析新老生代參數配置前言Heap區新老生代結語

啟動測試代碼,設定堆區大小為30M,新生代大小為20M,然後我們在來看一下記憶體分布的情況:

閑談JVM(二):淺析新老生代參數配置前言Heap區新老生代結語

可以看到,新生代大小為20M,老生代為10M,也就是說,NewRatio的預設值并未生效。

SurvivorRatio

新生代由Eden和兩塊Survivor組成,這兩塊Survivor通常一個叫做From Space,一個叫做To Space,并且兩個大小一緻,每次GC發生的時候,會将Eden和From Space裡的可達對象往To Space裡拷貝,或者晉升到Old。

GC完成之後正常情況下是Eden為空的,并且會對換下From Space和To Space的位置,對換完之後的To Space又為空了。

SurvivorRatio表示新生代中三個分代,Eden、Survivor1(From)、Survivor2(To)的比值。

該參數的使用方式如下:

-XX:SurvivorRatio=8
           

在Linux環境下,JDK8中該參數的預設大小即為8,表示Eden:From:To的比值為8:1:1。

我們來驗證一下:

還是啟動Demo程式,然後檢視:

閑談JVM(二):淺析新老生代參數配置前言Heap區新老生代結語

在看一下真正的記憶體分布情況:

我們設定JVM參數:

java -Xms30M -Xmx30M -Xmn12M -XX:SurvivorRatio=1 HelloWorld
           

這裡我們将SurvivorRatio設定為1,即Eden:From:To的比值為1:1:1。

使用指令檢視記憶體的實際分布情況:

jstat -gc PID 1000 3
           

輸出結果:

S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
4096.0 4096.0  0.0    0.0    4096.0   819.9    18432.0      0.0     2240.0  1.2    0.0    0.0        0    0.000   0      0.000    0.000
4096.0 4096.0  0.0    0.0    4096.0   819.9    18432.0      0.0     2240.0  1.2    0.0    0.0        0    0.000   0      0.000    0.000
4096.0 4096.0  0.0    0.0    4096.0   819.9    18432.0      0.0     2240.0  1.2    0.0    0.0        0    0.000   0      0.000    0.000
           

其中幾個關鍵的參數:

  • S0C survivor0大小
  • S1C survivor1大小
  • EC Eden區大小

我們可以看到,三個區域的大小均為4096K,即4M,驗證了SurvivorRatio參數。

新生代的GC

新生代的GC垃圾回收器,主要有三種,這裡簡單說明一下:

1、Serial 收集器,它是一個單線程的收集器,但它的單線程的意義并不僅僅說明它隻會是使用一個 CPU 或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。

2、ParNew 收集器,ParNew 收集器其實就是 Serial 收集器的多線程版本。ParNew 最重要的一點,是唯一的可以與老生代的CMS 收集器配合使用的新生代收集器,可以通過參數

-XX:+UseParNewGC

進行指定。

3、Parallel Scavenge 收集器,它與其他收集器的不同之處在于:它的關注點與其他收集器不同。CMS 等收集器的關注點是盡可能地縮短垃圾收集時使用者線程的停頓時間,而 Parallel Scavenge 收集器的目标則是達到一個可控制的吞吐量( Throughput)。

在JDK8中,生産環境中較為常見的組合是新生代的 ParNew GC + 老生代的CMS GC,這套組合也是比較推薦的。

老生代參數配置

上面我們了解完了Heap區中新生代的主要參數配置,那麼下面聊一下老生代的配置,關于老生代的參數配置,并不多,老生代的大小 = 堆區大小 - 新生代大小,我們依舊是通過實際情況,驗證我們的說法。

啟動測試程式,設定堆區大小30M,新生代大小10M,驗證老生代大小是否為20M:

java -Xms30M -Xmx30M -Xmn10M HelloWorld
           

列印記憶體分布:

S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
1024.0 1024.0  0.0    0.0    8192.0   820.1    20480.0      0.0     2240.0  1.2    0.0    0.0        0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   820.1    20480.0      0.0     2240.0  1.2    0.0    0.0        0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   820.1    20480.0      0.0     2240.0  1.2    0.0    0.0        0    0.000   0      0.000    0.000
           

其中OC一項,為老生代目前大小,為20480K,即20M,驗證了我們的說法。

對象何時進入老生代?

我們都知道,對象剛被建立時,一般情況下是會被建立在新生代的,隻有超過指定門檻值的大對象,才會被直接建立在老生代當中,大多數的對象的生命周期僅存在于新生代,會在新生代階段就被回收掉了,但是仍有部分對象會進入到老生代中區,那麼,新生代的對象何時會進入老生代?

進入到老生代的時機,可以通過參數進行控制:

-XX:MaxTenuringThreshold=15
           
Each object in Java heap has a header which is used by Garbage Collection (GC) algorithm. The young space collector (which is responsible for object promotion) uses a few bit(s) from this header to track the number of collections object that have survived (32-bit JVM use 4 bits for this, 64-bit probably some more).

對象從新生代晉升到老年代的年齡門檻值(每次 Young GC 留下來的對象年齡加一),預設值15,表示對象要經過15次 GC 才能從新生代晉升到老年代。

但是在老生代的CMS GC下,該預設值為6,我們來驗證一下,設定GC算法為CMS:

java -Xms30M -Xmx30M -Xmn10M -XX:+UseConcMarkSweepGC HelloWorld
           

通過jinfo檢視:

C:\Users\sheqian.xgy>jinfo -flag MaxTenuringThreshold 12768
-XX:MaxTenuringThreshold=6
           

可以看到,CMS GC下,晉升的次數預設為6。

不過需要注意的是,當設定了這個值得時候,第一次會以它為準,而在運作階段,該門檻值是動态調整,不過不會超過這個值。

老生代GC

老生代的垃圾收集器,主要分為四種,分别如下:

1、Serial Old 收集器,Serial Old 是 Serial 收集器的老年代版本,它同樣是一個單線程收集器,使用 “标記-整理” 算法。一般情況下,不會使用。

2、Parallel old 收集器,Parallel Scavenge 收集器的老年代版本,使用多線程和 “标記-整理” 算法。

3、CMS 收集器,以擷取最短回收停頓時間為目标,目前較為推薦的GC 收集器,多數應用于網際網路站或者B/S系統的伺服器端上。

4、G1 收集器,Java 9以後的預設收集器,目前最炙手可熱的GC 收集器,可以說兼顧了性能與時間的GC 收集器。

在Java8中,預設的GC收集器采用了Parallel GC,也可以通過參數

-XX:+UseParallelGC

進行指定,來看一下Oracle官方的說法:

The parallel collector (also referred to here as the throughput collector) is a generational collector similar to the serial collector; the primary difference is that multiple threads are used to speed up garbage collection. The parallel collector is enabled with the command-line option

-XX:+UseParallelGC

. By default, with this option, both minor and major collections are executed in parallel to further reduce garbage collection overhead.

The parallel collector is selected by default on server-class machines. In addition, the parallel collector uses a method of automatic tuning that allows you to specify specific behaviors instead of generation sizes and other low-level tuning details. You can specify maximum garbage collection pause time, throughput, and footprint (heap size).

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html

關于GC 收集器的内容,本篇先不做過多詳細的介紹,在後面的篇幅中,會着重對比各個GC 收集器。

結語

本篇,我們介紹了Heap區中新生代與老生代的參數配置,了解了新老生代較為常用的參數配置,其中新生代的參數控制是較為重要的,因為大多數對象在這個階段就會被回收掉,新生代的大小如果設定過大,會增大YGC的壓力,設定過小則會頻繁堆滿,觸發YGC,是以需要根據線上的業務實際情況,酌情調整。

下一篇,我們會對Java8中新增的本地元空間(metaSpace)的參數配置進行介紹,敬請期待。

本篇參考:

JVM 學習——垃圾收集器與記憶體配置設定政策:

http://matt33.com/2016/09/18/jvm-basic2/#Parallel-Scavenge-%E6%94%B6%E9%9B%86%E5%99%A8

Oralce官方文檔 Parallel Collector:

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html

JVM監控和調優常用指令工具總結

https://www.cnblogs.com/wxisme/p/9878494.html