天天看點

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例

  • 一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化
    • 1.1 系統背景
    • 1.2 系統核心業務流程
    • 1.3 系統的高峰運作壓力
    • 1.4 基于G1垃圾回收器的優化
      • 1.4.1 G1垃圾回收器的預設記憶體布局
      • 1.4.2 GC停頓時間的設定
      • 1.4.3 到底多長時間會觸發新生代GC?
      • 1.4.4 新生代gc如何優化?
      • 1.4.5 mixed gc如何優化?
  • 二、每秒10萬并發的BI系統的優化
    • 2.1 案例背景
    • 2.2 技術痛點
    • 2.3 頻繁Young GC 影響不大
    • 2.4 優化方式一:提升機器配置,運用大記憶體機器
    • 2.5 使用G1 優化
  • 三、模拟垃圾回收的場景
    • 3.1 YoungGC 的示範
      • 3.1.1示例代碼
      • 3.1.2 程式的JVM參數示範
      • 3.1.3 列印JVM GC日志
      • 3.1.4 執行代碼時的記憶體分析
      • 3.1.5 gc日志結果詳解
    • 3.2 模拟對象從新生代進入老年代的場景
      • 3.2.1 動态年齡判定規則
      • 3.2.2 Young GC過後存活對象放不下Survivor區域,直接進入老年代
      • 3.2.3 對象達到15歲年齡之後自然進入老年代的場景
      • 3.2.4 大對象直接進入老年代
    • 3.3 老年代 FullGC 的示範
  • 四、JVM 優化思路總結

一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化

1.1 系統背景

百萬級注冊使用者的線上教育平台,主要目标使用者群體是幾歲到十幾歲的孩子,注冊使用者大概是幾百萬的規模,日活使用者規模大概在幾十萬。

系統的業務流程其實也不複雜,我們可以排除掉一些選課、排課、浏覽課程詳情以及付費購買之類的低頻的行為。

這樣的一個平台,使用人群是幼稚園的孩子到中國小的孩子。他們平時白天都要上學,一般也就是晚上放學之後到八九點鐘的樣子,是最活躍使用這個平台的時候,還有就是周末也是最活躍使用這個平台的時候。

這裡尤為關鍵的需要注意的,就是每天晚上那兩三小時的高峰時期,幾乎可以認為每天幾十萬日活使用者都會集中在這個時間段來平台上上線上課程。是以這個晚上兩三小時的時間段裡,将會是平台每天絕對的高峰期,而且白天幾乎沒什麼流量,可能99%的流量都集中在晚上。

1.2 系統核心業務流程

這樣的一個系統,在上課的時候主要高頻使用主打的是互動環節。這個遊戲互動功能,一定會承載使用者高頻率、大量的互動點選

比如在完成什麼任務的時候必須要點選很多的按鈕,頻繁的進行互動,然後系統背景需要接收大量的互動請求,并且記錄下來使用者的互 動過程和互動結果。

1.3 系統的高峰運作壓力

核心點就是搞明白在晚上兩三小時高峰期内,每秒鐘會有多少請求,每個請求會連帶産生多少對象,占用多少記憶體,每個請求要處 理多長時間。

大緻估算一下,比如說晚上3小時高峰期内有總共60萬活躍使用者,平均每個使用者大概會使用1小時左右來上課,那麼每小時大概會有20萬活躍使用者同時線上學習。這20萬活躍使用者因為需要進行大量的互動操作,是以大緻可以認為是每分鐘進行1次互動操作,一小時内會進行60次互動操作。

那麼20萬使用者在1小時内會進行1200萬次互動操作,平均到每秒鐘大概是3000次左右的互動操作,這是一個很合理的數字。

那麼每秒鐘要承載3000并發請求,根據經驗來看,一般系統的核心服務需要部署5台4核8G的機器來抗住是差不多的,每台機器每秒鐘抗個600請求,這個壓力可以接受,一般不會導緻當機的問題。

那麼每個請求會産生多少個對象呢?

一次互動請求不會有太複雜的對象,他主要是記錄一些使用者互動過程的,可能會跟一些積分之類的東西有關聯。

所有大緻估算一下,一次互動請求大緻會連帶建立幾個對象,占據幾KB的記憶體,比如我們就認為是5KB吧那麼一秒600請求會占用3MB 。

1.4 基于G1垃圾回收器的優化

1.4.1 G1垃圾回收器的預設記憶體布局

采用4核8G的機器來部署系統,每台機器每秒會有600個請求會 占用3MB左右的記憶體空間。

那麼假設我們對機器上的JVM,配置設定4G給堆記憶體,其中新生代預設初始占比為5%,最大占比為60%,每個Java線程的棧記憶體為1MB, 中繼資料區域(永久代)的記憶體為256M,此時JVM參數如下:

“-Xms4096M -Xmx4096M  -Xss1M  -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseG1GC“
           

-XX:G1NewSizePercent

”參數是用來設定新生代初始占比的,不用設定,維持預設值為5%即可。

