[JVM] JVM調優相關總結

一  問題描述

1  頻繁Full GC  

  現象描述:堆內存佔用較快,運行到3天時就達到100%,並觸發了老年代GChtml

  

JVM知識回顧:
  Java的堆內存由新生代(New or Young)和老年代(Old)組成。新生代進一步劃分爲一個Eden空間和兩個Survivor空間S0、S1(也稱爲From、To)。Eden空間是對象被建立時的地方,通過新生代GC後存活的對象存放到Survivor空間,通過幾輪新生代GC仍然存活會存放到Old區。java

  在JVM默認參數下,Survivor空間過小。致使在GC清理後,Eden區不少對象沒法存放到Survivor區。所以,這些對象被過早地保存在老年代中,這會致使老年代佔用增加很快。經過jmap -heap pid查看堆內存分佈狀況以下:python

  

  上圖是設置了SurviorRatio=6時(Eden區與Survivor區比例爲6:1),按照計算髮現Survior區比設置的小不少。這個數值仍是動態變化的,有時候十幾兆,有時候幾兆。緣由是:JDK8使用ParallelGC做爲默認GC算法,在這個算法下使用了「AdaptiveSizePolicy」策略,它會動態調整Eden和Survivor的空間。若是開啓 AdaptiveSizePolicy,則每次 GC 後會從新計算 Eden、From 和 To 區的大小,計算依據是 GC 過程當中統計的 GC 時間、吞吐量、內存佔用量。算法

  關閉AdaptiveSizePolicy有兩種辦法:緩存

    1. 經過JMV參數關閉:-XX:-UseAdaptiveSizePolicy服務器

    2. 老年代使用CMS回收器,CMS不會開啓UseAdaptiveSizePolicy。開啓CMS回收器,新生代會默認使用ParNew GC回收器(併發收集器)。 數據結構

-XX:SurvivorRatio=6 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSScavengeBeforeRemark -XX:NativeMemoryTracking=detail
-XX:+UseConcMarkSweepGC  使用CMS收集器

-XX:+ UseCMSCompactAtFullCollection  Full GC後,進行一次碎片整理;整理過程是獨佔的,會引發停頓時間變長

-XX:+CMSFullGCsBeforeCompaction  設置進行幾回Full GC後,進行一次碎片整理

-XX:ParallelCMSThreads  設定CMS的線程數量(通常狀況約等於可用CPU數量)

-XX:+CMSScavengeBeforeRemark  在remark以前強制進行一次Young GC。

  修改參數優化後:多線程

  

 優化後效果:併發

  

2  對堆外內存估算少

  Java程序佔用內存分爲堆內存和非堆內存,堆內存裏分新生代和老年代,非堆內存包含方法區、jdk8的MetaSpace、棧空間、程序計數器等。咱們常常關注的是堆內存,而較少關注非堆內存。酒店靜態外網接口應用縮容時,容器內存是2G,並修改了堆內存最大限制爲1.5G(-Xms1536m -Xmx1536m)。也就是說java進程的非堆內存+操做系統的其餘進程佔用的內存若是不超過500M,就不會引發oom問題。經過使用Native Memory Tracking (NMT) 來分析JVM內部內存使用狀況,能夠發現非堆內存佔用達到800~900M。所以系統長時間運行後,內存佔用可能會超出2G,所以宕機。oracle

   Native Memory Tracking (NMT) 是Hotspot VM用來分析VM內部內存使用狀況的一個功能。在JVM參數中加入-XX:NativeMemoryTracking=detail,打開NMT。而後登陸堡壘機,經過jcmd VM.native_memory summary命令,能夠查看內存佔用狀況。

  下面是對一個2G內存容器,Xmx爲1G的應用的統計。每一項含義說明,見官方說明

  其中第一項Java Heap是堆內存。其餘就能夠理解外非堆內存的佔用,共700M。其中Thread項是線程佔用的內存,默認每一個線程1M,一共303個線程佔用了300M。

Native Memory Tracking:

Total: reserved=3000668KB, committed=1755712KB
-                 Java Heap (reserved=1048576KB, committed=1048576KB)
                            (mmap: reserved=1048576KB, committed=1048576KB) 
 
