一些長時間GC停頓問題的排查及解決辦法

對於許多企業級應用,尤爲是OLTP應用來講,長暫停極可能致使服務超時,而對這些運行在JVM上的應用來講,垃圾回收(GC)多是長暫停最主要的緣由。本文將描述一些可能碰到GC長暫停的不一樣場景,以及說明咱們如何排查和解決這些GC停頓的問題。ios

下面是一些應用在運行時,可能致使GC長暫停的不一樣場景。算法

1. 碎片化

這個絕對要排在第一位。由於,正是由於碎片化問題--CMS最致命的缺陷,致使這個統治了OLAP系統十多年的垃圾回收器直接退出歷史舞臺(CMS已是deprecated,將來版本會被移除,請珍惜那些配置了CMS的JVM吧),面對G1以及最新的ZGC,天生殘(碎)缺(片)的CMS毫無還手之力。bash

對於CMS,因爲老年代的碎片化問題,在YGC時可能碰到晉升失敗(promotion failures,即便老年代還有足夠多有效的空間,可是仍然可能致使分配失敗,由於沒有足夠連續的空間),從而觸發Concurrent Mode Failure,發生會徹底STW的FullGC。FullGC相比CMS這種併發模式的GC須要更長的停頓時間才能完成垃圾回收工做,這絕對是Java應用最大的災難之一。網絡

爲何CMS場景下會有碎片化問題?因爲CMS在老年代回收時,採用的是標記清理(Mark-Sweep)算法,它在垃圾回收時並不會壓縮堆,日積月累,致使老年代的碎片化問題會愈來愈嚴重,直到發生單線程的Mark-Sweep-Compact GC,即FullGC,會徹底STW。若是堆比較大的話,STW的時間可能須要好幾秒,甚至十多秒,幾十秒都有可能。架構

接下來的cms gc日誌,因爲碎片率很是高,從而致使promotion failure,而後發生concurrent mode failure,觸發的FullGC總計花了17.1365396秒才完成:併發

{Heap before GC invocations=7430 (full 24):

parnew generation total 134400K, used 121348K[0x53000000, 0x5c600000, 0x5c600000)

eden space 115200K, 99% used [0x53000000, 0x5a07e738, 0x5a080000)

from space 19200K, 32% used [0x5a080000, 0x5a682cc0, 0x5b340000)

to space 19200K, 0% used [0x5b340000, 0x5b340000, 0x5c600000)

concurrent mark-sweep generation total 2099200K, used 1694466K [0x5c600000, 0xdc800000, 0xdc800000)

concurrent-mark-sweep perm gen total 409600K, used 186942K [0xdc800000, 0xf5800000, 0xfbc00000)

10628.167: [GC Before GC:

Statistics for BinaryTreeDictionary:

------------------------------------

Total Free Space: 103224160

Max Chunk Size: 5486

Number of Blocks: 57345

Av. Block Size: 1800

Tree Height: 36 <---- High fragmentation

Statistics for IndexedFreeLists:

--------------------------------

Total Free Space: 371324

Max Chunk Size: 254

Number of Blocks: 8591 <---- High fragmentation

Av. Block Size: 43

free=103595484

frag=1.0000 <---- High fragmentation

Before GC:

Statistics for BinaryTreeDictionary:

------------------------------------

Total Free Space: 0

Max Chunk Size: 0

Number of Blocks: 0

Tree Height: 0

Statistics for IndexedFreeLists:

--------------------------------

Total Free Space: 0

Max Chunk Size: 0

Number of Blocks: 0

free=0 frag=0.0000

10628.168: [ParNew (promotion failed) Desired survivor size 9830400 bytes, new threshold 1 (max 1)

- age 1: 4770440 bytes, 4770440 total: 121348K->122157K(134400K), 0.4263254secs]

10628,594: [CMS10630.887: [CMS-concurrent-mark: 7.286/8.682 secs] [Times: user=14.81, sys=0.34, real=8.68 secs]

(concurrent mode failure):1698044K->625427K(2099200K), 17.1365396 secs]

1815815K->625427K(2233600K), [CMS Perm : 186942K->180711K(409600K)]

After GC:

Statistics for BinaryTreeDictionary:

------------------------------------

Total Free Space: 377269492

Max Chunk Size:

377269492

Number of Blocks: 1 <---- No fragmentation

Av. Block Size: 377269492

Tree Height: 1 <---- No fragmentation

Statistics for IndexedFreeLists:

--------------------------------

Total Free Space: 0

Max Chunk Size: 0

Number of Blocks: 0

free=377269492

frag=0.0000 <---- No fragmentation

After GC:

Statistics for BinaryTreeDictionary:

------------------------------------

Total Free Space: 0

Max Chunk Size: 0

Number of Blocks: 0

Tree Height: 0

Statistics for IndexedFreeLists:

--------------------------------

Total Free Space: 0

Max Chunk Size: 0

Number of Blocks: 0

free=0 frag=0.0000

, 17.5645589 secs] [Times: user=17.82 sys=0.06, real=17.57 secs]

Heap after GC invocations=7431 (full 25):

parnew generation total 134400K, used 0K [0x53000000, 0x5c600000, 0x5c600000)

eden space 115200K, 0% used [0x53000000, 0x53000000, 0x5a080000)

from space 19200K, 0% used [0x5b340000, 0x5b340000, 0x5c600000)

to space 19200K, 0% used [0x5a080000, 0x5a080000, 0x5b340000)

concurrent mark-sweep generation total 2099200K, used 625427K [0x5c600000, 0xdc800000, 0xdc800000)

concurrent-mark-sweep perm gen total 409600K, used 180711K [0xdc800000, 0xf5800000, 0xfbc00000)

}

