一.问题
书接上文,没想到吧居然还有下集😬😬
昨天这个Excel导出的功能又内存溢出了,我都要哭了
我这次决定规规矩矩,好好地找一下到底是什么原因造成了内存溢出
二.分析问题
- 百度研究了一下,按照步骤操作了一波
- 首先需要生成一个dump文件用于内存分析
- 在项目运行配置中修改配置
修改项目运行时的配置文件
配置 | 说明 |
---|---|
-Xms200m | 设置内存最小值 |
-Xmx200m | 设置内存最大值 |
-XX:+HeapDumpOnOutOfMemoryError | 出现内存溢出的时候生成dump |
-XX:HeapDumpPath=/Users/xfn/Download/ | dump文件生成的位置 |
- 请求内存溢出的接口,系统内存溢出(如果没有溢出就尝试把上面的内值调小一点),可以看到已经生成了一个hprof结尾的文件
三.下载分析工具
- mat是一个很好用的内存分析工具,这里附上下载地址
这里需要注意一下mac电脑直接打开可能会报错,需要右键显示包内容然后运行MemoryAnalyzer
- 导入dump文件
-
这么多的功能按钮我也不是特别清楚是什么作用,但是这个Leak Suspects可以精确到内存溢出的代码
-
这里列出了几个可能造成内存溢出的地方,点击See stacktrace可以看到详细的报错行
- 分析这算错误信息,可以看出来,POI导出占用了一部分的内存,但是这部分内存是必须占用的,已经修改为SXSSF,已经不太好继续优化了,所以接着往下看
- 分析另一个问题,这里好像是因为产生了过多的VcJv这个对象造成的内存消耗,这么一看好像就是这里的可能性大一点了
- 按照异常的提示一步步知道出现异常的代码行,但是这个只是一个查询操作,理论上不应该出现内存溢出的问题,又没有生成那么多的对象
- 到这为止我又没有什么头绪了,只能开始排查代码,我发现了一块代码在循环里面执行了对象的初始化操作,这种操作简直恐怖,你这内存咋能不溢出呢
-
正当我准备开始改这段代码的时候,测试和我说了个很重要的信息,说我改过之后比之前更容易出现内存溢出这个问题,之前2w条数据才会出现内存溢出,现在1w不到就出现了(什么!你是不是觉得我的代码写的垃圾!!!不对。。。。我好想知道是什么原因了)
-
这是一段请求超时降级的代码,是我加上去的,就是说当这个请求超过6s还没有请求结束,就终止这个请求提前给前端响应,然后开始一个新的线程继续执行这个任务,并将下载的文件发送到文件服务器
public String degradeExportAssSubAll(String userName, String defaultsobid, String begin, String end, String asscategory, String export, HttpServletResponse response) {
FutureTask<String> future = new FutureTask<>(() -> {
// 直接下载到本地
exportAssSubAll(userName, defaultsobid, begin, end, asscategory, export, response, false);
return "0";
});
// 执行这个任务
XFNThreadPool.excelPool.execute(future);
try {
// 监听任务执行的时间
future.get(timeOut, TimeUnit.MILLISECONDS);
} catch (Exception e) {
// 如果超时,中断这个任务
future.cancel(true);
// 执行降级策略,下载到协同服务
XFNThreadPool.exportFallBackPool.execute(() -> exportAssSubAll(userName, defaultsobid, begin, end, asscategory, export, response, true));
// 抛出异常给前端
return "16814";
}
return "0";
}
- 理论上FutureTask提供的这个cancel(true)方法应该是可以终止这个任务的,那如果这行代码没有终止任务,是不是就会出现测试说的这种现象,于是我翻阅了这段源码,发现他是用interrupt()来终止线程的(原来就是你造成的原因啊)
interrupt:仅仅是在当前线程中打一个停止标记,并不是真正的停止线程,也就是说,线程并不会立即终止,而是通知目标线程,有人希望你终止
-
我觉得可能就是这段造成的内存溢出,任务刚开始执行的时候,超时,但是这个任务并没有真的被中断,内存并没有被释放掉,然后我又开始了一个线程重新执行这个任务,从而导致的内存溢出
-
修改代码,将数据原先的数据查询和导出拆分开为查询任务和导出任务,只监控查询任务,当查询任务超时时,不中断这个任务,提前给前端响应,然后等查询任务执行结束,获取返回值,再调用导出任务,这么写避免了任务执行2次而产生的内存溢出问题,也提高了效率
public String degradeExportAssSubAll(String userName, String defaultsobid, String begin, String end, String asscategory, String export, HttpServletResponse response) {
// 创建一个异步监听任务
FutureTask<JSONArray> future = new FutureTask<>(() -> {
// 执行数据查询任务
return getAssSubAll(defaultsobid, begin, end, asscategory);
});
// 执行这个任务
XFNThreadPool.excelPool.execute(future);
try {
// 监听任务执行的时间
future.get(timeOut, TimeUnit.MILLISECONDS);
// 任务没有超时,将Excel下载到本地
exportAssSubAll(begin, end, userName, export, defaultsobid, false, future.get(), response);
} catch (Exception e) {
log.info("任务执行超过" + timeOut + "ms.....抛出异常" + e.toString() +"......开始执行降级任务....");
// 任务超时,将Excel下载到协同服务
try {
// 创建一个新的线程,执行导出方法
XFNThreadPool.excelPool.execute(() -> {
try {
// 等待异步任务执行结束,获取结果数据,再执行导出任务
exportAssSubAll(begin, end, userName, export, defaultsobid, true, future.get() , response);
} catch (Exception ex) {
ex.printStackTrace();
}
});
// 提前给前端响应
return "16841";
} catch (Exception ex) {
ex.printStackTrace();
}
}
return "0";
}
- 上测试环境调试,发现在3w+数据的情况下,内存也不会溢出
四.总结
你以为我想说的是mat的使用?其实我想说的是FutureTask😬😬
讲道理遇到问题还是一步步理性分析的好