JVM系列之第一篇:調優實戰總結

前言

   本篇文章主要在狸貓技術窩中有關JVM中調優的一些實戰基礎上進行總結,能夠算是本身的一篇學習總結。主要以目前主流的兩種垃圾回收組合方式,ParNew +CMS及G1垃圾回收器爲基礎,梳理下調優思路、GC日誌如何閱讀及引起OOM的區域和緣由。html

ParNew +CMS組合

   ParNew通常用在新生代的垃圾回收器,CMS用在老年代的垃圾回收器,他們都是多線程併發機制,性能更好,如今通常是線上生產系統的標準組合。java

Minor GC

   Minor GC又稱年輕代垃圾回收,年輕代垃圾回收主要採用複製算法,因爲年輕代對象大都「朝生夕死」,爲下降內存使用率瓶頸,設置了Eden區和2個Survior區,1個Eden區佔80%內存空間,每一塊Survivor區各佔10%內存空間。當前Minor GC主要採用ParNew垃圾回收器。程序員

何時嘗試觸發Minor GC?

   新生代剩餘內存空間放不下新對象,此時須要觸發GC。算法

觸發Minor GC狀況有:數組

  • 新生代現有存活對象小於老年代剩餘內存,即老年代空間足以支撐可能晉升的對象
  • 狀況1不成立時,設置空間擔保而且能夠擔保成功(當前JDK版本下都有默認開啓了空間擔保),即老年代空間大於歷次Minor GC後進入老年代的平均大小。

Minor GC以前作了什麼?

    判斷老年代的可用內存是否已經小於了新生代的所有對象大小了,若是是,判斷-XX:HandlePromotionFailure參數是否設置,若是有這個參數,那麼就會繼續嘗試進行下一步判斷:看老年代的內存大小,是否大於以前每一次Minor GC後進入老年代的對象的平均大小。若是判斷失敗,或者空間分配擔保沒有設置,就會直接觸發一次FullGC,對老年代進行垃圾回收,儘可能騰出來一些空間,而後再執行Minor GC。bash

Minor GC結果

1.Minor GC事後,剩餘的存活對象,小於Survivor區域大小,存活對象進入Survivor區。多線程

2.Minor GC事後,存活對象大於Survivor區域大小,小於老年代可用空間大小,直接進入老年代併發

3.Minor GC事後,存活對象大於Survivor區域大小,也大於老年代可用空間大小,此時,就會發生Handle Promotionoracle

Old GC

   Old GC又稱老年代垃圾回收,針對老年代進行垃圾的回收器主要有Serial Old及CMS。若是Minor GC後存活對象大於老年代裏的剩餘空間,這個時候觸發一次Old GC, 將老年代裏的沒人引用的對象給回收掉,而後纔可能讓Minor GC事後剩餘的存活對象進入老年代裏面。app

對象如何進入老年代?

   當對象躲過15次Minor GC後、符合動態對象判斷規則、大對象及Minor GC後的對象太多沒法放入Survivor區域等場景,都會觸發對象進入老年代,下面將逐一分析每種場景。

1.躲過15次GC以後進入老年代
  • 對象每次在新生代躲過一次GC被轉移到一塊Survivor區域只可以,此時他的年齡就會增加一歲
  • 默認的設置下,當對象的年齡達到15歲的時候,也就是躲過15次GC的時候,他就會轉移到老年代裏區。具體多少歲進入老年代,能夠經過JVM參數-XX:MaxTenuringThreshold來設置,默認是15歲。
2.動態對象年齡判斷
  • 若是一次新生代gc事後,發現當前放對象的Survior區域裏,幾個年齡的對象的總大小大於了這塊Survior區域的內存大小的50%,好比說age1 + age2 + age3的對象大小總和,超過了Survivor區域的50%,那麼就把age3年齡以上的對象都放入老年代。
  • 動態年齡判斷規則,也會讓一些新生代的對象進入老年代。

   不管15次GC以後進入老年代,仍是動態年齡判斷規則,都是但願可能長期存活的對象,儘早進入老年代。

