又是一个程序员粗心的代码引起频繁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 something
            executor.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.281
  0.00   0.00 100.00  99.98  78.57  83.36      5    0.633   371  331.965  332.598
  0.00   0.00 100.00  99.98  78.57  83.36      5    0.633   376  336.996  337.629
  0.00   0.00 100.00  99.98  78.57  83.36      5    0.633   381  340.795  341.428
  0.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 something
        executor.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 something
        executor.execute(() -> {
            try{
                doFutureContract(contract);
            }catch (Exception e){
                e.printStackTrace();
            }
        });
    });
}

或者将这一块直接改成同步处理,不需要线程池:

private static void buildBar(){
    List<FutureContract> futureContractList = getAllFutureContract();
    futureContractList.forEach(contract -> {
        // do something
        try{
            doFutureContract(contract);
        }catch (Exception e){
            e.printStackTrace();
        }
    });
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355