-XX:G1MaxNewSizePercent

”參數是用來設定新生代最大占比的,也不用設定,維持預設值為60%即可。

此時堆記憶體共4G,那麼此時會除以2048,計算出每個Region的大小,此時每個Region的大小就是2MB,剛開始新生代就占5%的Region,可以認為新生代就是隻有100個Region,有200MB的記憶體空間,如下圖所示。

1.4.2 GC停頓時間的設定

-XX:MaxGCPauseMills

”,他的預設值是200 毫秒

也就是說咱們希望每次觸發一次GC的時候導緻的系統停頓時間(也就是“Stop the World”)不要超過200毫秒,避免系統因為GC長時間卡死。

1.4.3 到底多長時間會觸發新生代GC?

系統運作起來之後,會不停的在新生代的Eden區域内配置設定對象,按照之前的推算是每秒配置設定3MB的對象。

G1裡是動态靈活的,他會根據你設定的gc停頓時間給你的新生代不停 配置設定更多Region,然後到一定程度,感覺差不多了,就會觸發新生代gc,保證新生代gc的時候導緻的系統停頓時間在你預設範圍内。G1它本身是這樣的一個運作原理,他會根據你預設的gc停頓時間,給新生代配置設定一些 Region,然後到一定程度就觸發gc,并且把gc時間控制在預設範圍内,盡量避免一次性回收過多的Region導緻gc停 頓時間超出預期。

G1到底會配置設定多少個Region給新生代,多久觸發一次新生代gc,每次耗費多 長時間,這些都是不确定的,必須通過一些工具去檢視系統實際情況才知道,這個提前是無法預知的。

1.4.4 新生代gc如何優化?

對于G1而言,我們首先應該給整個JVM的堆區域足夠的記憶體,比如我們在這裡就給了JVM超過5G的記憶體,其中堆記憶體有4G的記憶體。

接着就應該合理設定“

-XX:MaxGCPauseMills

”參數

如果這個參數設定的小了,那麼說明每次gc停頓時間可能特别短,此時G1一旦發現你對幾十個Region占滿了就立即觸發新生代gc,然後gc頻率特别頻繁,雖然每次gc時間很短。

如果這個參數設定大了,那麼可能G1會允許你不停的在新生代理配置設定新的對象,然後積累了很多對象了,再一次性回收幾百個Region

此時可能一次GC停頓時間就會達到幾百毫秒,但是GC的頻率很低。比如說30分鐘才觸發一次新生代GC,但是每次停頓500毫秒。

這個參數到底如何設定,需要結合後續給大家講解的系統壓測工具、gc日志、記憶體分析工具結合起來進行考慮, 盡量讓系統的gc頻率别太高,同時每次gc停頓時間也别太長,達到一個理想的合理值。

1.4.5 mixed gc如何優化?

老年代在堆記憶體裡占比超過45%就會觸發。核心的點,在于“

-XX:MaxGCPauseMills

”這個參數。

假設“

-XX:MaxGCPauseMills

”參數設定的值很大,導緻系統運作很久,新生代可能都占用了堆記憶體的60%了,此時才觸發新生代gc。

那麼存活下來的對象可能就會很多,此時就會導緻Survivor區域放不下那麼多的對象,就會進入老年代中。或者是你新生代gc過後,存活下來的對象過多,導緻進入Survivor區域後觸發了動态年齡判定規則,達到了Survivor 區域的50%,也會快速導緻一些對象進入老年代中。

是以這裡核心還是在于調節“-XX:MaxGCPauseMills”這個參數的值,在保證他的新生代gc别太頻繁的同時,還得考慮每次gc過後的存活對象有多少,避免存活對象太多快速進入老年代,頻繁觸發mixed gc。至于到底如何優化這個參數,都要結合後續大量工具的使用。

二、每秒10萬并發的BI系統的優化

2.1 案例背景

所謂BI系統,有數十萬甚至上百萬的商家在你的平台上做生意,會使用這個平台系統。此時一定會産生大量的資料,然後基于這些資料我們需要為商家提供一些資料報表,比如:每個商家每天有多少訪客?有多少交易?付費轉化率是多少?簡單來說,就是把一些商家平時日常經營的資料收集起來進行分析,然後把各種資料報表展示給商家的一套系 統。

這樣的一個BI系統,大緻的運作邏輯如下所示,首先從我們提供給商家日常使用的一個平台上會采集出來很多商家日常經營的資料,接着就可以對這些經營資料依托各種大資料計算平台,比如Hadoop、Spark、Flink等技術進行海量資料的計算,計算出來各種各樣的 資料報表。然後我們需要将計算好的各種資料分析報表都放入一些存儲中,比如說MySQL、Elastcisearch、HBase都可以存放類似的資料。最後一步,就是基于MySQL、HBase、Elasticsearch中存儲的資料報表,基于Java開發出來一個BI系統,通過這個系統把各種存儲好 的資料暴露給前端,允許前端基于各種條件對存儲好的資料進行複雜的篩選和分析,如下圖所示。

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結