3.大對象直接進入老年代
  • 經過參數-XX:PretenureSizeThreshold能夠設置對象直接進入老年代的閥值,能夠把他的值設置爲字節數,好比1048576字節,就是1MB。
  • 若是建立一個大於這個大小的對象,好比一個超大的數組或者別的大對象,此時就直接把這個大對象放到老年代裏,壓根不會通過新生代。
4.Minor GC後的對象太多沒法放入Survivor區域

   這裏須要考慮一個問題,就是老年代空間不夠放這些對象。若是老年代的內存大小是大於新生代全部對象的,此時就能夠對新生代觸發一次Minor GC,由於即便全部對象都存活,Survivor區放不下了,也能夠轉移到老年代去。若是Minor GC前,發現老年代的可用內存已經小於新生代的所有大小了,這個時候若是Minor GC後新生代的對象所有存活下來,都轉移到老年代去,老年代空間不夠,理論上,是有這種可能的。因此假如Minor GC以前,發現老年代的可用內存已經小於了新生代的所有對象大小了,就會看一個-XX:HandlePromotionFailure的參數是否設置了。若是有這個參數,那麼就會繼續嘗試進行下一步判斷:看老年代的內存大小,是否大於以前每一次Minor GC後進入老年代的對象的平均大小。若是判斷失敗,或者空間分配擔保沒有設置,就會直接觸發一次FullGC,對老年代進行垃圾回收,儘可能騰出來一些空間,而後再執行Minor GC。

   若是老年代回收後,仍然沒有足夠的空間存放Minor GC事後的剩餘存活對象,那麼此時就會致使OOM內存溢出

老年代的垃圾回收算法是什麼樣的?

標記整理算法

   標記老年代當前存活對象,這些對象多是零散分佈在內存中,而後將這些存活對象在內存裏移動,將存活對象儘可能挪動到一邊,將存活對象集中放置,避免回收後出現過多內存碎片。而後一次行把垃圾對象都回收掉。

標記清除算法

   先經過追蹤GC Roots的方法,看看各個對象是否被GC Roots給引用了,若是是的話,那就是存活對象,不然就是垃圾對象。先將垃圾對象標記出來,而後一次性把垃圾對象都回收掉,這種方法其實最大的問題就是會形成不少內存碎片。

老年代爲何不採用複製算法?

   老年代存活對象太多了,若是採用複製算法,每次挪動可能90%的存活對象,這就不合適了。因此採用先把存活對象挪到一塊兒緊湊一些,而後回收垃圾對象的方式。

老年代回收場景

1.Minor GC以前,老年代內存空間小於歷次Minor GC後升入老年代對象的平均大小,判斷Minor GC有風險,可能就會提早觸發老年代GC回收老年代垃圾對象。

2.Minor GC後的對象太多了,都要升入老年代,發現空間不足,觸發一次老年代的Old GC。

3.設置了-XX:CMSInitiatingOccuancyFaction參數,好比設置爲92%,好比說老年代空間使用超過92%了,此時就會自行觸發Old GC.

CMS回收過程

   CMS在執行一次垃圾回收的過程一共分爲4個階段。

1.初始標記

   標記出來全部GC Roots直接引用的對象,會讓系統的工做線程所有中止,進入「Stop the World」狀態。

2.併發標記

   追蹤老年代全部存活對象,老年代存活對象不少,這個過程就會很慢。

3.從新標記

   這個過程會標記整堆,包括年輕代和老年代。

4.併發清理

   找到零零散散分散再各個地方的垃圾對象,速度較慢。最後可能還要執行一次內存碎片整理,把大量的存活對象挪在一塊兒,空出來連續空間,這個過程仍然要STW,那就更慢了。

concurrent mode failure是什麼?

   CMS垃圾收集器特有的錯誤,CMS的垃圾清理和引用線程是並行進行的,若是在並行清理的過程當中老年代的空間不足以容納應用產生的垃圾,則會拋出「concurrent mode failure」。