-                     Class (reserved=1126128KB, committed=85464KB)
                            (classes #13164)
                            (malloc=1776KB #25469) 
                            (mmap: reserved=1124352KB, committed=83688KB) 
 
-                    Thread (reserved=311789KB, committed=311789KB)
                            (thread #303)
                            (stack: reserved=310456KB, committed=310456KB)
                            (malloc=979KB #1567) 
                            (arena=355KB #605)
 
-                      Code (reserved=258501KB, committed=54209KB)
                            (malloc=8901KB #11187) 
                            (mmap: reserved=249600KB, committed=45308KB) 
 
-                        GC (reserved=101204KB, committed=101204KB)
                            (malloc=97784KB #800) 
                            (mmap: reserved=3420KB, committed=3420KB) 
 
-                  Compiler (reserved=456KB, committed=456KB)
                            (malloc=325KB #600) 
                            (arena=131KB #3)
 
-                  Internal (reserved=87776KB, committed=87776KB)
                            (malloc=87744KB #19745) 
                            (mmap: reserved=32KB, committed=32KB) 
 
-                    Symbol (reserved=18473KB, committed=18473KB)
                            (malloc=14533KB #158025) 
                            (arena=3940KB #1)
 
-    Native Memory Tracking (reserved=3674KB, committed=3674KB)
                            (malloc=215KB #3261) 
                            (tracking overhead=3459KB)
 
-               Arena Chunk (reserved=226KB, committed=226KB)
                            (malloc=226KB) 
 
-                   Unknown (reserved=43864KB, committed=43864KB)
                            (mmap: reserved=43864KB, committed=43864KB)

解決方案建議

  一、設置Xmx和Xms時要爲堆外內存留出足夠的空間,建議大於1G,若是內存資源容許大於2G。

  二、優化JVM參數。
    a) 對應接口類型的應用,使用CMS回收器,減小停頓時間,保證接口性能。

-XX:SurvivorRatio=6-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0-XX:+CMSScavengeBeforeRemark -XX:NativeMemoryTracking=detail

  b) 對應後臺定時任務,MQ消費監聽等類型的應用,使用默認的Parallel Scavenge+Parallel Old回收器,優先保證吞吐量。並關閉UseAdaptiveSizePolicy參數。

-XX:SurvivorRatio=6-XX:-UseAdaptiveSizePolicy

二  JVM GC調優

1  GC回收的區域

  JVM GC只回收堆區和方法區內的對象。而棧區的數據,在超出做用域後會被JVM自動釋放掉,因此其不在JVM GC的管理範圍內。

2  如何判斷對象是否可被回收

  (1)判斷對象是否存活

    a)引用計數算法:

    給對象中添加一個引用計數器,每當有一個地方引用它時,計數器+1,當引用失效,計數器-1。任什麼時候刻計數器爲0的對象就是不可能再被使用的

  優勢:實現簡單,斷定效率高效,被actionscript3和python中普遍應用。
  缺點:沒法解決對象之間的相互引用問題。java沒有采納

    b)可達性分析算法:

    經過一系列稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GCRoots沒有任何引用鏈相連的時候,則證實此對象是不可用的。

    好比以下,右側的對象是到GCRoot時不可達的,能夠斷定爲可回收對象。

  

    在java中,能夠做爲GCRoot的對象包括如下幾種:

  * 虛擬機棧中引用的對象。

  * 方法區中靜態屬性引用的對象。

  * 方法區中常量引用的對象。

  * 本地方法中JNI引用的對象。

  基於以上,咱們能夠知道,噹噹前對象到GCRoot中不可達時候,即會知足被垃圾回收的可能。

  (2)判斷是否能夠被回收

  那麼是否是這些對象就非死不可,也不必定,此時只能宣判它們存在於一種「緩刑」的階段,要真正的宣告一個對象死亡。至少要經歷兩次標記:

第一次:對象可達性分析以後,發現沒有與GCRoots相鏈接,此時會被第一次標記並篩選。

第二次:對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,此時會被認定爲不必執行。

    在finalize裏能夠將該對象從新賦予給某個引用,從而使對象不會被回收。

3  何時執行GC

  Eden區空間不夠存放新對象的時候,執行Minor GC。升到老年代的對象大於老年代剩餘空間的時候執行Full GC,或者小於的時候被HandlePromotionFailure 參數強制Full GC 。

  調優主要是減小 Full GC 的觸發次數,能夠經過 NewRatio(新生代和老年代的大小比例) 控制新生代轉老年代的比例,經過MaxTenuringThreshold 設置對象進入老年代的年齡閥值。

  基於分代垃圾回收機制,新生代分爲:Eden區、兩個Survivor區(From區和To區),每次只佔用Eden區和一個Survivor區,另一個Survivor區空閒。新建立的對象優先在Eden區分配分配內存空間,當Eden區的空間不足以存放新對象時,會觸發一次Minor GC,將Eden區和一個Survivor區中存活的對象複製到另外一個Survivor區,而且對象的年齡+1,而後清空Eden區和Survivor區。在知足以下狀況之一,新生代中的對象會移動到老年代:

  (1)Eden區滿時,進行Minor GC,當Eden和一個Survivor區中依然存活的對象沒法放入到另外一個Survivor中,則經過分配擔保機制提早轉移到老年代中。 

  (2)若對象體積太大, 新生代沒法容納這個對象,-XX:PretenureSizeThreshold超過這個值的時候,對象直接在old區分配內存,默認值是0,意思是無論多大都是先在eden中分配內存, 此參數只對Serial及ParNew兩款收集器有效。

  (3)長期存活的對象將進入老年代。

          虛擬機對每一個對象定義了一個對象年齡(Age)計數器。當年齡增長到必定的臨界值時,就會晉升到老年代中,該臨界值由參數:-XX:MaxTenuringThreshold來設置。

          若是對象在Eden出生並在第一次發生MinorGC時仍然存活,而且可以被Survivor中所容納的話,則該對象會被移動到Survivor中,而且設Age=1;之後每經歷一次Minor GC,該對象還存活的話Age=Age+1。

  (4)動態對象年齡斷定。

          虛擬機並不老是要求對象的年齡必須達到MaxTenuringThreshold才能晉升到老年代,若是在Survivor區中相同年齡(設年齡爲age)的對象的全部大小之和超過Survivor空間的一半,年齡大於或等於該年齡(age)的對象就能夠直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。

    JVM引入動態年齡計算,主要基於以下兩點考慮:

    1. 若是固定按照MaxTenuringThreshold設定的閾值做爲晉升條件: a)MaxTenuringThreshold設置的過大,本來應該晉升的對象一直停留在Survivor區,直到Survivor區溢出,一旦溢出發生,Eden+Svuvivor中對象將再也不依據年齡所有提高到老年代,這樣對象老化的機制就失效了。 b)MaxTenuringThreshold設置的太小,「過早晉升」即對象不能在新生代充分被回收,大量短時間對象被晉升到老年代,老年代空間迅速增加,引發頻繁的Major GC。分代回收失去了意義,嚴重影響GC性能。

    2. 相同應用在不一樣時間的表現不一樣:特殊任務的執行或者流量成分的變化,都會致使對象的生命週期分佈發生波動,那麼固定的閾值設定,由於沒法動態適應變化,會形成和上面相同的問題。

  持久代(Permanent generation)也稱之爲方法區(Method area):用於保存類常量以及字符串常量。注意,這個區域不是用於存儲那些從老年代存活下來的對象,這個區域也可能發生GC。發生在這個區域的GC事件也被算爲 Major GC 。只不過在這個區域發生GC的條件很是嚴苛,必須符合如下三種條件纔會被回收:

  一、全部實例被回收

  二、加載該類的ClassLoader 被回收

  三、Class 對象沒法經過任何途徑訪問(包括反射)

  Major GC:清理永久代,可是因爲不少MojorGC 是由MinorGC 觸發的,因此有時候很難將MajorGC 和MinorGC區分開。

  FullGC:是清理整個堆空間—包括年輕代和永久代。FullGC 通常消耗的時間比較長,遠遠大於MinorGC,所以,有時候咱們必須下降FullGC 發生的頻率。

4  垃圾回收算法 

  (1)「標記-清除」(Mark-Sweep)算法:首先標記出全部須要回收的對象,在標記完成後統一回收掉全部被標記的對象。

    它的主要缺點有兩個:一個是效率問題,標記和清除過程的效率都不高;另一個是空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使,當程序在之後的運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

  (2)「複製」(Mark-Copying)算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。

  這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,持續複製長生存期的對象則致使效率下降。

  (3)「標記-整理」(Mark-Compact)算法,標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。

  在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,所以通常選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清除」或「標記-整理」算法來進行回收。

5  系統崩潰前的一些現象

  (1)每次垃圾回收的時間愈來愈長,由以前的10ms延長到50ms左右,FullGC的時間也有以前的0.5s延長到四、5s

  (2)FullGC的次數愈來愈多,最頻繁時隔不到1分鐘就進行一次FullGC

  (3)老年代的內存愈來愈大而且每次FullGC後老年代沒有內存被釋放

   以後系統會沒法響應新的請求,逐漸到達OutOfMemoryError的臨界值。

6  緣由分析

  (1)爲何崩潰前垃圾回收的時間愈來愈長?

    根據內存模型和垃圾回收算法,垃圾回收分兩部分:內存標記、清除(複製),標記部分只要內存大小固定時間是不變的,變的是複製部分,由於每次垃圾回收都有一些回收不掉的內存,因此增長了複製量,致使時間延長。因此,垃圾回收的時間也能夠做爲判斷內存泄漏的依據。

  (2)爲何Full GC的次數愈來愈多?

    所以內存的積累,逐漸耗盡了年老代的內存,致使新對象分配沒有更多的空間,從而致使頻繁的Full GC垃圾回收。

  (3)爲何年老代佔用的內存愈來愈大?

    由於年輕代的內存沒法被回收,愈來愈多的對象被Copy到年老代。

8  調優目標

  (1)將進入老年代的對象數量降到最低

  (2)減小Full GC的執行時間

9  調優方法

  (1)針對JVM堆的設置,通常能夠經過-Xms -Xmx限定其最小、最大值,爲了防止垃圾收集器在最小、最大之間收縮堆而產生額外的時間,咱們一般把最大、最小設置爲相同的值。

  (2)年輕代和年老代將根據默認的比例(1:2)分配堆內存,能夠經過調整兩者之間的比率NewRadio來調整兩者之間的大小。也能夠針對回收代,好比年輕代,經過 -XX:newSize -XX:MaxNewSize來設置其絕對大小。一樣,爲了防止年輕代的堆收縮,咱們一般會把-XX:newSize -XX:MaxNewSize設置爲一樣大小

  (3)合理設置年輕代和老年代大小

    更大的年輕代必然致使更小的年老代,大的年輕代會延長普通GC的週期,但會增長每次GC的時間,小的年老代會致使更頻繁的Full GC。

    更小的年輕代必然致使更大年老代,小的年輕代會致使young GC很頻繁,但每次的GC時間會更短;大的年老代會減小Full GC的頻率,可是會增長老年代的gc時間。

    如何選擇應該依賴應用程序對象生命週期的分佈狀況:

      a)若是應用存在大量的臨時對象,應該選擇更大的年輕代;

      b)若是存在相對較多的持久對象,年老代應該適當增大。

    但不少應用都沒有這樣明顯的特性,在抉擇時應該根據如下兩點:

     (A)本着Full GC儘可能少的原則,讓年老代儘可能緩存經常使用對象,JVM的默認比例1:2也是這個道理

     (B)經過觀察應用一段時間,看應用在峯值時年老代會佔多少內存,在不影響Full GC的前提下,根據實際狀況加大年輕代,好比能夠把比例控制在1:1。但應該給年老代至少預留1/3的增加空間。

  (4)在配置較好的機器上(好比多核、大內存),能夠爲年老代選擇並行收集算法: -XX:+UseParallelOldGC ,默認爲Serial收集器。

  (5)線程堆棧的設置:每一個線程默認會開啓1M的堆棧,用於存放棧幀、調用參數、局部變量等,對大多數應用而言這個默認值太了,通常256K就足用。理論上,在內存不變的狀況下,減小每一個線程的堆棧,能夠產生更多的線程,但這實際上還受限於操做系統。最大線程數計算公式以下:

    (MaxProcessMemory – JVMMemory – ReservedOsMemory) / (ThreadStackSize) = Number of threads

    注:

      MaxProcessMemory:進程最大尋址空間。(通常爲服務器內存)

      JVMMEMORY:JVM的內存空間(堆+永久區)即-Xmx大小 (應該是實際分配大小)

      ReservedOsMemory:操做系統預留內存

      ThreadStackSize:-Xss大小

  (6)能夠經過下面的參數打Heap Dump信息

      -XX:HeapDumpPath

      -XX:+PrintGCDetails

      -XX:+PrintGCTimeStamps

      -Xloggc:/usr/aaa/dump/heap_trace.txt

    經過下面參數能夠控制OutOfMemoryError時打印堆的信息

      -XX:+HeapDumpOnOutOfMemoryError

10  調優案例

   (1)一個服務系統,常常出現卡頓,分析緣由,發現Full GC時間太長

    執行命令: jstat  -gc  pid:

 S0C        S1C        S0U      S1U      EC       EU        OC         OU       MC       MU        CCSC       CCSU      YGC     YGCT    FGC    FGCT     GCT   
4032.0     4032.0     2541.6    0.0    32320.0  17412.6   80620.0    51918.6   88576.0   84546.7   12288.0   11336.9    522     5.408   5      6.946    5.952

  解釋以下:

  S0C:第一個倖存區的大小;S1C:第二個倖存區的大小;S0U:第一個倖存區的使用大小;S1U:第二個倖存區的使用大小;EC:Enden區的大小;EU:Enden區的使用大小;

  OC:老年代大小;OU:老年代使用大小;MC:方法區大小;MU:方法區使用大小;CCSC:壓縮類空間大小;CCSU:壓縮類空間使用大小;YGC:年輕代垃圾回收次數;

  YGCT:年輕代垃圾回收消耗時間;FGC:老年代垃圾回收次數;FGCT:老年代垃圾回收消耗時間;GCT:垃圾回收消耗總時間

  分析上面的數據,發現Young GC執行了522次,耗時5.408秒,每次Young GC耗時1ms,在正常範圍,而Full GC執行了5次,耗時6.946秒,每次平均1.389s,數據顯示出來的問題是:Full GC耗時較長,分析該系統的參數發現,NewRatio=9,也就是說,新生代和老生代大小之比爲1:9,這就是問題的緣由:

  1,新生代過小,致使對象提早進入老年代,觸發老年代發生Full GC;

  2,老年代較大,進行Full GC時耗時較大;

  優化的方法是:調整NewRatio的值,調整到4,發現Full GC沒有再發生,只有Young GC在執行。這就是把對象控制在新生代就清理掉,沒有進入老年代(這種作法對一些應用是頗有用的,但並非對全部應用都要這麼作)

   (2)一應用在性能測試過程當中,發現CPU佔用率很高,Full GC頻繁,使用jmap -dump:format=b,file=文件名.hprof pid 來dump內存,生成dump文件,並使用Eclipse下的mat工具進行分析,發現:

  

  從圖中能夠看出,這個線程存在問題,隊列LinkedBlockingQueue所引用的大量對象並未釋放,從而致使一直在執行Full GC,從而致使CPU佔用率達到100%。

11  生產環境問題定位方法

  (1)經過ps -ef | grep java或者執行top -c ,顯示進程運行信息列表。按下P,進程按照cpu使用率排序獲取程序的pid

  

  (2)查看該pid下線程對應的系統佔用狀況。top -Hp 8813 ,顯示一個進程的線程運行信息列表。按下P,進程按照cpu使用率排序

  

  (3)發現pid 8851線程佔用的CPU最大

  (4)將這幾個pid轉爲16進制, printf 「0x%x\n」 8851 爲0x2293

  (5)下載當前的java線程棧jstack pid>pid.log 將線程棧 dump 到日誌文件中

  (6)在日誌中查詢(5)中對應的線程狀況,發現都是GC線程致使的 

  (7)dump java堆數據

    jmap -dump:format=b,file=heap.log pid 保存堆快照

  (8)使用MAT加載堆文件,能夠看到javax.crypto.JceSecurity對象佔用了95%的內存空間,初步定位到問題。而後排查代碼,查看是什麼致使的內存溢出。例如使用了靜態變量的Map,因此每次運行到某個方法時都會向這個Map put一個對象,而這個map屬於類的維度,因此不會被GC回收。這就致使了大量的new的對象不被回收。

  (9)優化代碼,若是經過這個流程沒法解決問題,或者無法優化代碼,那麼走最後一步:GC調優:

12  GC調優流程

(1)GC日誌介紹

  YoungGC日誌解釋以下:

  

  FullGC日誌解釋以下:

  

(2)查看GC 日誌 

    1768.617: [GC [PSYoungGen: 1313280K->31072K(1341440K)] 3990240K->2729238K(4137984K), 0.0992420 secs] [Times: user=0.36 sys=0.01, real=0.10 secs]

    1770.525: [GC [PSYoungGen: 1316704K->27632K(1345536K)] 4014870K->2750306K(4142080K), 0.0552640 secs] [Times: user=0.20 sys=0.00, real=0.06 secs]

    [Full GC [PSYoungGen: 47079K->0K(1350144K)] [ParOldGen: 2780532K->191662K(2796544K)] 2827611K->191662K(4146688K)

    [PSPermGen: 60530K->60530K(524288K)],3.4921610 secs] [Times: user=13.39 sys=0.08, real=3.49 secs]

  日誌介紹:

    PSYoungGen: 1313280K->31072K(1341440K)]

      格式爲[PSYoungGen: a->b(c)]。PSYoungGen表示新生代使用的是多線程垃圾收集器Parallel Scavenge。a爲GC前新生代已佔用空間,b爲GC後新生代已佔用空間。新生代又細分爲一個Eden區和兩個Survivor區,Minor GC以後Eden區爲空,b就是Survivor中已被佔用的空間。括號裏的c表示整個新生代的大小。

    3990240K->2729238K(4137984K)

      格式爲x->y(z)。x表示GC前堆的已佔用空間,y表示GC後堆已佔用空間,z表示堆的總大小。

      由新生代和Java堆佔用大小能夠算出年老代佔用空間,此例中就是4137984K-1341440K=2796544k=2731M。

    [Times: user=0.36 sys=0.01, real=0.10 secs]

      提供cpu使用及時間消耗,user是用戶態消耗的cpu時間,sys是系統態消耗的cpu時間,real是實際的消耗時間。

    分析上述日誌,能夠看出兩個問題:

      1. 每次Minor GC,晉升至老年代的對象體積較大,平均爲20m+(2750306K-2729238K=21068K=20.57M),這致使老年代佔用持續升高,Full GC頻率較高,直觀現象是內存佔用一直升高;

      2. Full GC的時間很長,上面看到的是3.49 secs,這致使ull FGC開銷很大,直觀現象是CPU佔用一直上升,達到100%;

    於是調優思路很明確:

       1. 減小每次Young GC晉升到老年代的對象大小;

       2. 儘量的減小每次Full GC的時間開銷;

(3)進行了以下的嘗試

  一. 新生代使用默認的Parallel Scavenge GC,可是加入以下參數 :

    -Xmn1350m -XX:-UseAdaptiveSizePolicy  -XX:SurvivorRatio=6

  調優思路:

    Young GC每次晉升到Old Gen的內容較多,而這極可能是由於Parallel Scavenge垃圾收集器會動態的調整JVM的Eden 和Survivor區,致使Survior空間太小,致使更多對象進入老年代。 

    -Xmn1350m設置堆內新生代的大小。經過這個值能夠獲得老生代的大小:-Xmx減去-Xmn。

    -XX:-UseAdaptiveSizePolicy表示關閉動態調全年輕代區大小和相應的Survivor區比例, -XX:SurvivorRatio=6表示Eden去和Suvior區的大小比例爲6:2:2

  調優效果:

     

  能夠看到調優後full gc頻率大爲減小(由4min一次--->變爲30h一次),同時由於少了頻繁調整new gen的開銷,ygc耗時也略微減小了。

  遺留問題:

    雖然Full GC頻率大爲下降,可是每次Full GC的耗時仍是同樣,500ms+~2000ms

  二. 老年代改用CMS GC,加入jvm參數以下(原來的配置不變):

    -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+UseCMSInitiatingOccupancyOnly 

    -XX:CMSInitiatingOccupancyFraction=70

  調優思路:

    老年代使用CMS GC,啓用碎片整理,下降Full GC的耗時,此時新生代會默認使用ParNew GC收集器

     調優效果:

       

  Oldgen GC開銷仍是較大,雖然比ps gc略好,並且經過gc日誌發現,主要耗時都是在remark的rescan階段

  91832.767: [CMS-concurrent-mark-start]
  91834.022: [CMS-concurrent-mark: 1.256/1.256 secs] [Times: user=4.06 sys=0.94, real=1.25 secs]
  91834.022: [CMS-concurrent-preclean-start]
  91834.040: [GC91834.040: [ParNew: 1091621K->50311K(1209600K), 0.0469420 secs] 3059979K->2018697K(4021504K), 0.0473540 secs] [Times: user=0.16   sys=0.01, real=0.05 secs]
  91834.123: [CMS-concurrent-preclean: 0.051/0.101 secs] [Times: user=0.31 sys=0.05, real=0.10 secs]
  91834.123: [CMS-concurrent-abortable-preclean-start]
  91834.900: [CMS-concurrent-abortable-preclean: 0.769/0.777 secs] [Times: user=2.36 sys=0.53, real=0.78 secs]
  91834.903: [GC[YG occupancy: 595674 K (1209600 K)]91834.904: [Rescan (parallel) , 0.6762340 secs]91835.580: [weak refs processing, 0.0728400 secs]91835.653: [scrub string table, 0.0009380 secs] [1 CMS-remark: 1968386K(2811904K)] 2564060K(4021504K), 0.7555510 secs] [Times: user=2.73 sys=0.03, real=0.76 secs]
  91835.659: [CMS-concurrent-sweep-start]

  三. 下降remark的時間開銷,加入參數:-XX:+CMSScavengeBeforeRemark

  調優思路:

    一般狀況下進行remark會先對new gen進行一次掃描,並且這個開銷佔比挺大,因此加上這個參數,在remark以前強制進行一次Young GC。

三  垃圾收集器介紹  

1  Serial、SerialOld(-XX:+UseSerialGC)

  Serial收集器是一個串行收集器。在JDK1.3以前是Java虛擬機新生代收集器的惟一選擇。目前也是ClientVM下ServerVM 4核4GB如下機器默認垃圾回收器。Serial收集器並非只能使用一個CPU進行收集,而是當JVM須要進行垃圾回收的時候,需暫停全部的用戶線程,直到回收結束。

  使用算法:新生代複製算法、老年代標記-整理算法;垃圾收集的過程當中會Stop The World(服務暫停)

   

  JVM中文名稱爲Java虛擬機,所以它像一臺虛擬的電腦在工做,而其中的每個線程都被認爲是JVM的一個處理器,所以圖中的CPU0、CPU1實際上爲用戶的線程,而不是真正的機器CPU。

  Serial收集器雖然是最老的,可是它對於限定單個CPU的環境來講,因爲沒有線程交互的開銷,專心作垃圾收集,因此它在這種狀況下是相對於其餘收集器中最高效的。

  SerialOld是Serial收集器的老年代收集器版本,它一樣是一個單線程收集器,這個收集器目前主要用於Client模式下使用。若是在Server模式下,它主要還有兩大用途:一個是在JDK1.5及以前的版本中與Parallel Scavenge收集器搭配使用,另一個就是做爲CMS收集器的後備預案,若是CMS出現Concurrent Mode Failure,則SerialOld將做爲後備收集器。

2  ParNew(-XX:+UseParNewGC)

  ParNew其實就是Serial收集器的多線程版本。除了Serial收集器外,只有它能與CMS收集器配合工做。

  使用算法:標記-複製算法

  參數控制:-XX:+UseParNewGC  ParNew收集器

       -XX:ParallelGCThreads 限制線程數量

  

  ParNew是許多運行在Server模式下的JVM首選的新生代收集器。可是在單CPU的狀況下,它的效率遠遠低於Serial收集器,由於線程切換須要消耗時間,因此必定要注意使用場景。

3  Parallel Scavenge(-XX:+UseParallelGC)

  Parallel Scavenge又被稱爲吞吐量優先收集器,和ParNew 收集器相似,是一個新生代並行收集器。目前是默認垃圾回收器。

  使用算法:複製算法

  Parallel Scavenge收集器的目標是達到一個可控件的吞吐量,所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)。若是虛擬機總共運行了100分鐘,其中垃圾收集花了1分鐘,那麼吞吐量就是99% 。

4  ParallelOld(-XX:+UseParallelOldGC)

  ParallelOld是並行收集器,和SerialOld同樣,ParallelOld是一個老年代收集器,是老年代吞吐量優先的一個收集器。這個收集器在JDK1.6以後纔開始提供的,在此以前,Parallel Scavenge只能選擇Serial Old來做爲其老年代的收集器,這嚴重拖累了Parallel Scavenge總體的速度。而ParallelOld的出現後,「吞吐量優先」收集器才名副其實!

  使用算法:標記 - 整理算法

  

  在注重吞吐量與CPU數量大於1的狀況下,均可以優先考慮Parallel Scavenge + ParalleloOld收集器。

5  CMS (-XX:+UseConcMarkSweepGC)

  CMS是一個老年代收集器,全稱 Concurrent Low Pause Collector,是JDK1.4後期開始引用的新GC收集器,在JDK1.五、1.6中獲得了進一步的改進。CMS是對於響應時間的重要性需求大於吞吐量要求的收集器對於要求服務器響應速度高的狀況下,使用CMS很是合適。當CMS進行GC失敗時,會自動使用Serial Old策略進行GC。開啓CMS回收器,新生代會默認使用ParNew GC回收器

  CMS的一大特色,就是用兩次短暫的暫停來代替串行或並行標記整理算法時候的長暫停。

  使用算法:標記 - 清理

  CMS的執行過程以下:

  a)初始標記(STW initial mark)

    在這個階段,須要虛擬機停頓正在執行的應用線程,官方的叫法STW(Stop Tow World)。這個過程從GC Roots掃描直接關聯的對象,並做標記。這個過程會很快的完成。

  b)併發標記(Concurrent marking)

    這個階段緊隨初始標記階段,在「初始標記」的基礎上繼續向下追溯標記。注意這裏是併發標記,表示用戶線程能夠和GC線程一塊兒併發執行,這個階段不會暫停用戶的線程

  c)併發預清理(Concurrent precleaning)

    這個階段仍然是併發的,JVM查找正在執行「併發標記」階段時候進入老年代的對象(可能這時會有對象重新生代晉升到老年代,或被分配到老年代)。經過從新掃描,減小在一個階段「從新標記」的工做,由於下一階段會STW。

  d)從新標記(STW remark)

    這個階段會再次暫停正在執行的應用線程,從新重根對象開始查找並標記併發階段遺漏的對象(在併發標記階段結束後對象狀態的更新致使),並處理對象關聯。這一次耗時會比「初始標記」更長,而且這個階段能夠並行標記。

  e)併發清理(Concurrent sweeping)

    這個階段是併發的,應用線程和GC清除線程能夠一塊兒併發執行。

  f)併發重置(Concurrent reset)

    這個階段仍然是併發的,重置CMS收集器的數據結構,等待下一次垃圾回收。