2.2 技術痛點

在少數商家的量級之下,這個系統是沒多大問題的,運作的非常良好,但是問題恰恰就出在突然使用系統的商家數量開始暴漲的時候。

當商家的數量級達到幾萬的時候。此類BI系統的特點,就是在BI系統中有一種資料報表,他是支援前端頁面有一個JS腳本,自動每隔幾秒鐘就發送 請求到背景重新整理一下資料的,這種報表稱之為“實時資料報表”。

假設僅僅就幾萬商家作為你的系統使用者,很可能同一時間打開那個實時報表的商家就有幾千個,然後每個商家打開實時報表之後,前端頁面都會每隔幾秒鐘發送請求到背景來加載最新資料,基本上會出現你BI系統部署的每台機器每 秒的請求會達到幾百個,我們假設就是每秒500個請求吧。然後每個請求會加載出來一張報表需要的大量資料,因為BI系統可能還需要針對那些資料進行記憶體中的現場計算加工一下,才能傳回給 前端頁面展示。

根據測算,每個請求大概需要加載出來100kb的資料進行計算,是以每秒500個請求,就需要加載出來50MB的資料到記憶體中進行計算。

2.3 頻繁Young GC 影響不大

在上述系統運作模型下,基本上每秒會加載50MB的資料到Eden區中,隻要區區200s,也就 是3分鐘左右的時間,就會迅速填滿Eden區,然後觸發一次Young GC對新生代進行垃圾回收。

當然1G左右的Eden進行Young GC其實速度相對是比較快的,可能也就幾十ms的時間就可以搞定了。其實對系統性能影響并不大。而且上述BI系統場景下,基本上每次Young GC後存活對象可能就幾十MB,甚至是 幾MB。

是以如果僅僅隻是這樣的話,那麼大家可能會看到如下場景,BI系統運作幾分鐘過後,就會突然卡頓個10ms,但是對終端使用者和系統 性能幾乎是沒有影響的。

2.4 優化方式一:提升機器配置,運用大記憶體機器

随着越來越多的商家來使用,并發壓力越來越大,甚至高峰期會有每秒10萬的并發壓力。

如果還是用4核8G的機器來支撐,那麼可能需要部署上百台機器來抗住每秒10萬的高并發壓力。是以一般針對這種情況,我們會提升機器的配置,本身BI系統就是非常吃記憶體的系統,是以我們将部署的機器全面提升到了16核32G的 高配置機器上去。每台機器可以抗個每秒幾千請求,此時隻要部署比如二三十台機器就可以了。

但是此時問題就來了,大家可以想一下,如果要是用大記憶體機器的話,那麼新生代至少會配置設定到20G的大記憶體,Eden區也會占據16G以 上的記憶體空間。此時每秒幾千請求的話,每秒大概會加載到記憶體中幾百MB的資料,那麼大概可能幾十秒,甚至1分鐘左右就會填滿Eden區,會就需要執行Young GC。

此時Young GC要回收那麼大的記憶體,速度會慢很多,也許此時就會導緻系統卡頓個幾百毫秒,或者1秒鐘。系統卡頓時間過長,必然會導緻瞬間很多請求積壓排隊,嚴重的時候會導緻線上系統時不時出現前端請求逾時的問題,就是 前端請求之後發現一兩秒後還沒傳回就逾時報錯了。

2.5 使用G1 優化

對這個系統的一個優化,就是采用G1垃圾回收器來應對大記憶體的Young GC過慢的問題。對G1設定一個預期的GC停頓時間,比如100ms,讓G1保證每次Young GC的時候最多停頓100ms,避免影響終端使用者的使用。此時效果是非常顯著的,G1會自動控制好在每次Young GC的時候就回收一部分Region,確定GC停頓時間控制在100ms以内。

這樣的話,也許Young GC的頻率會更高一些,但是每次停頓時間很小,這樣對系統影響就不大了。

三、模拟垃圾回收的場景

3.1 YoungGC 的示範

3.1.1示例代碼

public class YoungGCTest {

    public static void main(String[] args) {
        byte[] array1 = new byte[1024 * 1024];
        array1 = new byte[1024 * 1024];
        array1 = new byte[1024 * 1024];
        array1=null;

        byte[] array2=new byte[2*1024*1024];
    }

}
           

3.1.2 程式的JVM參數示範

我們用以下JVM參數來運作代碼:

-XX:NewSize=5242880 
-XX:MaxNewSize=5242880 
-XX:InitialHeapSize=10485760 
-XX:MaxHeapSize=10485760 
-XX:SurvivorRatio=8 
-XX:PretenureSizeThreshold=10485760 
-XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC
           

上述參數都是基于JDK 1.8版本來設定的,不同的JDK版本對應的參數名稱是不太一樣的,但是基本意思是類似的。

-XX:InitialHeapSize”和“-XX:MaxHeapSize