concurrent mode failure影響

  老年代的垃圾收集器從CMS退化爲Serial Old,全部應用線程被暫停,停頓時間變長。

可能緣由及方案
  • 緣由1:CMS觸發太晚

    方案:將-XX:CMSInitiatingOccupancyFraction=N調小;

  • 緣由2:空間碎片太多

    方案:開啓空間碎片整理,並將空間碎片整理週期設置在合理範圍;

-XX:+UseCMSCompactAtFullCollection (空間碎片整理) -XX:CMSFullGCsBeforeCompaction=n,執行多少次Full GC以後再執行一次內存碎片整理工做,默認是0,意思就是每次Full GC以後都會進行一次內存整理。

  • 緣由3:垃圾產生速度超過清理速度 晉升閾值太小; Survivor空間太小,致使溢出; Eden區太小,致使晉升速率提升; 存在大對象;

爲何說老年代的Full GC要比新生代的Minor GC慢?

  • 新生代執行速度快,由於直接從GC Roots出發就追蹤哪些對象是活的便可,新生代存活對象是不多的,這個速度是很快的,不須要追蹤多少對象,最後直接把存活對象放入Survivor中,就一次性直接回收Eden和以前使用的Survivor了。

  • 在老年代回收併發標記階段,他須要追蹤全部存活對象,老年代存活對象不少,這個過程就很慢。

  • 從新標記這個過程要標記整堆,併發清理階段並非一次性回收一大片內存,而是找到零零散散在各個地方的垃圾對象,速度也很慢。

  • 最後還須要執行一次內存碎片整理,把大量的存活對象給挪在一塊兒,空來聯繫內存空間,這個過程還得STW。

  • 併發清理時,若是剩餘內存空間不足以存放要進入老年代的對象,會引起」Concurrent Mode Failure「問題,這時會採用」Serial Old「垃圾回收器,STW以後會重新進行一次Old GC,這就更耗時了。

G1垃圾回收器

   JDK8後出現了G1垃圾回收器,經過-XX:+UseG1GC來指定G1垃圾回收器,是當下比較先進的垃圾回收器。G1能夠作到讓你來設定垃圾回收對系統的影響,他本身經過把內存拆分爲大量小Region,以及追蹤每一個Region中能夠回收的對象大小和預估時間,最後在垃圾回收的時候,儘可能把垃圾回收對系統形成的影響控制在你指定的時間範圍內,同時在有限的時間內儘可能回收儘量躲的垃圾對象。

G1垃圾回收器特色

  • 把java堆內存分爲多個大小相等的Region
  • 邏輯上,也會有新生代和老年代的概念
  • 能夠設置一個垃圾回收的預期停頓時間

region設置問題

  • -XX:+UseG1GC來指定G1垃圾回收器,此時會自動用堆大小除以2048,jvm最多能夠有2048個Region,而後Region的大小必須是2的倍數,好比說1MB、2MB、4MB之類的。能夠經過-XX:G1HeapRegionSize指定。
  • 默認新生代對堆內存的佔比是5%,也能夠經過-XX:G1NewSizePercent來設置新生代初始佔比的,其實維持這個默認值便可。系統運行中,JVM其實會不停地給新生代增長更多的Region,可是新生代佔比最多不超過60%,能夠經過-XX:G1MaxNewSizePercent來設置。

G1新生代是如何回收的?

   新生代也是有eden和survivor劃分的,也是經過-XX:SurvivorRatio能夠劃分eden和survivor各自大小。觸發垃圾回收的機制也是相似的,隨着不停地在新生代eden對應的region中放對象,jvm會不停地給新生代加入更多的region,直到新生代佔堆大小的最大比例60%,好比說新生代1200個region了,裏面的eden可能佔據了1000個region,每一個survivor是100個region,並且eden區還佔滿了對象,這時會觸發新生代gc,g1採用以前說過的複製算法進行垃圾回收,進入一個STW狀態,併發eden對應的region中的存活對象放入S1的region中,接着回收掉eden對應的region中的垃圾對象。    g1是能夠設定目標gc停頓時間的,也就是g1執行gc的時候最多可讓系統停頓多長時間,能夠經過-XX:MaxGCPauseMills參數來設定,默認值是200ms。

