背景:JAVA APP,主要功能是處理日志並存入db
現象:運行一段時間就出現OOM問題,查看GC log發現運行沒多久就一直Full GC,並且拋出OOM的異常。
[Full GC (Ergonomics) [PSYoungGen: 529920K->525999K(614912K)] [ParOldGen: 1398052K->1397869K(1398272K)] 1927972K->1923868K(2013184K), [Metaspace: 33827K->33827K(1079296K)], 4.1812153 secs] [Times: user=32.38 sys=0.07, real=4.19 secs]
[Full GC (Ergonomics) [PSYoungGen: 529920K->525483K(614912K)] [ParOldGen: 1397869K->1397848K(1398272K)] 1927789K->1923331K(2013184K), [Metaspace: 33832K->33832K(1079296K)], 5.6714054 secs] [Times: user=43.50 sys=0.09, real=5.67 secs]
GC日志中老年代非常大,而且回收效果也不明顯。遇到這種問題,第一感覺還是有內存泄露,雖然Java能自己回收內存,但是不能保證我自己寫的程序沒有問題,比如曾經犯過一個錯誤往list一直add object,卻沒有remove,並且list也不釋放,導致list越來越大,最后內存OOM。本着大膽假設,小心求證的理念,開始排查問題。
遇到內存問題,最好是希望能夠直觀的看到Java程序堆中現在有哪些對象,有哪些對象數目一直在遞增而沒有被回收。為此需要借助工具來排查了,visualVM是非常好的能滿足需求的一個工具。
啟動visualVM,啟動程序,通過Visual GC查看內存情況。
從上圖中,我們很明顯發現程序的Old Generation占用空間是在不斷地上漲,並且沒有明顯下跌,說明生成的對象都是存在內存里面的,並沒有被釋放掉。接下來查看一下內存中有哪些對象
使用Sampler標簽頁中的Memory 采集功能,查看內存中當前是哪些對象,從采樣結果來看Request對象是非常多的,而且一直在遞增。Request對象在程序中使用較多,接下來看下是哪個線程一直在積壓Request
查看Per thread allocations,很容易看出insertRequestList線程分配的內存最多。
對照程序我恍然大悟,發現問題就出在插入數據上。程序中插入數據庫采用異步批量插入方式,當生成一個Request對象就存入一個LinkedBlockingQueue,集滿一定量進行批量插入。而我設置的LinkedBlockingQueue初始化了一個非常大的capacity,這就導致來不及插入的Request全部都堆積在LinkedBlockingQueue中了。
臨時解決方案:將LinkedBlockingQueue的capacity設置小一些,並且采用put方式插入Request到LinkedBlockingQueue,當插入來不及的時候就等待。
修改代碼后再來看下visualVM結果:
old Generation每次GC能回收大量的Object,不會出現Full GC了。
Request對象數量也並不是一直在增長了。但是InsertRequestThread線程仍然是分配了很多的內存,可以得知該線程必然還是積壓了大量的Request對象的。因為只采用了臨時解決方案,mysql插入數據速度上不去,積壓在所難免。
附錄:
visualVM遠程監控
在執行Java 應用程序的服務器上先生成一個jstatd.all.policy
grant codebase "file:${JAVA_HOME}/lib/tools.jar" { permission java.security.AllPermission; };
然后執行命令
nohup ${JAVA_HOME}/bin/jstatd -J-Djava.rmi.server.hostname=10.111.123.234 -J-Djava.security.policy=jstatd.all.policy -J-Dcom.sun.management.jmxremote.authenticate=false -J-Dcom.sun.management.jmxremote.ssl=false -J-Dcom.sun.management.jmxremote.port=8888 &
然后在本地啟動visualVM,Remote添加10.111.123.234,默認端口1099就可以監控服務器上的程序運行狀況了,非常方便。
如果出現access denied ("java.util.PropertyPermission" "java.rmi.server.ignoreSubClasses" "write")報警,則建議將$JAVA_HOME和jstatd.all.policy都使用絕對路徑。