”就是初始堆大小和最大堆大小,

-XX:NewSize

”和“

XX:MaxNewSize

”是初始新生代大小和最大新生代大小,

-XX:PretenureSizeThreshold=10485760

”指定了大對象門檻值是10MB。

相當于給堆記憶體配置設定10MB記憶體空間,其中新生代是5MB記憶體空間,其中Eden區占4MB,每個Survivor區占0.5MB, 大對象必須超過10MB才會直接進入老年代,年輕代使用ParNew垃圾回收器,老年代使用CMS垃圾回收器,看下圖圖示。

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結

3.1.3 列印JVM GC日志

在系統的JVM參數中加入GC日志的列印選型,如下所示:

-XX:+PrintGCDetils:列印詳細的gc日志

-XX:+PrintGCTimeStamps

:這個參數可以列印出來每次GC發生的時間

-Xloggc:gc.log

:這個參數可以設定将gc日志寫入一個磁盤檔案

加上這個參數之後,jvm參數如下所示:

-XX:NewSize=5242880 
-XX:MaxNewSize=5242880 
-XX:InitialHeapSize=10485760 
-XX:MaxHeapSize=10485760 
-XX:SurvivorRatio=8 
-XX:PretenureSizeThreshold=10485760 
-XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-Xloggc:gc.log
           

運作即可,此時運作完畢後,會在工程目錄中出現一個gc.log檔案,裡面就是本次程式運作的gc日志。

3.1.4 執行代碼時的記憶體分析

  1. 執行第一行代碼

    byte[] array1 = new byte[1024 * 1024];

    這行代碼運作,就會在JVM的Eden區内放入一個1MB的對象,同時在main線程的虛拟機棧中會壓入一個main() 方法的棧幀,在main()方法的棧幀内部,會有一個“array1”變量,這個變量是指向堆記憶體Eden區的那個1MB的數組。
    【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結
  2. 執行第二行和第三行代碼

    array1 = new byte[1024 * 1024];

    此時前面兩個數組都沒有人引用了,就都成了垃圾對象。
    【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結
  3. 執行第四行代碼

    array1=null;

    這行代碼一執行,就讓array1這個變量什麼都不指向了,此時會導緻之前建立的3個數組全部變成垃圾對象
    【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結
  4. 執行第五行代碼

    byte[] array2=new byte[2*1024*1024];

    此時會配置設定一個2MB大小的數組,嘗試放入Eden區中。明顯是不行的,因為Eden區總共就4MB大小,而且裡面已經放入了3個1MB的數組了,是以剩餘空間隻有1MB了,此時你放一個2MB的數組是放不下的。

是以這個時候就會觸發年輕代的Young GC。

3.1.5 gc日志結果詳解

gc日志結果:

Java HotSpot(TM) 64-Bit Server VM (25.151-b12) for windows-amd64 JRE (1.8.0_151-b12), built on Sep  5 2017 19:33:46 by "java_re" with MS VC++ 10.0 (VS2010)

Memory: 4k page, physical 33450456k(25709200k free), swap 38431192k(29814656k free)

CommandLine flags: -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=5242880 XX:NewSize=5242880 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=10485760 -XX:+PrintGC -XX:+PrintGCDetails XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC

0.268: [GC (Allocation Failure) 0.269: [ParNew: 4030K->512K(4608K), 0.0015734 secs] 4030K->574K(9728K), 
0.0017518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

Heap

par new generation   total 4608K, used 2601K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)

eden space 4096K,  51% used [0x00000000ff600000, 0x00000000ff80a558, 0x00000000ffa00000)

from space 512K, 100% used [0x00000000ffa80000, 0x00000000ffb00000, 0x00000000ffb00000)

to   space 512K,   0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)

concurrent mark-sweep generation total 5120K, used 62K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)

Metaspace  used 2782K, capacity 4486K, committed 4864K, reserved 1056768K

class space  used 300K, capacity 386K, committed 512K, reserved 1048576K
           
  1. 啟動參數

    在GC日志中,可以看到如下内容:

表示這次運作程式采取的JVM參數是什麼,基本都是我們設定的,同時還有一些參數預設就給設定了。

如果沒設定JVM參數的話,顯示的就是系統預設JVM參數。預設給的記憶體是很小的。

  1. 一次GC的概要說明

    如下日志資訊:

0.268: [GC (Allocation Failure) 0.269: [ParNew: 4030K->512K(4608K), 0.0015734 secs] 4030K->574K(9728K), 
0.0017518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

           

概要說明了本次GC的執行情況,GC (Allocation Failure),表明出現 GC 的原因,因為建立 array2 時要配置設定一個2MB的數組,此時Eden區記憶體不夠,是以就出現了“Allocation Failure”,也就是對象配置設定失敗。是以此時就要觸發一次Young GC。

數字“0.268”,表示系統運作以後過了多少秒發生了本次GC,比如這裡就是大概系統運作之後過了大概200多毫秒,發生了本次GC。

ParNew: 4030K->511K(4608K), 0.0012884 secs

“ParNew”的意思,表示指定的是ParNew垃圾回收器執行GC的。

4030K->512K(4608K)

這個代表的意思是,年輕代可用空間是4608KB,也就是4.5MB,Eden區是4MB,兩個Survivor中隻有一個是可以放存活對象的,另外一個是必須一緻保持空閑的,是以他考慮年輕代的可用空間,就是Eden+1個Survivor的大小,也就是4.5MB。

4030K->512K,意思就是對年輕代執行了一次GC,GC之前都使用了4030KB了,但是GC之後隻有512KB的對象 是存活下來的。

0.0015734 secs,這個就是本次gc耗費的時間,看這裡來說大概耗費了1.5ms,僅僅是回收3MB的對象而已。

4030K->574K(9728K), 0.0017518 secs,這段話指的是整個Java堆記憶體的情況

意思是整個Java堆記憶體是總可用空間9728KB(9.5MB),其實就是年輕代4.5MB+老年代5M,然後GC前整個Java堆記憶體裡使用了4030KB,GC之後Java堆記憶體使用了574KB。

[Times: user=0.00 sys=0.00, real=0.00 secs]

這個意思就是本次gc消耗的時間,也就是說本次gc就耗費了幾毫秒,從秒為機關來看,幾乎是0。

gc回收之後,從4030KB記憶體使用降低到了512KB的記憶體使用。

也就是說這次gc日志有512KB的對象存活了下來,從Eden區轉移到了Survivor1區,其實我們可以把稱呼改改,叫做 Survivor From區,另外一個Survivor叫做Survivor To區。

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結
  1. GC過後的堆記憶體使用情況
Heap

par new generation   total 4608K, used 2601K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)

           

上述日志中 par new generation total 4608K, used 2601K,這就是說“ParNew”垃圾回收器負責的年輕代總共有 4608KB(4.5MB)可用記憶體,目前是使用了2601KB(2.5MB)。array2數組和512KB的未知對象的總大小(包含數組額外使用的記憶體空間)。

eden space 4096K,  51% used [0x00000000ff600000, 0x00000000ff80a558, 0x00000000ffa00000)

from space 512K, 100% used [0x00000000ffa80000, 0x00000000ffb00000, 0x00000000ffb00000)

to   space 512K,   0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)
           

通過上述GC日志就能驗證我們的推測是完全準确的,Eden區此時4MB的記憶體被使用了51%,就是因為有一個2MB的數組在裡面。

然後From Survivor區,512KB是100%的使用率,此時被之前gc後存活下來的512KB的未知對象給占據了。

concurrent mark-sweep generation total 5120K, used 62K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)

Concurrent Mark-Sweep 垃圾回收器,也就是 CMS垃圾回收器,管理的老年代記憶體空間一共是5MB,此時使用了62KB的空間,可以通過記憶體分析工具檢視。