G1對象何時重新生代進入老年代呢?

  • 對象在新生代躲過來不少次的垃圾回收,達到來必定的年齡了,-XX:MaxTenuringThreshold參數能夠設置這個年齡,他就會進入老年代
  • 動態年齡斷定規則,若是一旦發現某次新生代GC事後,存活對象超過了Survivor的50%,此時判斷,好比年齡爲1歲,2歲,3歲,4歲的對象的大小總和超過了Survivor的50%,此時4歲以上的對象所有進入老年代,這就是動態年齡斷定規則

大對象Rregion

   對於G1內存模型來講,G1提供了專門的Region來存放大對象,而不是讓大對象進入老年代的Region中。在G1中,大對象的斷定規則就是一個大對象超過了一個region的50%,好比一個region是2MB,只要一個大對象超過了1MB,就會被放入大對象專門的region中,並且一個大對象若是太大,可能會橫跨多個region來存放。在新生代、老年代在回收的時候,會順帶着大對象一塊兒回收。

發新生代+老年代混合垃圾回收

   G1有一個參數,-XX:InitiatingHeapOccupancyPercent,默認值是45%,若是老年代佔據了堆內存45%的Region的時候,此時就會嘗試觸發一個新生代+老年代一塊兒回收的混合回收階段。

G1垃圾回收過程

  • 初始標記 僅僅標記一下GC Roots直接能引用的對象,這個過程速度是很快的,須要進入STW狀態。
  • 併發標記 這個階段會容許系統程序的運行,同時進行GC Roots追蹤,從GC Roots開始追蹤全部的存活對象,併發階段仍是很耗時的,由於要追蹤所有的存活對象。可是這個階段是能夠和系統程序併發運行,因此對系統程序的影響不太大。並且JVM會對併發標記階段對對象作出的一些修改記錄起來,好比哪一個對象被新建了,哪一個對象失去了引用。
  • 最終標記 這個階段會進入STW,系統程序是禁止運行的,可是會根據併發標記階段記錄的那些對象修改,最終標記一下有哪些存活對象,有哪些是垃圾對西那個。
  • 混合回收 這個階段會計算老年代中每一個Region中的存活對象數量,存活對象的佔比,還有執行垃圾回收的預期性能和效率。接着會中止系統程序,而後盡心盡力儘快進行垃圾回收,此時會選擇部分Region進行回收,由於必須讓垃圾回收的停頓時間控制在咱們指定的範圍內。混合回收,會重新生代、老年代、大對象裏各自挑選一些Region,保證指定的時間(好比200ms)回收儘量多的垃圾。

G1垃圾回收器的一些參數

  • -XX:G1MixedGCCountTarget,就是在一次混合回收的過程當中,最後一個階段執行幾回混合回收,默認值是8次。意味着最後一個階段,先中止系統運行,混合回收一些Region,再恢復系統運行,接着再次禁止系統運行,混合回收一些Region,反覆8次。反覆回收屢次的意義在於,儘量讓系統不要停頓時間過長,能夠在屢次回收的間隙,也運行一下。
  • -XX:G1HeapWastePercent,默認值是5%,在混合回收的時候,對Region回收都是基於複製算法進行的,都是把要回收的Region裏的存活對象放入其餘Region,而後這個Region中的垃圾對象所有清理掉。這樣的話在回收過程不斷空出來新的Region,一旦空閒出來的Region數量達到了堆內存的5%,此時就會當即中止混合回收,意味着本次混合回收就結束了,也就是說進行4次混合回收後,發現空閒Region達到了5%,就不會進行後續的混合回收。從這裏也能看出G1總體是基於複製算法進行Region垃圾回收的,不會出現內存碎片的問題,不須要像CMS那樣標記-清理以後,再進行內存碎片的整理。
  • -XX:G1MixedGCLiveThresholdPercent,默認值85%,當一個Region的存活對象多餘85%,這個時候就不會回收。由於copy到別的Region的成本也是很高的。

