一次通过jvm排查堆内存不断增大最后导致docker容器自动重启的问题

2023-05-16

1.事件背景:

        生产有个定时任务,经常跑不出数据,通过监控发现对应的那台机器内存一跑这个定时任务就会陡增。 由于应用部署在容器中,当内存跑满后会自动重启,所以导致定时任务无法执行完毕。上图:

 内存飙升。

 

容器重启。

2.看代码进行分析

  public ReturnT<String> batteryOperatingStateCheckJob(String param){
        try {
            Date date = DateUtil.offsetDay(new Date(),-1);
            try {
                if(StrUtil.isNotBlank(param)){
                    date = DateUtil.parse(param);
                }
            }catch (Exception e){
                XxlJobLogger.log("参数["+param+"]格式错误:"+e.getMessage());
            }
            String yesterday = DateUtil.format(date,"yyyy-MM-dd");
            Date beginDate = new Date();
            log(yesterday+" 电池运营状态日盘点任务开始:当前每页记录数:"+pageSize);
            //1.先清空表
            batteryCheckService.truncateBatteryCheckDetail(yesterday);
            //查询所有电池信息并插入Oracle
            Query query = new Query();
            //从mongo中统计数据总量
            long total = secondaryMongoTemplate.count(query, BatteryStatusInfo.class);
            //分页 一页1w数据
            int pages =(int)Math.ceil((double)total/pageSize);
            //循环插入数据库里面用了一个线程池5个线程
            for(int i=0;i<pages;i++){
                ExecutorService executor = ThreadUtil.newExecutor(5);;
                query.skip(pageSize * i).limit(pageSize);
                log("第"+(i+1)+"批数据:正在查询...");
                //将mongo查出的数据按照1000一个分组成list
                List<List<BatteryCheckInfo>> subList = getPageList(query,yesterday);
                log("第"+(i+1)+"批数据:查询完成。");
                //向线程池中添加任务
                subList.forEach(sub-> executor.submit(
                        () -> {
                            log("sql执行中。。。");
                             batteryCheckService.batchInsertBatteryCheckDetail(sub, yesterday);
                        }
                ));
                //等待线程池任务执行完毕后再进行下一次循环
                executor.shutdown();
                while (!executor.isTerminated()){
                        Thread.sleep(3000);
                }
                log("一次循环执行完毕:");
//                System.gc();
                }
            }

        }catch (Exception e){
            e.printStackTrace();
            return new ReturnT<>("盘点失败:"+e.getMessage());
        }

    }

代码逻辑简单来说就是,从mongo中取数据然后批量插入到数据库中。

初步分析认为,既然for循环一次会插入1w数据 那下一次循环的时候,这1w数据应该会释放内存当fullgc的时候这1w的数据引用肯定会被垃圾回收。但是为什么内存没有向下的波动呢。

3.本地复现(由于网络原因 无法远程监控生产jvm)

因此本地跑一下数据通过jvisualVM进行观察内存状况。(我通过junit进行单元测试)

理论方法执行完毕后应该会堆内存会下降。我这里睡眠是为了后边继续观察当前线程的监控情况。

 看堆内存从开始执行的时候并不是有规律的波动,如红色框中。

在方法执行完毕后为何会居高不下 如蓝色框中。

 看老年代中的内存也是一直在增加。

后来考虑到gc发生是需要特定情况的为了更好的观察我手动gc更好利于分析。(上面代码中注释放开  每次for循环以后gc一次, 代码执行完毕后gc一次)

2.改完之后再次进行运行进行观察。

有明显的堆内存回收 但是感觉每次回收都回收不全,由于生产数据量很大所以最终还是有可能导致占满内存。那么现在的问题就是找到为什么每次gc为什么还有部分没用回收 感觉像是什么东西在逐渐叠加。而且在方法执行完毕后的gc还是没有完全回收。

蓝色框是junit睡眠20s后的gc  但是这个gc完后还是占用了很大的内存。

 4.解决

将上面的堆信息转储。然后用mat进行打开分析

 发现 druid 里面有个实例占用内存占了77.7%

 后来百度发现

JdbcDataSourceStat、MybatisSQL拼接引起老年代内存不断增加_Resee_Z的博客-CSDN博客

一查原来发现可能是druid用的有问题 果断关闭之后再次运行程序观察。

符合预期完美解决。

 

5.结论

一个配置的错误饶了一大圈 说明还是对配置和底层不熟,不过只有遇到问题才能进步。

java visualVM 地址  visualvm 和jdk 对应版本下载地址列表 - 走看看

mat下载地址:Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一次通过jvm排查堆内存不断增大最后导致docker容器自动重启的问题 的相关文章

随机推荐