```powershell
Metaspace  used 2782K, capacity 4486K, committed 4864K, reserved 1056768K

class space  used 300K, capacity 386K, committed 512K, reserved 1048576K
           

上述兩段日志也很簡單,意思就是Metaspace中繼資料空間和Class空間,存放一些類資訊、常量池之類的東西,此時他 們的總容量,使用記憶體,等等。

used capacity commited 和reserved,MetaSpace由一個或多個Virtual Space(虛拟空間)組成。虛拟空間是作業系統的連續存儲空間,虛拟空間是按需配置設定的。當被配置設定時,虛拟空間會向作業系統預留(reserve)空間,但還沒有被送出(committed)。

MetaSpace的預留白間(reserved)是全部虛拟空間的大小。 虛拟空間的最小配置設定單元是MetaChunk(也可以說是 Chunk)。

當新的Chunk被配置設定至虛拟空間時,與Chunk相關的記憶體空間被送出了(committed)。MetaSpace的committed 指的是所有Chunk占有的空間。

每個Chunk占據空間不同,當一個類加載器(Class Loader)被gc時,所有與之關聯的Chunk被釋放(freed)。這些被釋放的Chunk被維護在一個全局的釋放數組裡。

MetaSpace的capacity指的是所有未被釋放的Chunk占據的空間。 這麼看gc日志發現自己committed是4864K, capacity4486K。有一部分的Chunk已經被釋放了,代表有類加載器被回收了

3.2 模拟對象從新生代進入老年代的場景

3.2.1 動态年齡判定規則

如果Survivor區域内年齡1+年齡2+年齡3+年齡n的對象總和大于Survivor區的50%,此時年齡n以上的對象會進入 老年代。這就是所謂的動态年齡判定規則。

代碼示例:

public class GCTest2 {

    public static void main(String[] args) {
        byte[] array1 = new byte[2 * 1024 * 1024];
        array1 = new byte[2 * 1024 * 1024];
        array1 = new byte[2 * 1024 * 1024];
        array1 = null;

        byte[] array2 = new byte[128 * 1024];

        byte[] array3 = new byte[2 * 1024 * 1024];
        array3 = new byte[2 * 1024 * 1024];
        array3 = new byte[2 * 1024 * 1024];
        array3 = new byte[128 * 1024];
        array3 = null;

        byte[] array4 = new byte[2 * 1024 * 1024];
    }
}
           

JVM 啟動參數設定如下:

-XX:InitialHeapSize=20971520
-XX:MaxHeapSize=20971520
-XX:NewSize=10485760
-XX:MaxNewSize=10485760
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15
-XX:PretenureSizeThreshold=10485760
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:gcTest2.log
           

此處,新生代通過“

-XX:NewSize

”設定為10MB,然後其中Eden區是8MB,每個Survivor區是1MB,Java堆總大小是20MB,老年代是10MB,大對象必須超過10MB才會直接進入老年代。

通過“

-XX:MaxTenuringThreshold=15

”設定了,隻要對象年齡達到15歲才會直接進入老年代。

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結

執行代碼後的gc日志如下:

Java HotSpot(TM) Client VM (25.65-b01) for windows-x86 JRE (1.8.0_65-b17), built on Oct  6 2015 17:26:22 by "java_re" with MS VC++ 10.0 (VS2010)

Memory: 4k page, physical 8274996k(1681152k free), swap 13780020k(3192280k free)

CommandLine flags: 
-XX:InitialHeapSize=20971520 
-XX:MaxHeapSize=20971520 
-XX:MaxNewSize=10485760 
-XX:MaxTenuringThreshold=15 
-XX:NewSize=10485760 
-XX:OldPLABSize=16 
-XX:PretenureSizeThreshold=10485760 
-XX:+PrintGC -XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-XX:SurvivorRatio=8 
-XX:+UseConcMarkSweepGC 
-XX:-UseLargePagesIndividualAllocation 
-XX:+UseParNewGC 

0.152: [GC (Allocation Failure) 0.153: [ParNew: 8129K->705K(9216K), 0.0007779 secs] 8129K->705K(19456K), 
0.0009952 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

0.154: [GC (Allocation Failure) 0.154: [ParNew: 7006K->0K(9216K), 0.0017488 secs] 7006K->695K(19456K), 
0.0018044 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
 
Heap
 par new generation   total 9216K, used 2130K [0x04c00000, 0x05600000, 0x05600000)
  eden space 8192K,  26% used [0x04c00000, 0x04e14938, 0x05400000)
  from space 1024K,   0% used [0x05400000, 0x05400000, 0x05500000)
  to   space 1024K,   0% used [0x05500000, 0x05500000, 0x05600000)
  
 concurrent mark-sweep generation total 10240K, used 695K [0x05600000, 0x06000000, 0x06000000)
 
 Metaspace       used 2097K, capacity 2280K, committed 2368K, reserved 4480K
           

過程分析:

當執行下面的代碼:

byte[] array3 = new byte[2 * 1024 * 1024];

時,Eden 區總10MB 已經存放了 3 * 2MB + 128KB ,此時建立 array3 會觸發一次 YoungGC。

即日志中的以下部分:

0.152: [GC (Allocation Failure) 0.153: [ParNew: 8129K->705K(9216K), 0.0007779 secs] 8129K->705K(19456K), 
0.0009952 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
           

系統運作 0.152s 後發生了第一次YoungGC,原因是

Allocation Failure

配置設定對象記憶體失敗,采用了 ParNew 垃圾回收器,年輕代的可用記憶體(Eden區+1個Survivor區)為9MB(9216K),YoungGC 之前占用了8129K,GC之後有 705K 的對象(年輕代剛開始會有512KB的未知對象+128KB+使用數組的記憶體)存活下來,本次GC耗費時間0.99ms。

整個Java堆記憶體是總可用空間19456KB(19MB),其實就是年輕代9MB+老年代10M,然後GC前整個Java堆記憶體裡使用了8129KB,GC之後Java堆記憶體使用了705KB。

[Times: user=0.00 sys=0.00, real=0.00 secs]

表示本次gc消耗的時間,也就是說本次gc就耗費了幾毫秒,從秒為機關來看,幾乎是0。

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結

此時Survivor From區裡的那700kb的對象,熬過一次gc,年齡就會增長1歲。而且此時Survivor區域總大小是1MB,此時Survivor區域中的存活對象已經有700KB了,超過了50%。

當執行代碼

byte[] array4 = new byte[2 * 1024 * 1024];

時,Eden 區總10MB 已經存放了 3 * 2MB + 128KB ,此時建立 array4 會觸發第二次 YoungGC。

即:

0.154: [GC (Allocation Failure) 0.154: [ParNew: 7006K->0K(9216K), 0.0017488 secs] 7006K->695K(19456K), 
0.0018044 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
           

系統運作 0.154s 後發生了第二次YoungGC,原因是

Allocation Failure

配置設定對象記憶體失敗,采用了 ParNew 垃圾回收器,年輕代的可用記憶體為9MB(9216K),YoungGC 之前占用了7006K,GC之後為0,沒有對象在年輕代,本次GC耗費時間1.7ms。

整個Java堆記憶體是總可用空間19456KB(19MB),年輕代9MB+老年代10M,然後GC前整個Java堆記憶體裡使用了70,06KB,GC之後Java堆記憶體使用了695KB。

此時在Eden區裡有3個2MB的數組和1個128KB的數組,會被回收掉,而根據動态年齡判斷規則:年齡1+年齡2+年齡n的對象總大小超過了Survivor區域的50%,年齡n以上的對象進入老年代。Survivor From 這裡的對象都是年齡1的,是以直接全部進入老年代了。

從日志可以看出:

CMS管理的老年代,此時使用空間剛好是695KB(array2引用的128KB數組和512KB未知對象),證明此時Survivor裡的對象觸發了動态年齡判定規則進入老年代了。

然後array4變量引用的那個2MB的數組,此時就會配置設定到Eden區域中。

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結

Eden區目前就是有一個2MB的數組。日志如下:

Heap
 par new generation   total 9216K, used 2130K [0x04c00000, 0x05600000, 0x05600000)
  eden space 8192K,  26% used [0x04c00000, 0x04e14938, 0x05400000)
  from space 1024K,   0% used [0x05400000, 0x05400000, 0x05500000)
  to   space 1024K,   0% used [0x05500000, 0x05500000, 0x05600000)
           

兩個Survivor區域都是空的,因為之前存活的700KB的對象都進入老年代了,是以當然現在Survivor裡都是空的了。

3.2.2 Young GC過後存活對象放不下Survivor區域,直接進入老年代

代碼示例:

public class GCTest3 {

    public static void main(String[] args) {
        byte[] array1 = new byte[2 * 1024 * 1024];
        array1 = new byte[2 * 1024 * 1024];
        array1 = new byte[2 * 1024 * 1024];

        byte[] array2 = new byte[128 * 1024];
        array2=null;

        byte[] array3 = new byte[2 * 1024 * 1024];

    }
}
           

執行代碼`byte[] array3 = new byte[2 * 1024 * 1024];``,Eden 區記憶體不夠觸發YoungGC時的記憶體分析:

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結