回收失敗時的Full GC

若是在進行Mixed回收的時候,不管是年輕代仍是老年代都基於複製算法進行回收,都要把各個Region的存活對象copy到別的Region裏去,萬一出現copy的過程當中發現沒有空閒Region能夠承載本身的存活對象了,就會觸發一次失敗。一旦失敗,立馬就會切換爲中止系統程序,而後採用單線程進行標記、清理和壓縮整理,空閒出來一批Region,這個過程是極慢極慢的。

如何對JVM參數進行優化?

G1垃圾回收器

G1新生代優化

  • 給整個JVM堆區域足夠的內存
  • 合理設置-XX:MaxGCPauseMills參數,參數設置小,每次gc停頓時間可能特別短,gc頻率提升。參數設置過大,可能G1會運去不停地在新生代分配新的對象,而後積累了不少對象,再一次性回收幾百個Region。

mixed gc優化

   老年代在堆內存裏佔比超過45%觸發mixed gc 優化的思路仍是儘可能避免對象過快進入老年代,儘可能避免頻繁觸發mixed gc。優化的核心點是:避免老年代達到InitiatingHeapOccupancyPercent設置的值,即避免對象過快進入老年代。

  • 1.讓垃圾對象儘可能在新生代就被回收掉,儘可能讓短命對象不進老年代。也就是合理設置—XX:SurvivorRatio值。
  • 2.提升觸發mixed gc時InitiatingHeapOccupancyPercent的值,這樣mixed gc機率下降,但這樣作會加大gc回收時計算負擔。
  • 3.合理調節-XX:MaxGCPauseMills參數的值,保證他的新生代gc別太頻繁的同時,還得考慮每次gc事後的存活對象有多少,避免存活對象太多,快速進入老年代,頻發觸發mixed gc。

parnew+cms垃圾回收器

   合理分配堆內存,經過調整s區和e區大小來控制進入老年代對象速度,從而減小頻繁old gc。

兩種垃圾回收器對比

  1. parnew+cms和g1回收的最大區別是是否會進行整堆回收。
  2. g1能夠設置預估停頓時間,適用於低延遲應用
  3. g1從總體上看採用複製算法,適合會產生大量碎片的應用。
  4. parnew+cms回收器比較適合內存小,對象在新生代存活週期短的應用;g1適合內存較大的計算應用,由於整堆回收會比較耗時。

gc日誌解讀

   學會解讀gc日誌能夠很好地分析堆使用狀況,是進行調優及解決頻繁full gc必備技能。下面咱們以parnew+cms垃圾回收器爲例,分析下gc日誌。

新生代gc日誌

public class JvmTest {

    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];
    }
}
複製代碼

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

  • 0.268表示系統運行268毫秒後觸發了本次gc
  • GC (Allocation Failure) 表示對象分配失敗觸發GC
  • [ParNew: 4030K->512K(4608K), 0.0015734 secs] ParNew標示年輕代GC,4608k標示年輕代可用空間4.5MB, 即eden 區 + 1個survivor區大小。4030K->512K表示GC以前使用了4030K,GC以後只有512K的對象。
  • 4030K->574K(9728K);4030K表示gc前整堆的使用了4030K,gc後使用了574K,整堆大小是9728K。
  • 0.0015734 secs 表示此次gc耗時1.5ms。 注意:在GC以前,Eden區裏放裏3個1MB的數組,一共3MB,也就是3072KB的對象,但爲裏存儲這個數組,JVM 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]

JVM退出時打印當前堆內存的使用狀況,分析以下:

  • par new generation total 4608K, used 2601K;說明ParNew垃圾回收器負責的年輕代共有4608KB可用內存,目前使用 了2601KB。
  • from space,100% used;代表以前gc後存活下來的512KB的未知對象將from space佔滿
  • to space,0% used; from space與to space兩個區域不能同時被使用,其中一個存放前一次gc存活對象後,另外一個就是閒置的。
  • concurrent mark-sweep generation total 5120K, used 62K;代表使用Concurrent Mark-Sweep垃圾回收器,即CMS垃圾回收器,老年代內存空間一共是5MB,此時使用了62KB的空間。

