2018-12-18内存溢出排查过程记录

先说结论

Session采用redis管理之后,定时清理过期Session的逻辑需要优化。
CustomerShiroSessionDao.getActiveSessions()这个方法,
用于AbstractValidatingSessionManager.validateSessions()调用,
用于ExecutorServiceSessionValidationScheduler定时清理过期的Session
目前的实现逻辑是从redis中读取所有sessionKey,再遍历所有key一个个读取Session
Session数据量较大时,就会出现内存溢出。

排查过程

上周五遇到内存溢出,把内存调大,当时没出现了,今天又出现了,看来是个问题
java.lang.OutOfMemoryError: Java heap space
将内存dump到文件中,用JProfiler分析,发现大量的SysFunction对象,65万个实例,这个肯定不正常
内存溢出排查过程记录 - 图1

char[]共占用900M内存,其中99%是被String引用的

内存溢出排查过程记录 - 图2

而String占用内存368M+900M,大部分是被SimpleSession和SysFunction/SysRole引用的

内存溢出排查过程记录 - 图3

随便找几个SysFunction看一下引用关系

内存溢出排查过程记录 - 图4

跟踪GC Root

内存溢出排查过程记录 - 图5

从下图可以看出,都是ExecutorServiceSessionValidationScheduler创建的

这个类是用于定时清理过期的Session
内存溢出排查过程记录 - 图6

看一下CustomerShiroSessionDao.getActiveSessions(),难道我的redis有很多Session?

内存溢出排查过程记录 - 图7

找到SessionKey的前缀

内存溢出排查过程记录 - 图8

登录redis-cli,查询keys shiro_session:*

内存溢出排查过程记录 - 图9
内存溢出排查过程记录 - 图10
是挺多的。
但按说1712个Session也不应该溢出,实际上只加载了1390个Session就已经溢出了
内存溢出排查过程记录 - 图11

来看一下Session里面有哪些内容

内存溢出排查过程记录 - 图12
有一个currFunctions的List列表,还有一个curAllFunctions的List列表
SysFunction对象下面还有children和buttons两个List
内存溢出排查过程记录 - 图13
Session中放了太多东西了,看来这就是内存溢出的原因了。

改进建议

采用Redis管理session之后,有些逻辑需要优化,我在之前的项目中解决过
毕竟从本地JVM变成socket请求了,不是建一个CustomerShiroSessionDao就行的,需要全盘考虑
1、优化从Redis清理过期Session的逻辑,利用Redis的过期机制
创建Session时设定30分钟过期,用户访问时更新Session的过期时间(1分钟访问多次也只刷新一次)

2、利用线程变量缓存Session信息,同一个WEB请求只会从Redis读取一次Session信息
否则一个WEB请求会发几十次从Redis读取Session,Redis也会成为瓶颈

3、List信息不直接放在Session中
SysFunction用Map结构保存到Redis中,整个系统一共只有100多个(目前的存储方式是Session数量x100多)
Session中只保存当前用户对应的List
要用到的时候再根据List从Map结构中取List
4、不要直接缓存SysFunction这个DTO对象,建一个新对象,只放菜单名称,图标等必要信息
去掉createUser/createTime/updateUser/updateTime等用不到的字段

遗留问题

为什么会产生这么多Session呢?我的redis只是我自己在用,我只登录过一个账号。。。