1、问题现象
在平台导入60w的人员,子系统A同时会生成对应数量用户。发现系统运行一段时间后,就会因为OOM而崩溃,由于JAVA 启动参数中设置了 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=XXX
,所以可以看到有生成对应的堆快照(.hprof)文件。
2、解析堆快照
- 不管三七二十一,先把堆快照拿来看一下。这里使用 Eclipse的 Memory Analyzer Tool解析堆快照文件。
- 解析后发现,有内存溢出前的报错,定位到代码的具体位置如下:
– 启动了一个固定周期的定时任务,检查用户有效期是否过期的判断。
// 静态代码块:
{
// 定时检查用户有效期,若过期推送消息
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
handleUserPeriod();
} catch (BusinessException e) {
logger.info("处理用户有效期失败:{}", e.getMessage());
}
}
}, 5, 30, TimeUnit.MINUTES);
}
– 在handleUserPeriod()
方法中,首先是一行:
List<User> existUserList = this.baseDao.baseOperator().selectList(new EntityWrapper<>());
... // 后面就是在内存中遍历用户,判断用户是否有效期过期的操作。
这会将所有用户都加载至内存中。至此可见,正好是这里报的OOM错误。这在用户量少的时候没有问题,但是当用户量大且JVM堆内存不足够匹配时,就会导致问题。
3、解决方案
问题:
1、一次性读取了全量的用户
2、判断用户有效期过期的操作是在内存中进行。
针对上述问题的解决办法:
1、不一次性读取全量数据,采用分批读取的办法。(时间换空间,IO更多了但也影响不大)
2、将判断用户过期的逻辑直接放在sql语句中,可以保证只读取需要的过期用户,忽略正常的用户。
3、原先使用select * 查出所有的字段,而后发现后续其实只需要用到用户“id”和“在线状态”字段,所以其实可以只读取出需要的字段,避免不必要的读取的IO消耗。
4、代码改造
根据上述思考,改进的伪代码如下:
public void handleUserPeriod() throws BusinessException {
logger.info("查询过期用户开始!");
boolean needHandle = true;
while (needHandle) {
Date currentTime = new Date();
// 这里为了避免OOM,限制每次最多查询 EXPIRED_USER_EACH_TIME (这里取1000)个过期用户
List<User> existExpiredUserList = this.baseDao.getExpiredUserList(currentTime, EXPIRED_USER_EACH_TIME);
logger.info("查询到过期用户数量:" + existExpiredUserList.size());
handleExpiredUsers(existExpiredUserList, currentTime);
if (existExpiredUserList.size() < EXPIRED_USER_EACH_TIME) {
needHandle = false;
}
}
}
5、添加数据验证
添加60W的用户,对比改造前和改造后的情况。发现改造前确实会OOM,而改造后的内存占用影响很小。至此大功告成。