解析Metaspace的使用狀況

   Metaspace是從JVM進程的虛擬地址空間中分離出來的,用以保存類元數據。JVM在啓動時根據-XX:MetaspaceSize保留初始大小,該大小具備特定於平臺的默認值。

   Metaspace由一個或多個虛擬空間組成。虛擬空間是由操做系統得到的連續地址空間。他們是按需分配的。在分配時,虛擬空間預留(reserves)了操做系統的內存,但尚未提交。Metaspace reserved是全部虛擬空間的總大小。虛擬空間中的分配單元是Metachunk,當從虛擬空間分配新塊時,相應的內存將committed, Metaspace committed是全部塊的總大小。 從 docs.oracle.com/javase/8/do… 中能夠對used,committed,reserved,capacity有了概述解釋;

In the line beginning with Metaspace, the used value is the amount of space used for loaded classes. The capacity value is the space available for metadata in currently allocated chunks. The committed value is the amount of space available for chunks. The reserved value is the amount of space reserved (but not necessarily committed) for metadata. The line beginning with class space line contains the corresponding values for the metadata for compressed class pointers.

  • used表示加載的類的空間量,capacity表示當前分配塊(非空閒塊)的空間,其小於commited的空間量;committed表示當從虛擬空間分配新塊時,相應的內存將會被提交,即已經申請提交的可用分配塊,其大小要大於used的;reserved指元空間的總大小,空間被分紅塊,每一個塊只能包含與某一個類加載器關聯的類元數據。關於這幾個參數的定義解釋能夠參考一篇文章: www.jianshu.com/p/cd34d6f3b…
  • class space是指實際上被用於放class的那塊內存的和,關於這塊,從此會進行詳細分析。

Full GC日誌分析

   Full GC有如下表象,如機器CPU負載太高,系統沒法處理請求或者處理過慢。引發Full GC的緣由有不少,主要有JVM參數設置不合理和代碼層面問題兩大類。JVM參數設置不合理,如新生代堆內存大小設置不合理、Eden與Survivor比例設置不合理,抑或是metaspace設置太小等。代碼層面問題,主要是程序員本身的問題,好比說對外提供查詢接口沒有作限制,一次查詢太多對象;應用中存在頻繁大量導出,且查詢沒有限制條件;代碼中顯示調用gc等。

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];
    }
}
複製代碼

   結合上述配置,咱們能夠發現,數組array1這個大對象會直接進入老年代;以後連續分配了4個數組,其中3個是2MB的數組,1個是128KB的數組,會所有進入eden區。當再分配array6時,會發現eden區空間不夠,須要觸發一次minor gc,可是因爲array2,array3,array4,array5都被變量引用了,會直接進入老年代,由於老年代裏已經存在4MB的數據了,難以存放這麼大的數據,所以會觸發一次Full GC。Full GC會對老年代進行Old GC,同時通常會跟一次Young GC關聯,還會觸發一次Metaspace的GC。下面咱們分析下GC日誌。

0.308: [GC (Allocation Failure) 0.308: [ParNew (promotion failed): 7260K->7970K(9216K), 0.0048975 secs]0.314: [CMS: 8194K- >6836K(10240K), 0.0049920 secs] 11356K->6836K(19456K), [Metaspace: 2776K->2776K(1056768K)], 0.0106074 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

Heap

par new generation total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)

eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)

from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)

to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)

concurrent mark-sweep generation total 10240K, used 6836K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)

