天天看點

探秘Java虛拟機——記憶體管理與垃圾回收(轉)

本文主要是基于Sun JDK 1.6 Garbage Collector(作者:畢玄)的整理與總結,原文請讀者在網上搜尋。

1、Java虛拟機運作時的資料區

​​

探秘Java虛拟機——記憶體管理與垃圾回收(轉)

​​

2、常用的記憶體區域調節參數

-Xms:初始堆大小,預設為實體記憶體的1/64(<1GB);預設(MinHeapFreeRatio參數可以調整)空餘堆記憶體小于40%時,JVM就會增大堆直到-Xmx的最大限制

-Xmx:最大堆大小,預設(MaxHeapFreeRatio參數可以調整)空餘堆記憶體大于70%時,JVM會減少堆直到 -Xms的最小限制

-Xmn:新生代的記憶體空間大小,注意:此處的大小是(eden+ 2 survivor space)。與jmap -heap中顯示的New gen是不同的。整個堆大小=新生代大小 + 老生代大小 + 永久代大小。 

在保證堆大小不變的情況下,增大新生代後,将會減小老生代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。

-XX:SurvivorRatio:新生代中Eden區域與Survivor區域的容量比值,預設值為8。兩個Survivor區與一個Eden區的比值為2:8,一個Survivor區占整個年輕代的1/10。

-Xss:每個線程的堆棧大小。JDK5.0以後每個線程堆棧大小為1M,以前每個線程堆棧大小為256K。應根據應用的線程所需記憶體大小進行适當調整。在相同實體記憶體下,減小這個值能生成更多的線程。但是作業系統對一個程序内的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。一般小的應用, 如果棧不是很深, 應該是128k夠用的,大的應用建議使用256k。這個選項對性能影響比較大,需要嚴格的測試。和threadstacksize選項解釋很類似,官方文檔似乎沒有解釋,在論壇中有這樣一句話:"-Xss is translated in a VM flag named ThreadStackSize”一般設定這個值就可以了。

-XX:PermSize:設定永久代(perm gen)初始值。預設值為實體記憶體的1/64。

-XX:MaxPermSize:設定持久代最大值。實體記憶體的1/4。

3、記憶體配置設定方法

1)堆上配置設定   2)棧上配置設定  3)堆外配置設定(DirectByteBuffer或直接使用Unsafe.allocateMemory,但不推薦這種方式)

4、監控方法

1)系統程式運作時可通過jstat –gcutil來檢視堆中各個記憶體區域的變化以及GC的工作狀态; 

2)啟動時可添加-XX:+PrintGCDetails  –Xloggc:<file>輸出到日志檔案來檢視GC的狀況; 

3)jmap –heap可用于檢視各個記憶體空間的大小;

5)斷代法可用GC彙總

​​

探秘Java虛拟機——記憶體管理與垃圾回收(轉)

​​

一、新生代可用GC

1)串行GC(Serial Copying):client模式下預設GC方式,也可通過-XX:+UseSerialGC來強制指定;預設情況下 eden、s0、s1的大小通過-XX:SurvivorRatio來控制,預設為8,含義 

為eden:s0的比例,啟動後可通過jmap –heap [pid]來檢視。

      預設情況下,僅在TLAB或eden上配置設定,隻有兩種情況下會在老生代配置設定: 

      1、需要配置設定的記憶體大小超過eden space大小; 

      2、在配置了PretenureSizeThreshold的情況下,對象大小大于此值。

      預設情況下,觸發Minor GC時:

      之前Minor GC晉級到old的平均大小 < 老生代的剩餘空間 < eden+from Survivor的使用空間。當HandlePromotionFailure為true,則僅觸發minor gc;如為false,則觸發full GC。

      預設情況下,新生代對象晉升到老生代的規則:

     1、經曆多次minor gc仍存活的對象,可通過以下參數來控制:以MaxTenuringThreshold值為準,預設為15。

     2、to space放不下的,直接放入老生代;

2)并行GC(ParNew):CMS GC時預設采用,也可采用-XX:+UseParNewGC強制指定;垃圾回收的時候采用多線程的方式。

3)并行回收GC(Parallel Scavenge):server模式下預設的GC方式,也可采用-XX:+UseParallelGC強制指定;eden、s0、s1的大小可通過-XX:SurvivorRatio來控制,但預設情況下