CMS的缺點:

  一、內存碎片。因爲使用了 標記-清理 算法,致使內存空間中會產生內存碎片。不過CMS收集器作了一些小的優化,就是把未分配的空間彙總成一個列表,當有JVM須要分配內存空間的時候,會搜索這個列表找到符合條件的空間來存儲這個對象。可是內存碎片的問題依然存在,若是一個對象須要3塊連續的空間來存儲,由於內存碎片的緣由,尋找不到這樣的空間,就會致使Full GC。

  二、須要更多的CPU資源。因爲使用了併發處理,不少狀況下都是GC線程和應用線程併發執行的,這樣就須要佔用更多的CPU資源,也是犧牲了必定吞吐量的緣由。

  三、須要更大的堆空間。由於CMS標記階段應用程序的線程仍是執行的,那麼就會有堆空間繼續分配的問題,爲了保障CMS在回收堆空間以前還有空間分配給新加入的對象,必須預留一部分空間。CMS默認在老年代空間使用68%時候啓動垃圾回收。能夠經過-XX:CMSinitiatingOccupancyFraction=n來設置這個閥值。

   參數控制:

    -XX:+UseConcMarkSweepGC  使用CMS收集器

    -XX:+ UseCMSCompactAtFullCollection Full GC後,進行一次碎片整理;整理過程是獨佔的,會引發停頓時間變長

    -XX:+CMSFullGCsBeforeCompaction  設置進行幾回Full GC後,進行一次碎片整理

    -XX:ParallelCMSThreads  設定CMS的線程數量(通常狀況約等於可用CPU數量)

   