Metaspace used 2782K, capacity 4486K, committed 4864K, reserved 1056768K class space used 300K, capacity 386K, committed 512K, reserved 1048576K

  • ParNew (promotion failed): 7260K->7970K(9216K), 0.0048975 secs;Eden區回收前有7000多KB的對象,回收以後發現一個都回收不掉,主要因爲上述幾個數組被變量引用了;出現promotion 失敗的緣由主要是Minor GC時,Survivor Space放不下,對象只能放入老年代,而此時老年代也放不下形成的。
  • CMS: 8194K- >6836K(10240K), 0.0049920 secs;代表old gc前老年代對象大小是8194KB,old gc後存活對象大小是6836K,老年代總空間大小是10240K。看到這裏可能會有一個疑惑,爲何old gc前會有8194K的對象呢?這主要是young gc後,往老年代放入了2個2MB的對象。後續繼續存放1個2MB和1個128KB的數組到老年代中,放不下,觸發Full GC。
  • 11356K->6836K(19456K);代表old gc前整堆使用了11356K,old gc後整堆使用了6836K。

下面分析full gc後堆內存的使用狀況

  • par new generation total 9216K, used 2130K;代表Full GC後,剩餘的2MB存放在enden區了。
  • from space 1024K, 0% used;young gc時沒有存活對象對象;
  • to space 1024K, 0% used;to space自己沒有參與到此次gc中,不存在使用情景;
  • concurrent mark-sweep generation total 10240K, used 6836K;代表使用CMS垃圾回收器,新生代中的6836K所有對象進入了老年代;

   儘可能讓每次Young GC後的存活對象⼩於Survivor區域的50%,都留存在年輕代⾥。儘可能別讓對象進 ⼊⽼年代。儘可能減小Full GC的頻率,避免頻繁Full GC對JVM性能的影響。

上線時如何肯定jvm參數?

   系統通過單測、集測及測試環境後,進入預發環境進行壓測,觀察內存使用、Young GC的觸發頻率,Young GC的耗時,每次YoungGC後有多少對象是存活下來的,每次Young GC事後有多少對象進⼊了⽼年代,⽼年代對象增加的速率,Full GC的觸發頻率。

經過ps -ef | grep java獲取java進程pid,利用jstat工具查看gc狀況;

[tian~]$ jstat -gc 2236
 S0C    S1C    S0U    S1U      EC    EU        OC        OU        MC      MU      CCSC    CCSU      YGC     YGCT    FGC     FGCT     GCT   
20480.0 20480.0 269.9 0.0  163840.0 97683.3  319488.0  271892.4  673268.0 661182.8 78048.0 75954.8   508    9.526    18      1.737   11.263
複製代碼
S0C:這是From Survivor區的⼤⼩
S1C:這是To Survivor區的⼤⼩
S0U:這是From Survivor區當前使⽤的內存⼤⼩
S1U:這是To Survivor區當前使⽤的內存⼤⼩
EC:這是Eden區的⼤⼩
EU:這是Eden區當前使⽤的內存⼤⼩
OC:這是⽼年代的⼤⼩
OU:這是⽼年代當前使⽤的內存⼤⼩
MC:這是⽅法區(永久代、元數據區)的⼤⼩
MU:這是⽅法區(永久代、元數據區)的當前使⽤的內存⼤⼩
YGC:這是系統運⾏迄今爲⽌的Young GC次數
YGCT:這是Young GC的耗時
FGC:這是系統運⾏迄今爲⽌的Full GC次數
FGCT:這是Full GC的耗時
GCT:這是全部GC的總耗時
複製代碼

   能夠利用jstat -gc PID 1000 10命令,每隔1s更新出來最新的一行jstat統計信息,一共執行10次統計,觀察每隔一段時間jvm中eden區對象佔用變化。若是系統訪問量較低,能夠適當延長觀察時間長度,這樣就能夠大體推測出每次gc停頓時間長度。如今也有比較好的可視化監測工具如JVisualVM和Cat等。

經常使用GC參數