以-XX:InitialSurivivorRatio為準,此值預設為8,代表的為新生代大小 : s0,這點要特别注意。

      預設情況下,當TLAB、eden上配置設定都失敗時,判斷需要配置設定的記憶體大小是否 >= eden space的一半大小,如是就直接在老生代上配置設定;

      預設情況下的垃圾回收規則:

      1、在回收前PS GC會先檢測之前每次PS GC時,晉升到老生代的平均大小是否大于老生代的剩餘空間,如大于則直接觸發full GC;

      2、在回收後,也會按照上面的規則進行檢測。

      預設情況下的新生代對象晉升到老生代的規則:

     1、經曆多次minor gc仍存活的對象,可通過以下參數來控制:AlwaysTenure,預設false,表示隻要minor GC時存活,就晉升到老生代;NeverTenure,預設false,表示永不晉升到老生代;上面兩個都沒設定的情冴下,如UseAdaptiveSizePolicy,啟動時以InitialTenuringThreshold值作為存活次數的門檻值,在每次ps gc後會動态調整,如不使用UseAdaptiveSizePolicy,則以MaxTenuringThreshold為準。

     2、to space放不下的,直接放入老生代。

     在回收後,如UseAdaptiveSizePolicy,PS GC會根據運作狀态動态調整eden、to以及TenuringThreshold的大小。如果不希望動态調整可設定-XX:-UseAdaptiveSizePolicy。如希望跟蹤每次的變化情況,可在啟劢參數上增加: PrintAdaptiveSizePolicy。

二、老生代可用GC

1、串行GC(Serial Copying):client方式下預設GC方式,可通過-XX:+UseSerialGC強制指定。

    觸發機制彙總:

   1)old gen空間不足;

   2)perm gen空間不足;

   3)minor gc時的悲觀政策;

   4)minor GC後在eden上配置設定記憶體仍然失敗;

   5)執行heap dump時;

   6)外部調用System.gc,可通過-XX:+DisableExplicitGC來禁止。

2、并行回收GC(Parallel Scavenge): server模式下預設GC方式,可通過-XX:+UseParallelGC強制指定; 并行的線程數為當cpu core<=8 ? cpu core : 3+(cpu core*5)/8或通過-XX:ParallelGCThreads=x來強制指定。如ScavengeBeforeFullGC為true(預設值),則先執行minor GC。

3、并行Compacting:可通過-XX:+UseParallelOldGC強制指定。

4、并發CMS:可通過-XX:+UseConcMarkSweepGC來強制指定。并發的線程數預設為:( 并行GC線程數+3)/4,也可通過ParallelCMSThreads指定。

    觸發機制:

    1、當老生代空間的使用到達一定比率時觸發;

     Hotspot V 1.6中預設為65%,可通過PrintCMSInitiationStatistics(此參數在V 1.5中不能用)來檢視這個值到底是多少;可通過CMSInitiatingOccupancyFraction來強制指定,預設值并不是指派在了這個值上,是根據如下公式計算出來的: ((100 - MinHeapFreeRatio) +(double)(CMSTriggerRatio * MinHeapFreeRatio) / 100.0)/ 100.0; 其中,MinHeapFreeRatio預設值: 40   CMSTriggerRatio預設值: 80。

     2、當perm gen采用CMS收集且空間使用到一定比率時觸發;

     perm gen采用CMS收集需設定:-XX:+CMSClassUnloadingEnabled   Hotspot V 1.6中預設為65%;可通過CMSInitiatingPermOccupancyFraction來強制指定,同樣,它是根據如下公式計算出來的:((100 - MinHeapFreeRatio) +(double)(CMSTriggerPermRatio* MinHeapFreeRatio) / 100.0)/ 100.0; 其中,MinHeapFreeRatio預設值: 40    CMSTriggerPermRatio預設值: 80。

      3、Hotspot根據成本計算決定是否需要執行CMS GC;可通過-XX:+UseCMSInitiatingOccupancyOnly來去掉這個動态執行的政策。

      4、外部調用了System.gc,且設定了ExplicitGCInvokesConcurrent;需要注意,在hotspot 6中,在這種情況下如應用同時使用了NIO,可能會出現bug。

6、GC組合

1)預設GC組合

​​

探秘Java虛拟機——記憶體管理與垃圾回收(轉)

​​

2)可選的GC組合

​​

探秘Java虛拟機——記憶體管理與垃圾回收(轉)

​​

7、GC監測

1)jstat –gcutil [pid] [intervel] [count]

2)-verbose:gc // 可以輔助輸出一些詳細的GC資訊;-XX:+PrintGCDetails // 輸出GC詳細資訊;-XX:+PrintGCApplicationStoppedTime // 輸出GC造成應用暫停的時間