6  G1收集器

  G1是目前技術發展的最前沿成果之一,HotSpot開發團隊賦予它的使命是將來能夠替換掉JDK1.5中發佈的CMS收集器。與CMS收集器相比G1收集器有如下特色:

  1. 空間整合,G1收集器採用標記整理算法,不會產生內存空間碎片。分配大對象時不會由於沒法找到連續空間而提早觸發下一次GC。

  2. 可預測停頓,這是G1的另外一大優點,下降停頓時間是G1和CMS的共同關注點,但G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲N毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已是實時Java(RTSJ)的垃圾收集器的特徵了。

  上面提到的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。使用G1收集器時,Java堆的內存佈局與其餘收集器有很大差異,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔閡了,它們都是一部分(能夠不連續)Region的集合。

   

  G1的新生代收集跟ParNew相似,當新生代佔用達到必定比例的時候,開始出發收集。和CMS相似,G1收集器收集老年代對象會有短暫停頓。

  收集步驟:

  一、標記階段,首先初始標記(Initial-Mark),這個階段是停頓的(Stop the World Event),而且會觸發一次普通Mintor GC。對應GC log:GC pause (young) (inital-mark)

  二、Root Region Scanning,程序運行過程當中會回收survivor區(存活到老年代),這一過程必須在young GC以前完成。

  三、Concurrent Marking,在整個堆中進行併發標記(和應用程序併發執行),此過程可能被young GC中斷。在併發標記階段,若發現區域對象中的全部對象都是垃圾,那個這個區域會被當即回收(圖中打X)。同時,併發標記過程當中,會計算每一個區域的對象活性(區域中存活對象的比例)。

   

  四、Remark, 再標記,會有短暫停頓(STW)。再標記階段是用來收集 併發標記階段 產生新的垃圾(併發階段和應用程序一同運行);G1中採用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

  五、Copy/Clean up,多線程清除失活對象,會有STW。G1將回收區域的存活對象拷貝到新區域,清除Remember Sets,併發清空回收區域並把它返回到空閒區域鏈表中。

   

  六、複製/清除過程後。回收區域的活性對象已經被集中回收到深藍色和深綠色區域。

   

參考

一、jvm優化—— 圖解垃圾回收  https://my.oschina.net/u/1859679/blog/1548866

 二、GC算法 垃圾收集器  https://www.cnblogs.com/ityouknow/p/5614961.html

三、經過 jstack 與 jmap 分析一次線上故障  http://www.importnew.com/28916.html

四、性能調優-------(三)1分鐘帶你入門JVM性能調優  https://blog.csdn.net/wolf_love666/article/details/79787735

五、JVM相關  https://blog.csdn.net/wolf_love666/article/details/85712922

六、JVM命令大全  https://www.cnblogs.com/ityouknow/p/5714703.html

七、JVM調優之---一次GC調優實戰  http://www.cnblogs.com/onmyway20xx/p/6626567.html

八、從實際案例聊聊Java應用的GC優化  https://tech.meituan.com/2017/12/29/jvm-optimize.html

相關文章
相關標籤/搜索