背景
在服务器上部署用户中心后发现其内存占用比其他Springboot应用都要高,且启动后内存占用自动升高,存在异常
排查
用arthas分析一下jvm内存情况,发现分配的内存远小于已使用的内存,老年代空间使用也不多,初步推迟是新生代空间占用过多导致内存居高不下
既然如此,导出堆内存快照分析一下内存里都有哪些对象占用了内存
堆内存快照导入jprofile后发现,被GCRoot引用的内存只有104M(总的堆内存占用有800多M),固存在大量临时内存,再看看哪些地方引用了byte[]
未发现明显异常,均是第三方类库的引用
为什么会存在大量byte[]呢?什么情况下会产生大量的byte[]数据呢?网络IO?文件IO?
网络IO就是mysql和redis会有,文件IO是否有也未知
尝试启动项目后断开VPN,这样就不会有网络IO了吧
结果内存情况依旧如此,网络IO—pass
本地跑下项目用jprofiler监控一下看看
观察到byte[]占用的内存会不断增多,但总内达到800多M时会触发young gc(差不多两分钟一次)
再看看CPU视图下的调用树
就是Springboot启动和一些业务线程的调用。
但是,devtools?不是用来热部署的么,正好项目中有引入spring-boot-devtools的依赖,难道是热部署一直扫描jar包外的文件的IO操作导致的byte[]内存升高?
带着猜测去除依赖验证一下
果然如此。。
解决办法
三种方法:
- 取消依赖
- 关闭热部署
spring.devtools.restart.enabled: false
- 调大文件轮训间隔时间
spring.devtools.restart.pollInterval: 1s(轮询类路径更改之间等待的时间量,默认1s。实测调整为10s后,20分钟才会young gc)
spring.devtools.restart.quietPeriod: 400ms(在触发重启之前,在不更改任何类路径的情况下所需的安静时间,默认400ms)
刨根问底
那么问题就来了,是什么原因导致的内存泄露呢?
通过阅读devtools的源码,发现有一个File Watcher的线程,轮训的去扫描指定目录下文件并和上一次扫描的文件比较,来判断文件是否有变化
怀疑是在这个线程中,scan方法不断获取新的文件内容(current = getCurrentSnapshots();),导致新生代内存中files不断增加,内存溢出
尝试主动停止这个线程试试
重启项目,内存不在飙升
看看最新版本的devtools有没有优化这个问题,
FileSystemWatcher.java
很遗憾,这块代码逻辑没变。。。
在issue中发现有同样性能问题:https://github.com/spring-projects/spring-boot/issues/9882
最终决定使用基于NIO的文件监视器来解决这个问题,然而目前并未得到解决