-XX:+PrintGCDateStamps // GC發生的時間資訊;-XX:+PrintHeapAtGC // 在GC前後輸出堆中各個區域的大小;-Xloggc:[file] // 将GC資訊輸出到單獨的檔案中,建議都加上,這個消耗不大,而且對查問題和調優有很大的幫助。gc的日志拿下來後可使用GCLogViewer或gchisto進行分析。

3)圖形化的情況下可直接用jvisualvm進行分析。

4)檢視記憶體的消耗狀況

      (1)長期消耗,可以直接dump,然後MAT(記憶體分析工具)檢視即可

      (2)短期消耗,圖形界面情況下,可使用jvisualvm的memory profiler或jprofiler。

8、系統調優方法

步驟:1、評估現狀 2、設定目标 3、嘗試調優 4、衡量調優 5、細微調整

設定目标:

1)降低Full GC的執行頻率?

2)降低Full GC的消耗時間?

3)降低Full GC所造成的應用停頓時間?

4)降低Minor GC執行頻率?

5)降低Minor GC消耗時間?

例如某系統的GC調優目标:降低Full GC執行頻率的同時,盡可能降低minor GC的執行頻率、消耗時間以及GC對應用造成的停頓時間。

衡量調優:

1、衡量工具

1)列印GC日志資訊:-XX:+PrintGCDetails –XX:+PrintGCApplicationStoppedTime -Xloggc: {檔案名}  -XX:+PrintGCTimeStamps

2)jmap:(由于每個版本jvm的預設值可能會有改變,建議還是用jmap首先觀察下目前每個代的記憶體大小、GC方式) 

3)運作狀況監測工具:jstat、jvisualvm、sar 、gclogviewer

2、應收集的資訊

1)minor gc的執行頻率;full gc的執行頻率,每次GC耗時多少?

2)高峰期什麼狀況?

3)minor gc回收的效果如何?survivor的消耗狀況如何,每次有多少對象會進入老生代?

4)full gc回收的效果如何?(簡單的memory leak判斷方法)

5)系統的load、cpu消耗、qps or tps、響應時間

QPS每秒查詢率:是對一個特定的查詢伺服器在規定時間内所處理流量多少的衡量标準。在網際網路上,作為域名伺服器的機器性能經常用每秒查詢率來衡量。對應fetches/sec,即每秒的響應請求數,也即是最大吞吐能力。

TPS(Transaction Per Second):每秒鐘系統能夠處理的交易或事務的數量。

嘗試調優:

注意Java RMI的定時GC觸發機制,可通過:-XX:+DisableExplicitGC來禁止或通過 -Dsun.rmi.dgc.server.gcInterval=3600000來控制觸發的時間。

1)降低Full GC執行頻率 – 通常瓶頸

老生代本身占用的記憶體空間就一直偏高,是以隻要稍微放點對象到老生代,就full GC了;

通常原因:系統緩存的東西太多;

例如:使用oracle 10g驅動時preparedstatement cache太大;

查找辦法:現執行Dump然後再進行MAT分析;

(1)Minor GC後總是有對象不斷的進入老生代,導緻老生代不斷的滿

通常原因:Survivor太小了

系統表現:系統響應太慢、請求量太大、每次請求配置設定的記憶體太多、配置設定的對象太大...

查找辦法:分析兩次minor GC之間到底哪些地方配置設定了記憶體;

利用jstat觀察Survivor的消耗狀況,-XX:PrintHeapAtGC,輸出GC前後的詳細資訊;

對于系統響應慢可以采用系統優化,不是GC優化的内容;

(2)老生代的記憶體占用一直偏高

調優方法:① 擴大老生代的大小(減少新生代的大小或調大heap的 大小);

減少new注意對minor gc的影響并且同時有可能造成full gc還是嚴重;

調大heap注意full gc的時間的延長,cpu夠強悍嘛,os是32 bit的嗎?

② 程式優化(去掉一些不必要的緩存)

(3)Minor GC後總是有對象不斷的進入老生代

前提:這些進入老生代的對象在full GC時大部分都會被回收

調優方法:

① 降低Minor GC的執行頻率;

② 讓對象盡量在Minor GC中就被回收掉:增大Eden區、增大survivor、增大TenuringThreshold;注意這些可能會造成minor gc執行頻繁;

③ 切換成CMS GC:老生代還沒有滿就回收掉,進而降低Full GC觸發的可能性;

④ 程式優化:提升響應速度、降低每次請求配置設定的記憶體、

(4)降低單次Full GC的執行時間

通常原因:老生代太大了...

調優方法:1)是并行GC嗎?   2)更新CPU  3)減小Heap或老生代

