线上应用GC异常,为了降低影响,期间我们重启了几次。但是发现,重启后,很快老年代就被打满,无法回收。
dump内存快照
使用jmap,报错没权限
后面使用arthas执行了heapdump
分析内存使用情况
使用mat(memoryAnalyzer Tool)分析内存使用情况
很明显就是出现了内存泄漏
ZkEventThread中存在大量的ZkEvent
ZkEventThread中维护了一个ZkEvent的队列,ZkEventThread会依次从该队列中取ZkEvent,调用每个ZkEvent的监听。
ZkEvent里都是什么
查看ZkEvent的内容,发现大量的
Data of xxx changed sent to xxx,对照ZkClienta的代码,不难发现,这些都是Zk节点的DataChange事件,事件对应的zk节点都是同一个
这些ZkEvent的实现,都是ZkClient中的匿名类,下面的截图内容,对应的是ZkEvent的description属性
ClientRunningMonitor$1
从上图和下图都可以看到,ZkEvent对应的listener都是ClientRunningMonitor$1,而且是同一个地址
看一下ClientRunningMonitor$1的引用
ZkEvent中引用了ClientRunningMonitor$1
ZkClient中的dataListener引用了ClientRunningMonitor$1
查看ClientRunningMonitor$1的Path to GC ROOTS
结果和上面的一直,只有ZkEvent和ZkClient中的dataListener引用了
排查方向
经过对内存使用、代码分析,
发现CanalClusterConnector已经没有引用,但是ClientRunningMonitor$1没有取消订阅。
后续的排查方向:
- 为什么会出现大量的zk节点变更事件
- 为什么会出现CanalClusterConnector已经关闭,但ClientRunningMonitor$1没有取消订阅
问题发生的原因
为什么会出现大量的zk节点变更事件
ClientRunningMonitor内发生了循环
为什么ClientRunningMonitor没有取消订阅
查看ClusterCanalConnector.connect的代码,
currentConnector.connect();会调用上述的initRunning,产生循环,抛出异常
发生异常后,currentConnector.disconnect();会取消zk订阅,中断循环,
但是在currentConnector.disconnect();调用之前accessStrategy.currentNode()就已经报错了,导致disconnect失败,取消订阅失败