1.案例背景
在一次系统测试过程中,测试人员反馈平台业务功能不可用,请求协助排查。
我首先查看运管平台-状态监控,检查服务运行情况,发现状态显示服务运行正常,初步判断可能是服务假死造成的,而能造成服务假死的多半是jvm出问题了。查看运管日志,发现异常日志出现Java heap space,即jvm堆内存溢出。本文将介绍一下我的分析思路与处理过程。
2.排查思路
首先,根据本人目前所掌握的jvm知识与经验,猜测引起内存溢出有以下几种情况:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 使用的第三方软件中的BUG;
- 启动参数内存值设定的过小;
2.1 异常信息收集
根据组件启动脚本可知,在JAVA_OPTIONS变量中已配置了HeapDumpOnOutOfMemoryError参数,当组件服务内存溢出时,将会存储dump文件,可以通过分析dump文件进行问题排查。
-XX:+HeapDumpOnOutOfMemoryError
2.2 DUMP文件分析
获取Dump文件后,使用工具MAT(MemoryAnalyzer)加载dump文件进行内存分析。
通过MAT工具的Leak Suspects,该模块会自动分析内存溢出可疑点并给出一份可疑的分析报告。
从上面的截图中可以得出如下关键信息点:
- 可以明确,堆的总大小约为1G。
- 其中一个名为XNIO-2 task-113的线程内部持有一个List,该List中存放的对象占据了917.9MB内存,导致其他线程无法再申请资源,抛出堆内存溢出错误。
- 服务基于springboot框架开发,web容器为undertow,undertow底层便是使用的XNIO进行异步IO处理,而xnio task线程抛出异常说明是由一次http请求触发的。
- List变量中存放的是org.postgresql.jdbc.PgResultSet对象,PgResultSet表示pg数据库返回封装的结果集。从这一点可以初步推测,此次内存溢出的原因可能是在一次http请求查询数据时,从数据库一次查询出了太多数据导致了内存溢出。
2.3 线程栈分析
接下来,我通常的做法是直接去查看线程栈信息,通过线程栈可以定位到异常发生的线程入
口。
同样,通过MAT工具进入Thread Stack界面。查看上面提到的XNIO-2 task-113的线程栈信息,程序在执行ExportController.exportAlarmList()方法中的AlarmServiceImpl.queryAlarmListWithResult()时导致了内存溢出异常。
2.4 定位问题代码
找到ExportController.exportAlarmList()代码块(此处代码做了业务简化)。
/**
* 导出报警记录
* @param param
* @return
*/
public BaseResult exportAlarmList(@RequestBody @Valid
AlarmExportParam param) {
// 封装参数
AlarmQueryParam alarmQueryParam = newAlarmQueryParam();
BeanUtils.copyProperties(param, alarmQueryParam);// 设置pageNo和pageSize
alarmQueryParam.setPageNo(AlarmConstants.EXPORT_PAGE_NO);
alarmQueryParam.setPageSize(AlarmConstants.EXPORT_PAGE_SIZE);
BasePage<AlarmDTO> page = alarmService.queryAlarmListWithResult(alarmQueryParam);
ExportResultVO exportResultVO = alarmDownloaderExport.getExportResult(AlarmDTO.class, age.getList(), param.getExportHasPicture());
return BaseResult.success(exportResultVO);
}
根据线程栈信息可知,程序在调用alarmService.queryAlarmListWithResult()方法时发
生溢出,该方法是一个分页查询接口,那么进一步判断,分页每页查询数量可能过大。
BasePage<AlarmDTO> page = alarmService.queryAlarmListWithResult(alarmQueryParam);
查看AlarmConstants.EXPORT_PAGE_SIZE参数赋值。
public class AlarmConstants {
public static final Integer EXPORT_PAGE_SIZE =Integer.MAX_VALUE;
}
根据此处代码可知,原来是在分页查询报警记录时,分页每页查询数量设置成了Integer最
大值,当查询的数据很大时,相当于查询了全部记录并返回。
2.5 解决方案
- 修改分页每页查询数量,设置为1000
- 调整组件服务堆内存最大空间大小为1.5G
3.总结
- 总体上来说,产生内存溢出是由于代码写的不好造成的,因此提高代码的质量是最根
本的解决办法. - 研发人员在开发与自测过程中,需要关注边界条件。