此次GC會回收掉上圖中的2個2MB的數組和1個128KB的數組,然後留下 一個2MB的數組和1個未知的500KB的對象。

gc日志如下:

Java HotSpot(TM) Client VM (25.65-b01) for windows-x86 JRE (1.8.0_65-b17), built on Oct  6 2015 17:26:22 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8274996k(1580276k free), swap 13780020k(3646816k free)

CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC 

0.175: [GC (Allocation Failure) 0.175: [ParNew: 8129K->577K(9216K), 0.0014206 secs] 8129K->2626K(19456K), 
0.0015901 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

Heap
 par new generation   total 9216K, used 2707K [0x05200000, 0x05c00000, 0x05c00000)
  eden space 8192K,  26% used [0x05200000, 0x05414938, 0x05a00000)
  from space 1024K,  56% used [0x05b00000, 0x05b90660, 0x05c00000)
  to   space 1024K,   0% used [0x05a00000, 0x05a00000, 0x05b00000)
  
 concurrent mark-sweep generation total 10240K, used 2049K [0x05c00000, 0x06600000, 0x06600000)
 Metaspace       used 2097K, capacity 2280K, committed 2368K, reserved 4480K
           

由以上日志可知,

系統運作 0.175s 後發生了一次YoungGC,原因是

Allocation Failure

配置設定對象記憶體失敗,年輕代的可用記憶體為9MB(9216K),YoungGC 之前占用了8129K,GC之後為577。

此次gc時,因為Survivor區僅僅隻有1MB,而存活的對象有2MB和512KB的未知對象。從日志看可知,gc過後,年輕代裡剩下了500多KB的對象(512KB的未知對象),是以這些存活對象并不是全部放入老年代,在這種情況下,是會把部分對象放入Survivor區的。

整個Java堆記憶體是總可用空間19456KB(19MB),年輕代9MB+老年代10M,然後GC前整個Java堆記憶體裡使用了8129KB,GC之後Java堆記憶體使用了2626KB。

此時老年代裡有2MB的數組,是以可以認為,Young GC過後,發現存活下來的對象有2MB的數組和500KB的未知對象。

此時把500KB的未知對象放入Survivor中,然後2MB的數組直接放入老年代。

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結

3.2.3 對象達到15歲年齡之後自然進入老年代的場景

待補充…

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結

3.2.4 大對象直接進入老年代

待補充…

3.3 老年代 FullGC 的示範

代碼示例:

public class FullGCTest {

    public static void main(String[] args) {
        byte[] array1 = new byte[4 * 1024 * 1024];
        array1 = null;

        byte[] array2 = new byte[2 * 1024 * 1024];
        byte[] array3 = new byte[2 * 1024 * 1024];
        byte[] array4 = new byte[2 * 1024 * 1024];
        byte[] array5 = new byte[128 * 1024];

        byte[] array6 = new byte[2 * 1024 * 1024];
    }
}
           

設定JVM啟動參數:

-XX:NewSize=10485760 
-XX:MaxNewSize=10485760 
-XX:InitialHeapSize=20971520 
-XX:MaxHeapSize=20971520 
-XX:SurvivorRatio=8  
-XX:MaxTenuringThreshold=15 
-XX:PretenureSizeThreshold=3145728 
-XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-Xloggc:gc.log
           

這裡最關鍵一個參數,就是“

-XX:PretenureSizeThreshold=3145728

”這個參數要設定大對象門檻值為3MB,也就是超過3MB,就直接進入老年代。

gc日志分析:

Java HotSpot(TM) Client VM (25.65-b01) for windows-x86 JRE (1.8.0_65-b17), built on Oct  6 2015 17:26:22 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8274996k(1798160k free), swap 13780020k(3475820k free)

CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 
-XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=3145728 
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC 
-XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC
 
0.222: [GC (Allocation Failure) 0.222: [ParNew (promotion failed): 8129K->8835K(9216K), 0.0031393 secs]
0.225: [CMS: 8193K->6837K(10240K), 0.0045225 secs] 12225K->6837K(19456K), 
[Metaspace: 2093K->2093K(4480K)], 0.0079786 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

Heap
 par new generation   total 9216K, used 2130K [0x05200000, 0x05c00000, 0x05c00000)
  eden space 8192K,  26% used [0x05200000, 0x05414938, 0x05a00000)
  from space 1024K,   0% used [0x05b00000, 0x05b00000, 0x05c00000)
  to   space 1024K,   0% used [0x05a00000, 0x05a00000, 0x05b00000)
  
 concurrent mark-sweep generation total 10240K, used 6837K [0x05c00000, 0x06600000, 0x06600000)
 
 Metaspace       used 2097K, capacity 2280K, committed 2368K, reserved 4480K
           

建立數組 array1 時,大小為4MB,直接進入老年代,然後不再引用。

接下來連續建立的 4個數組,3個大小為2Mb,1個是128KB ,進入年輕代的Eden區(總空間為8MB),當執行建立2MB的數組

byte[] array6 = new byte[2 * 1024 * 1024];

時,觸發YoungGC。此時對象的引用情況如下:

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結

GC日志:

這行日志顯示了,Eden區原來是有8000多KB的對象,但是回收之後發現一個都回收不掉,因為上述幾個數組都被變量引用了。

是以此時會把這些對象放入到老年代裡去,但是此時老年代裡已經有一個4MB的數組了,無法放下3個2MB的數組和1個128KB的數組,此時觸發了CMS垃圾回收器的Full GC。(Full GC其實就是會對老年代進行Old GC, 同時一般會跟一次Young GC關聯,還會觸發一次中繼資料區(永久代)的GC。)

0.225: [CMS: 8193K->6837K(10240K), 0.0045225 secs] 12225K->6837K(19456K), 
[Metaspace: 2093K->2093K(4480K)], 0.0079786 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
           

此時就會回收掉其中的一個4MB的數組,因為他已經沒人引用了。老年代的記憶體占用從回收前的8MB變成了 6MB。

【JVM 學習筆記 04】:JVM 的場景模拟和優化案例一、基于G1垃圾回收器的百萬級使用者線上教育平台的性能優化二、每秒10萬并發的BI系統的優化三、模拟垃圾回收的場景四、JVM 優化思路總結

四、JVM 優化思路總結

  1. 項目上線初期:

    ①、上線前,根據預期的并發量、平均每個任務的記憶體需求大小等,然後評估需要使用幾台機器來承載,每台機器需要 什麼樣的配置。

    ②、根據系統的任務處理速度,評估記憶體使用情況,然後合理配置設定Eden、Survivor,老年代的記憶體大小。

總體原則就是,讓短命對象在YoungGC就被回收,不要進入老年代,長期存活的對象,盡早進入老年代,不要在新生 代複制來複制去。對系統響應時間敏感且記憶體需求大的,建議采用G1回收器

如何合理配置設定各個區域: 根據記憶體增速來評估多久進行Young GC 根據每次Young GC的存活,評估一下Survivor區的大小設定是否合理評估多久進行一次FullGC,産生的STW,是否可以接受?

  1. 公司的營運效果很佳,過了一段時間,系統負載增加了10倍,100倍:

    方案1:增加伺服器數量 根據系統負載的增比,同比增加機器數量,機器配置,和jvm的配置可以保持不變。

    方案2:使用更高配置的機器 更高的配置,意味着更快速的處理速度和更大的記憶體。響應時間敏感且記憶體需求大的使 用G1回收器這時候需要和‘項目上線初期’一樣,合理的使用配置和配置設定記憶體。

繼續閱讀