又是一个程序员粗心的代码引起频繁FullGC的案例
這是笨神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:
接下來點擊Actions下的Histogram,查找大對象:
下面貼出的是原圖,而不是筆者的Demo代碼跑出來的:
由這段代碼可知,大量的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:
?
解決問題
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();}}); }總結
以上是生活随笔為你收集整理的又是一个程序员粗心的代码引起频繁FullGC的案例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 扒一扒我遇见过哪些厌恶的技术面试官
- 下一篇: Consul和ZooKeeper的区别