背景
同事的项目voice-analyzer-web线上环境,监控系统频繁报警发生JVMFullGC卡顿次数频繁(4台服务器都报警),监控每10分钟统计一次,该时间段内Full GC超过10次就会报警。
线上JVM参数(有省略)
|
|
分配了4GB堆内存,年轻代1GB,老年代3GB,eden区800M,每个Survivor区100M,老年代占用率达到70%(2.1G左右)执行CMS GC
日志分析
初步分析gc.log
找一台线上机器下载gc.log(5M大小)到本地,推荐在线图像化分析gc地址geceasy)
|
|
观察频繁CMS GC相邻间隔时间8秒左右,检查CMS GC回收前后老年代内存使用情况:
|
|
老年代容量为3145728K,第一次在使用了2202154K时触发了CMS GC初始标记操作,第二次在使用了2202654K后触发CMS GC初始标记操作;2次CMS GC之间老年代反而增加了0.5M大小,初步怀疑方向:
CMSInitiatingOccupancyFraction比例大小,导致频繁触发- 该段时间有大量内存转移到老年代
- 堆内存泄漏

这张图比较直观展示老年代内存一直维持在2.1G左右,GC前后并没有降低老年代大小,而且这段时间并没有大量并发请求,怀疑堆内存泄漏。
分析heap dump
选择一台服务器,联系运维dump heap(2.4G),重启全部服务器(内存直线掉下来,老年代200M。。。)
使用工具VisualVm或者Eclipse MAT分析dump日志
使用VisualVM
- 检查堆内存大对象:

可见共有377094个double[]数组占有内存2G左右,retained size是该对象被GC之后所能回收到内存的总和;怀疑double[]对象泄漏,没有被回收
- 查看该对象的
GC Root:

上图可以看出该double[]被DataBuffer直接引用,最后被缓存在Guava LocalCache中;从GC Root可以看出,有ScheudleTask(DataPublisher中创建)引用了该对象
代码分析
- 进入
DataPublisher类中,检查ScheudleTask创建过程:
|
|
- 逆向检查
start()调用
由ServerStats类initialize方法触发
|
|
- 逆向检查
initialize()调用
调用关系如下:

由LoadBalancerStats类createServerStats方法触发
|
|
可以看出bufferSize=1000,Heap dump中每个double[]元素也是1000;而且,定时任务时间间隔为1秒
从缓存localcache的角度来看,如果元素一直增加,说明一直有新的Server被创建并添加到缓存中;但是据同事了解我们的Server只有10来个,缓存正常不会一直增长;怀疑是否代码问题导致不停创建新的Server?
- 检查
getServerStats调用关系
入口调用代码如下:
|
|
重点观察LoadBalancerCommand.submit()方法:
|
|
观察LoadBalancerContext.getServerStats()方法
|
|
- 检查
LoadBalancer创建过程
|
|
观察LoadBalancerBuilder.buildFixedServerListLoadBalancer()方法
|
|
从上面代码看,每隔3分钟就会重新创建新的LoadBalancer,每创建一个LoadBalancer,都会创建LoadBalancerStats作为LoadBalancer属性
难道每隔3分钟时间创建的Server对Guava LocalCache来说都是不同的吗?窃喜,感觉问题已经找到😄
现在需要确定2个WeightServer对象,具有相同的ip和port, 在LocalCache中是一个吗?
- 检查
WeightServer
具体的guava cache 源码不再叙述,整体逻辑设计参考CurrentHashMap
|
|
从上面看,2个WeightServer如果ip和port相同,经测试cache元素只有一个,guava cache认为是同一个元素。尴尬,猜错了😅。。。
重新检查代码
既然heap dump显示cache中的Server在不停增加,实际情况却是一个cache中只会有10来个Server;突然想起来,每个LoadBalancerStats对象都有一个cache对象
|
|
原因:定时任务执行initLoadbalance(),导致不停创建新的LoadBalancer,即LoadBalancerStats一直增加,全部cache缓存的Server也会增加
这里有一个问题,旧的cache为什么不被GC回收呢?回过头来看DataPublisher.start()方法:
|
|
从上面的代码看出,每个Server都关联了一个线程池执行定时任务,导致cache中Server对象一直被引用,GC不会回收这类对象。
比较有意思的地方:
|
|
这里的guava cache使用了expireAfterAccess和removalListener, 我猜robbin框架)作者本意是使用过期函数以及监听器在Server失效后关闭线程池运行,防止线程一直运行;但是guava cache的过期删除是被动的,就是说如果元素过期后再次被访问,会触发删除并重新加载
以该项目代码执行来看,只会一直添加新的cache,旧的cache不能被访问,导致缓存对象不能释放
解决方案
- 避免
LoadBalancer不停的创建,覆盖ServerList即可
|
|
结论
使用第三方框架,一定要了解底层运行机制。😄