(5)降低Minor GC執行頻率

通常原因:每次請求配置設定的記憶體多、請求量大

通常辦法:1)擴大heap、擴大新生代、擴大eden。注意點:降低每次請求配置設定的記憶體;橫向增加機器的數量分擔請求的數量。

(6)降低Minor GC執行時間

通常原因:新生代太大了,響應速度太慢了,導緻每次Minor GC時存活的對象多

通常辦法:1)減小點新生代吧;2)增加CPU的數量、更新CPU的配置;加快系統的響應速度

細微調整:

首先需要了解以下情況:

① 當響應速度下降到多少或請求量上漲到多少時,系統會宕掉?

② 參數調整後系統多久會執行一次Minor GC,多久會執行一次Full GC,高峰期會如何?

需要計算的量:

①每次請求平均需要配置設定多少記憶體?系統的平均響應時間是多少呢?請求量是多少、多常時間執行一次Minor GC、Full GC?

②現有參數下,應該是多久一次Minor GC、Full GC,對比真實狀況,做一定的調整;

必殺技:提升響應速度、降低每次請求配置設定的記憶體?

9、系統調優舉例

     現象:1、系統響應速度大概為100ms;2、當系統QPS增長到40時,機器每隔5秒就執行一次minor gc,每隔3分鐘就執行一次full gc,并且很快就一直full GC了;4、每次Full gc後舊生代大概會消耗400M,有點多了。

     解決方案:解決Full GC次數過多的問題

    (1)降低響應時間或請求次數,這個需要重構,比較麻煩;——這個是終極方法,往往能夠順利的解決問題,因為大部分的問題均是由程式自身造成的。

    (2)減少老生代記憶體的消耗,比較靠譜;——可以通過分析Dump檔案(jmap dump),并利用MAT查找記憶體消耗的原因,進而發現程式中造成老生代記憶體消耗的原因。

    (3)減少每次請求的記憶體的消耗,貌似比較靠譜;——這個是海市蜃樓,沒有太好的辦法。

    (4)降低GC造成的應用暫停的時間——可以采用CMS GS垃圾回收器。參數設定如下:

     -Xms1536m -Xmx1536m -Xmn700m -XX:SurvivorRatio=7 -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection

     -XX:CMSMaxAbortablePrecleanTime=1000 -XX:+CMSClassUnloadingEnabled -XX:+UseCMSInitiatingOccupancyOnly -XX:+DisableExplicitGC

    (5)減少每次minor gc晉升到old的對象。可選方法:1) 調大新生代。2)調大Survivor。3)調大TenuringThreshold。

      調大Survivor:目前采用PS GC,Survivor space會被動态調整。由于調整幅度很小,導緻了經常有對象直接轉移到了老生代;于是禁止Survivor區的動态調整了,-XX:-UseAdaptiveSizePolicy,并計算Survivor Space需要的大小,于是繼續觀察,并做微調…。最終将Full GC推遲到2小時1次。