Total time for which application threads were stopped: 17.5730653 seconds
複製代碼

2. GC時操做系統的活動

當發生GC時,一些操做系統的活動,好比swap,可能致使GC停頓時間更長,這些停頓多是幾秒,甚至幾十秒級別。app

若是你的系統配置了容許使用swap空間,操做系統可能把JVM進程的非活動內存頁移到swap空間,從而釋放內存給當前活動進程(多是操做系統上其餘進程,取決於系統調度)。Swapping因爲須要訪問磁盤,因此相比物理內存,它的速度的使人髮指。因此,若是在GC的時候,系統正好須要執行Swapping,那麼GC停頓的時間必定會很是很是很是恐怖。工具

下面是一段持續了29.48秒的YGC日誌:ui

{Heap before GC invocations=132 (full 0):

par new generation total 2696384K, used 2696384K [0xfffffffc20010000, 0xfffffffce0010000, 0xfffffffce0010000)

eden space 2247040K, 100% used [0xfffffffc20010000, 0xfffffffca9270000, 0xfffffffca9270000)

from space 449344K, 100% used [0xfffffffca9270000, 0xfffffffcc4940000, 0xfffffffcc4940000)

to space 449344K, 0% used [0xfffffffcc4940000, 0xfffffffcc4940000, 0xfffffffce0010000)

concurrent mark-sweep generation total 9437184K, used 1860619K [0xfffffffce0010000, 0xffffffff20010000, 0xffffffff20010000)

concurrent-mark-sweep perm gen total 1310720K, used 511451K [0xffffffff20010000, 0xffffffff70010000, 0xffffffff70010000)

2013-07-17T03:58:06.601-0700: 51522.120: [GC Before GC: :2696384K->449344K(2696384K), 29.4779282 secs] 4557003K->2326821K(12133568K) ,29.4795222 secs] [Times: user=915.56, sys=6.35, real=29.48 secs]
複製代碼

最後一行[Times: user=915.56, sys=6.35, real=29.48 secs]中real就是YGC時應用真實的停頓時間。spa

發生YGC的這個時間點,vmstat命令輸出結果以下:

r b w swap free re mf pi po fr de sr s0 s1 s2 s3 in sy cs us sy id

0 0 0 77611960 94847600 55 266 0 0 0 0 0 0 0 0 0 3041 2644 2431 44 8 48

0 0 0 76968296 94828816 79 324 0 18 18 0 0 0 0 1 0 3009 3642 2519 59 13 28

1 0 0 77316456 94816000 389 2848 0 7 7 0 0 0 0 2 0 40062 78231 61451 42 6 53

2 0 0 77577552 94798520 115 591 0 13 13 0 0 13 12 1 0 4991 8104 5413 2 0 98
複製代碼
YGC總計花了29秒才完成。vmstat命令輸出結果表示,可用swap空間在這個時間段減小了600m。這就意味着,在GC的時候,內存中的一些頁被移到了swap空間,這個內存頁不必定屬於JVM進程,多是其餘操做系統上的其餘進程。

從上面能夠看出,操做系統上可用物理內容不足以運行系統上全部的進程,解決辦法就是儘量運行更少的進程,增長RAM從而提高系統的物理內存。在這個例子中,Old區有9G,可是隻使用了1.8G(mark-sweep generation total 9437184K, used 1860619K)。咱們能夠適當的下降Old區的大小以及整個堆的大小,從而減小內存壓力,最小化系統上的應用發生swapping的可能。

除了swapping之外,咱們也須要監控瞭解長GC暫停時的任何IO或者網絡活動狀況等, 能夠經過iostat和netstat兩個工具來實現. 咱們還能經過mpstat查看CPU統計信息,從而弄清楚在GC的時候是否有足夠的CPU資源。

3. 堆空間不夠

