背景
同事的项目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
即可
|
|
结论
使用第三方框架,一定要了解底层运行机制。😄