10、垃圾回收的實作原理

      記憶體回收的實作方法:1)引用計數:不适合複雜對象的引用關系,尤其是循環依賴的場景。2)有向圖Tracing:适合于複雜對象的引用關系場景,Hotspot采用這種。常用算法:Copying、Mark-Sweep、Mark-Compact。

      Hotspot從root set開始掃描有引用的對象并對Reference類型的對象進行特殊處理。

      以下是Root Set的清單:1)目前正在執行的線程;2)全局/靜态變量;3)JVM Handles;4)JNI 【 Java Native Interface 】Handles;

      另外:minor GC隻掃描新生代,當老生代的對象引用了新生代的對象時,會采用如下的處理方式:在給對象賦引用時,會經過一個write barrier的過程,以便檢查是否有老生代引用新生代對象的情況,如有則記錄到remember set中。并在minor gc時,remember set指向的新生代對象也作為root set。

     新生代串行GC(Serial Copying):

     新生代串行GC(Serial Copying)完整記憶體的配置設定政策:

     1)首先在TLAB(本地線程配置設定緩沖區)上嘗試配置設定;

     2)檢查是否需要在新生代上配置設定,如需要配置設定的大小小于PretenureSizeThreshold,則在eden區上進行配置設定,配置設定成功則傳回;配置設定失敗則繼續;

     3)檢查是否需要嘗試在老生代上配置設定,如需要,則周遊所有代并檢查是否可在該代上配置設定,如可以則進行配置設定;如不需要在老生代上嘗試配置設定,則繼續;

     4)根據政策決定執行新生代GC或Full GC,執行full gc時不清除soft Ref;

     5)如需要配置設定的大小大于PretenureSizeThreshold,嘗試在老生代上配置設定,否則嘗試在新生代上配置設定;

     6)嘗試擴大堆并配置設定;

     7)執行full gc,并清除所有soft Ref,按步驟5繼續嘗試配置設定。  

     新生代串行GC(Serial Copying)完整記憶體回收政策

     1)檢查to是否為空,不為空傳回false;

     2)檢查老生代剩餘空間是否大于目前eden+from已用的大小,如大于則傳回true,如小于且HandlePromotionFailure為true,則檢查剩餘空間是否大于之前每次minor gc晉級到老生代的平均大小,如大于傳回true,如小于傳回false。

     3)如上面的結果為false,則執行full gc;如上面的結果為true,執行下面的步驟;

     4)掃描引用關系,将活的對象copy到to space,如對象在minor gc中的存活次數超過tenuring_threshold或配置設定失敗,則往老生代複制,如仍然複制失敗,則取決于HandlePromotionFailure,如不需要處理,直接抛出OOM,并退出vm,如需處理,則保持這些新生代對象不動;

    新生代可用GC-PS

    完整記憶體配置設定政策

    1)先在TLAB上配置設定,配置設定失敗則直接在eden上配置設定;

    2)當eden上配置設定失敗時,檢查需要配置設定的大小是否 >= eden space的一半,如是,則直接在老生代配置設定;

    3)如配置設定仍然失敗,且gc已超過頻率,則抛出OOM;

    4)進入基本配置設定政策失敗的模式;

    5)執行PS GC,在eden上配置設定;

    6)執行非最大壓縮的full gc,在eden上配置設定;

    7)在舊生代上配置設定;

    8)執行最大壓縮full gc,在eden上配置設定;

    9)在舊生代上配置設定;

    10)如還失敗,回到2。

   最悲慘的情況,配置設定觸發多次PS GC和多次Full GC,直到OOM。

   完整記憶體回收政策

   1)如gc所執行的時間超過,直接結束;

   2)先調用invoke_nopolicy

       2.1 先檢查是不是要嘗試scavenge;

       2.1.1 to space必須為空,如不為空,則傳回false;

       2.1.2 擷取之前所有minor gc晉級到old的平均大小,并對比目前eden+from已使用的大小,取更小的一個值,如老生代剩餘空間小于此值,則傳回false,如大于則傳回true;

       2.2 如不需要嘗試scavenge,則傳回false,否則繼續;

       2.3 多線程掃描活的對象,并基亍copying算法回收,回收時相應的晉升對象到舊生代;

       2.4 如UseAdaptiveSizePolicy,那麼重新計算to space和tenuringThreshold的值,并調整。

   3)如invoke_nopolicy傳回的是false,或之前所有minor gc晉級到老生代的平均大小 > 舊生代的剩餘空間,那麼繼續下面的步驟,否則結束;

   4)如UseParallelOldGC,則執行PSParallelCompact,如不是UseParallelOldGC,則執行PSMarkSweep。

    老生代并行CMS GC:

    優缺點:

    1) 大部分時候和應用并發進行,是以隻會造成很短的暫停時間;

    2)浮動垃圾,沒辦法,是以記憶體空間要稍微大一點;

    3)記憶體碎片,-XX:+UseCMSCompactAtFullCollection 來解決;

    4) 争搶CPU,這GC方式就這樣;

    5)多次remark,是以總的gc時間會比并行的長;

    6)記憶體配置設定,free list方式,so性能稍差,對minor GC會有一點影響;

    7)和應用并發,有可能配置設定和回收同時,産生競争,引入了鎖,JVM配置設定優先。

11、TLAB的解釋

     堆内的對象資料是各個線程所共享的,是以當在堆内建立新的對象時,就需要進行鎖操作。鎖操作是比較耗時,是以JVM為每個線在堆上配置設定了一塊“自留地”——TLAB(全稱是Thread Local Allocation Buffer),位于堆記憶體的新生代,也就是Eden區。每個線程在建立新的對象時,會首先嘗試在自己的TLAB裡進行配置設定,如果成功就傳回,失敗了再到共享的Eden區裡去申請空間。線上程自己的TLAB區域建立對象失敗一般有兩個原因:一是對象太大,二是自己的TLAB區剩餘空間不夠。通常預設的TLAB區域大小是Eden區域的1%,當然也可以手工進行調整,對應的JVM參數是-XX:TLABWasteTargetPercent。

參考文獻:

1、Sun JDK 1.6 GC(Garbage Collector)  作者:畢玄

繼續閱讀