又发生频繁FGC,这次是谁的锅
轉載自??又發生頻繁FGC,這次是誰的鍋
這是笨神JVMPocket群里一位名為"云何*住"的同學提出來的問題,問題現象是CPU飆高并且頻繁FullGC。
重現問題
這位同學的業務代碼比較復雜,為了簡化業務場景,筆者將其代碼壓縮成如下的代碼片段:
public?class?FullGCDemo?{private?static?ScheduledThreadPoolExecutor?executor?=?new?ScheduledThreadPoolExecutor(50,new?ThreadPoolExecutor.DiscardOldestPolicy());public?static?void?main(String[]?args)?throws?Exception?{executor.setMaximumPoolSize(50);//?模擬xxl-job?100ms?調用一次,?原代碼沒有這么頻繁for?(int?i=0;?i<Integer.MAX_VALUE;?i++){buildBar();Thread.sleep(100);}}private?static?void?buildBar(){List<FutureContract>?futureContractList?=?getAllFutureContract();futureContractList.forEach(contract?->?{//?do?somethingexecutor.scheduleWithFixedDelay(()?->?{try{doFutureContract(contract);}catch?(Exception?e){e.printStackTrace();}},?2,?3,?TimeUnit.SECONDS);});}private?static?void?doFutureContract(FutureContract?contract){//?do?something?with?futureContract}private?static?List<FutureContract>?getAllFutureContract(){List<FutureContract>?futureContractList?=?new?ArrayList<>();//?問題代碼這里每次只會new不到10個對象,?我這里new了100個是為了更快重現問題for?(int?i?=?0;?i?<?100;?i++)?{FutureContract?contract?=?new?FutureContract(i,?...?...);futureContractList.add(contract);}return?futureContractList;}
}
說明,為了更好的還原問題,FutureContract.java的定義建議盡量與問題代碼保持一致:
- 16個BigDecimal類型屬性
- 3個Long類型屬性
- 3個String類型屬性
- 4個Integer類型屬性
- 2個Date類型屬性
問題代碼運行時的JVM參數如下(JDK8):
java?-Xmx256m?-Xms256m?-Xmn64m?FullGCDemo
你也可以先自己獨立思考一下這塊代碼問題何在。
CPU飆高
這是第一個現象,top命令就能看到,找到我們的進程ID,例如91782。然后執行命令top -H -p 91782查看進程里的線程情況:
???PID?USER??????PR??NI??VIRT??RES??SHR?S?%CPU?%MEM????TIME+??COMMAND?????????????????????????????????????????????????????91784?yyapp?????20???0?2670m?300m??12m?R?92.2??7.8???4:14.39?java?????????????????????????????????????????????????????????91785?yyapp?????20???0?2670m?300m??12m?R?91.9??7.8???4:14.32?java?????????????????????????????????????????????????????????91794?yyapp?????20???0?2670m?300m??12m?S??1.0??7.8???0:09.38?java?????????????????????????????????????????????????????????91799?yyapp?????20???0?2670m?300m??12m?S??1.0??7.8???0:09.39?java?
由這段結果可知線程91784和91785很消耗CPU。將91784和91785分別轉為16進制,得到16688和16689。接下來通過執行命令命令jstack -l 91782 > 91782.log導出線程棧信息(命令中是進程ID),并在線程dump文件中尋找16進制數16688和16689,得到如下兩條信息:
"GC?task?thread#0?(ParallelGC)"?os_prio=0?tid=0x00007f700001e000?nid=0x16688?runnable?
"GC?task?thread#1?(ParallelGC)"?os_prio=0?tid=0x00007f7000020000?nid=0x16689?runnable
由這兩行結果可知,消耗CPU的是ParallelGC線程。因為問題代碼搭配的JVM參數沒有指定任何垃圾回收期,所以用的是默認的PS垃圾回收,所以這個JVM實例應該在頻繁FullGC,通過命令jstat -gcutil 91782 5s查看GC表現可以驗證,由這段結果可知,Eden和Old都占滿了,且不再發生YGC,但是卻在頻繁FGC,此時的應用已經不能處理任務,相當于假死了,好可怕:
??S0?????S1?????E??????O??????M?????CCS????YGC?????YGCT????FGC????FGCT?????GCT?0.00???0.00?100.00??99.98??78.57??83.36??????5????0.633???366??327.647??328.2810.00???0.00?100.00??99.98??78.57??83.36??????5????0.633???371??331.965??332.5980.00???0.00?100.00??99.98??78.57??83.36??????5????0.633???376??336.996??337.6290.00???0.00?100.00??99.98??78.57??83.36??????5????0.633???381??340.795??341.4280.00???0.00?100.00??99.98??78.57??83.36??????5????0.633???387??346.268??346.901
揪出真兇
到這里基本可以確認是有對象沒有釋放導致即使發生FullGC也回收不了引起的,準備dump進行分析看看Old區都是些什么妖魔鬼怪,執行命令jmap -dump:format=b,file=91782.bin 91782,用MAT分析時,強烈建議開啟keep unreachable objects:
?
keep unreachable objects
接下來點擊Actions下的Histogram,查找大對象:
?
histogram
下面貼出的是原圖,而不是筆者的Demo代碼跑出來的:
?
histogram view
由這段代碼可知,大量的FutureContract和BigDecimal(說明:因為FutureContract中有多達16個BigDecimal類型的屬性),FutureContract占了120MB,BigDecimal占了95MB。那么就可以斷定問題是與FutureContract相關的代碼造成的,如果是正常的JVM示例,Histogram試圖最占內存的是byte[]和char[]兩個數組,兩者合計一般會占去80%左右的內存,遠遠超過其他對象占用的內存。
接下來通過FutureContract就找到上面這塊buildBar方法代碼,那么為什么是這塊代碼無法釋放呢?單獨把這塊代碼擰出來看看,這里用到了ScheduledThreadPoolExecutor定時調度,且每3秒執行一次,然而定時器中需要的參數來自外面的List<FutureContract>,這就會導致List<FutureContract>這個對象一致被一個定時任務引用,永遠無法回收,從而導致FutureContract不斷晉升到Old區,直到占滿Old區然后頻繁FullGC。
private?static?void?buildBar(){List<FutureContract>?futureContractList?=?getAllFutureContract();futureContractList.forEach(contract?->?{//?do?somethingexecutor.scheduleWithFixedDelay(()?->?{try{doFutureContract(contract);}catch?(Exception?e){e.printStackTrace();}},?2,?3,?TimeUnit.SECONDS);});
}
那么為什么會出現這種情況呢?我相信一個程序員不應該犯這樣的低級錯誤,后來看到原生代碼,我做出一個比較合理的猜測,其本意可能是想通過調用Executor executor來異步執行,誰知小手一抖,在紅色框那里輸入了taskExecutor,而不是executor:
?
problem code
解決問題
OK,知道問題的根因,想解決問題就比較簡單了,將taskExecutor改成executor即可:
private?static?ThreadPoolExecutor?executor?=?new?ThreadPoolExecutor(50,?50,?0L,?TimeUnit.MILLISECONDS,?new?LinkedBlockingQueue<>(128));
private?static?void?buildBar(){List<FutureContract>?futureContractList?=?getAllFutureContract();futureContractList.forEach(contract?->?{//?do?somethingexecutor.execute(()?->?{try{doFutureContract(contract);}catch?(Exception?e){e.printStackTrace();}});});
}
或者將這一塊直接改成同步處理,不需要線程池:
private?static?void?buildBar(){List<FutureContract>?futureContractList?=?getAllFutureContract();futureContractList.forEach(contract?->?{//?do?somethingtry{doFutureContract(contract);}catch?(Exception?e){e.printStackTrace();}});
}
總結
以上是生活随笔為你收集整理的又发生频繁FGC,这次是谁的锅的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: stopwatch类使用
- 下一篇: GTP与MBR硬盘分区区别(UEFI介绍