若是應用程序須要的內存比咱們執行的Xmx還要大,也會致使頻繁的垃圾回收,甚至OOM。因爲堆空間不足,對象分配失敗,JVM就須要調用GC嘗試回收已經分配的空間,可是GC並不能釋放更多的空間,從而又回致使GC,進入惡性循環。

應用運行時,頻繁的FullGC會引發長時間停頓,在下面這個例子中,Perm空間幾乎是滿的,而且在Perm區嘗試分配內存也都失敗了,從而觸發FullGC:

166687.013: [Full GC [PSYoungGen:126501K->0K(922048K)] [PSOldGen: 2063794K->1598637K(2097152K)]2190295K->1598637K(3019200K) [PSPermGen: 165840K->164249K(166016K)],6.8204928 secs] [Times: user=6.80 sys=0.02, real=6.81 secs]

166699.015: [Full GC [PSYoungGen:125518K->0K(922048K)] [PSOldGen: 1763798K->1583621K(2097152K)]1889316K->1583621K(3019200K) [PSPermGen: 165868K->164849K(166016K)],4.8204928 secs] [Times: user=4.80 sys=0.02, real=4.81 secs]
複製代碼
一樣的,若是在老年代的空間不夠的話,也會致使頻繁FullGC,這類問題比較好辦,給足老年代和永久代,不要作太摳門的人了,嘿嘿。

4. JVM Bug

什麼軟件都有BUG,JVM也不例外。有時候,GC的長時間停頓就有多是BUG引發的。例如,下面列舉的這些JVM的BUG,就可能致使Java應用在GC時長時間停頓。

6459113: CMS+ParNew: wildly different ParNew pause times depending on heap shape caused by allocation spread

fixed in JDK 6u1 and 7

6572569: CMS: consistently skewed work distribution indicated in (long) re-mark pauses

fixed in JDK 6u4 and 7

6631166: CMS: better heuristics when combatting fragmentation

fixed in JDK 6u21 and 7

6999988: CMS: Increased fragmentation leading to promotion failure after CR#6631166 got implemented

fixed in JDK 6u25 and 7

6683623: G1: use logarithmic BOT code such as used by other collectors

fixed in JDK 6u14 and 7

6976350: G1: deal with fragmentation while copying objects during GC

fixed in JDK 8
複製代碼
若是你的JDK正好是上面這些版本,強烈建議升級到更新BUG已經修復的版本。

5. 顯示System.gc調用

檢查是否有顯示的System.gc調用,應用中的一些類裏,或者第三方模塊中調用System.gc調用從而觸發STW的FullGC,也可能會引發很是長時間的停頓。以下GC日誌所示,Full GC後面的(System)表示它是由調用System.GC觸發的FullGC,而且耗時5.75秒:

164638.058: [Full GC (System) [PSYoungGen: 22789K->0K(992448K)]

[PSOldGen: 1645508K->1666990K(2097152K)] 1668298K->1666990K(3089600K)

[PSPermGen: 164914K->164914K(166720K)], 5.7499132 secs] [Times: user=5.69, sys=0.06, real=5.75 secs]
複製代碼
若是你使用了RMI,能觀察到固定時間間隔的FullGC,也是因爲RMI的實現調用了System.gc。這個時間間隔能夠經過系統屬性配置:
-Dsun.rmi.dgc.server.gcInterval=7200000

-Dsun.rmi.dgc.client.gcInterval=7200000
複製代碼

JDK 1.4.2和5.0的默認值是60000毫秒,即1分鐘;JDK6以及之後的版本,默認值是3600000毫秒,即1個小時。

若是你要關閉經過調用System.gc()觸發FullGC,配置JVM參數 -XX:+DisableExplicitGC便可。

那麼如何定位並解決這類問題問題?

  1. 配置JVM參數:-XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps and -XX:+PrintGCApplicationStoppedTime. 若是是CMS,還須要添加-XX:PrintFLSStatistics=2,而後收集GC日誌。由於GC日誌能告訴咱們GC頻率,是否長時間停頓等重要信息。

  2. 使用vmstat, iostat, netstat和mpstat等工具監控系統全方位健康情況。

  3. 使用GCHisto工具可視化分析GC日誌,弄明白消耗了很長時間的GC,以及這些GC的出現是否有必定的規律。

  4. 嘗試從GC日誌中可否找出一下JVM堆碎片化的表徵。

  5. 監控指定應用的堆大小是否足夠。

  6. 檢查你運行的JVM版本,是否有與長時間停頓相關的BUG,而後升級到修復問題的最新JDK。

最後

後續會持續更新JVM專題知識及更多架構專題,你們以爲不錯能夠點個贊在關注下,之後還會分享更多文章!

相關文章
相關標籤/搜索