-Xmx8g -Xms8g -Xmn2g -Xss256k  Xms、Xmx表示堆的大小,Xmn表示年輕代大小,Xss表示線程棧擦小,默認1M
-XX:SurvivorRatio=2 新生代中Eden與Survivor比值,調優的關鍵,也就是調節新生代堆大小及SurvivorRatio的值,儘可能讓新生代垃圾對象存放在Survivor中;
-XX:MetaspaceSize=256m  
-XX:MaxMetaspaceSize=256m 元空間大小
-XX:+UseParNewGC 用並行收集器 ParNew 對新生代進行垃圾回收
-XX:+UseConcMarkSweepGC 併發標記清除收集器 CMS 對老年代進行垃圾回收。
-XX:ParallelGCThreads=2 Young GC工做時的並行線程數
-XX:ParallelCMSThreads=3 CMS GC 工做時的並行線程數
-XX:+CMSParallelRemarkEnabled 並行運行最終標記階段,加快最終標記的速度
-XX:+CMSParallelInitialMarkEnabled 初始階段開啓多線程併發執行,減小STW時間
-XX:+CMSScavengeBeforeRemark 在CMS從新標記階段以前,執行一次Young GC,由於從新標記是整堆標記的,執行一次Young GC,回收調年輕代裏沒人引用的對象,減小掃描對象。
-XX:MaxTenuringThreshold=15 對象重新生代晉升到老年代的年齡閾值(每次 Young GC 留下來的對象年齡加一),默認值15
-XX:+UseCMSCompactAtFullCollection 開啓碎片整理
-XX:CMSFullGCsBeforeCompaction=2 與-XX:+UseCMSCompactAtFullCollection配合使用,表示進行2次Full GC後進行整理
-XX:+UseCMSInitiatingOccupancyOnly 只根據老年代使用比例來決定是否進行CMS
-XX:CMSInitiatingOccupancyFraction=80 設置觸發CMS老年代回收的內存使用率佔比,達到80%時觸發old gc
-XX:+CMSClassUnloadingEnabled 默認開啓,表示開啓 CMS 對元空間的垃圾回收,避免因爲元空間耗盡帶來 Full GC
-XX:-DisableExplicitGC 禁止代碼中顯示調用GC
-XX:+HeapDumpOnOutOfMemoryError OOM時dump內存快照
-verbose:gc 表示輸出虛擬機中GC的詳細狀況
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:/app/log/xxx.log gc文件未知
複製代碼

OOM分析

   發生OOM的區域主要有三塊,一個Metaspace區域,一個是虛擬機棧內存,一個是堆內存空間。

Metaspace內存溢出

   Full GC時,必然會嘗試回收Metaspace區域中的類,固然回收條件是比較苛刻的,如這個類的類加載器先要被回收,類的全部對象實例都要被回收等,一旦Metaspace區域滿類,未必能回收掉裏面不少的類,JVM沒有回收太多空間,隨着程序運行,還要繼續往Metaspace區域中塞入更多的類,直接就會引起內存溢出問題。 引發Metaspace內存溢出的緣由

  • Metaspace設置多小;
  • 大量使用cglib之類的技術動態生成一些類,致使生成的類過多,將Metaspace塞滿,引發內存溢出;

棧內存溢出

   每一個線程的虛擬機棧的大小是固定的,線程調用一個方法,都會將本次方法調用的棧楨壓入虛擬機棧裏,這個棧枕裏是有方法的局部變量的。致使棧內存溢出的主要緣由是出現類遞歸調用。

堆內存溢出

   堆內存溢出主要是eden區不斷有存活對象進入老年代,觸發full gc後發現老年代回收對象較少,老年代仍然有大量存活對象,年輕代仍然有一批對象等着放進老年代,可是放不下,這時候拋出內存溢出異常。 通常來講,引發內存溢出主要有兩種場景:

  • 系統承載⾼併發請求,由於請求量過⼤,致使⼤量對象都是存活的,因此要繼續放⼊新的對象實在是不⾏了,此時就會引起OOM系統崩潰。
  • 系統有內存泄漏的問題,就是莫名其妙弄了不少的對象,結果對象都是存活的,沒有及時取消對他們的引⽤,致使觸發GC仍是⽆法回收,此時只能引起內存溢出,由於內存實在放不下更多對象了。 所以總結起來,⼀般引起OOM,要否則是系統負載過⾼,要否則就是有內存泄漏的問題。
相關文章
相關標籤/搜索