记一次线上服务内存泄漏问题排查始末原创
故障案发缘起
网关上线一周以来,运行一直稳定,从未出现CPU飙高的情况。发生故障的当天,CPU开始缓慢上升,但是上升的过程并不是直线,而是有下降,CPU下降一些后突然继续上升,到后期,CPU飙高速度开始加快,然后导致了其他连锁反应。
为什么之前七天程序运行正常,最后一天CPU突然飙高?
是程序运行逻辑有什么死循环吗?
没有,查看了小组伙伴新增的网关逻辑,代码都中规中矩,没有什么死循环。也没有使用线程不安全的并发容器。刚开始会怀疑,是不是使用了线程不安全的HashMap,触发了多线程下的Map链表死循环,但很快排除了这个问题,这是JDK1.8以前会遇到的。线上环境均使用1.8的JDK。
另外,如果程序真的有死循环逻辑,CPU从一开始就应该高啊,因为中途并没有改动过配置和组件集,因此,不存在这个问题。
但也实现没有想通,为什么程序的CPU会在多天以后突然上升。
当天小组的伙伴也在本地压测,通过jstack捕捉耗时线程来发现问题,但未有所获。
怀疑GC引起了CPU飙高
经过了长达半天的压力测试一无所获后,小组伙伴开始怀疑是否是GC造成的CPU飙高。因为有可能是程序执行一段时间后,某些原因引起了高频GC吃掉了机器的CPU。GC频繁可能是代码的对象分配不合理,也有可能是内存泄漏。
实时监测gc情况
找到正在进行压测的网关进程号,执行jstat -gc pid interval 就可以动态查看gc情况了。
例如上图。每一列的含义简单例举一下:
S0C:堆内存中年轻代第一个survivor区的容量。
S1C:堆内存中年轻代第二个survivor区容量。
S0U和S1U是对应的实际使用量。
EC和EC分别是Eden区的容量和实际使用量。
OC是OLD(年老代)代容量,OU是实际使用量。
YGC是截止到现在,程序发生YGC的次数。
FGC是截止到现在,程序发生Full GC的次数。
别的列先不说,这里先说一下入门级别的大部分情况下的javaGC的一些规律:
对象是从eden区创建,然后到s0最后到s1,如果多次垃圾回收后对象仍然存活,则有机会进入年老代。
eden区满了以后发生一次young GC。
old区满了会发生一次full GC (应用程序会停顿,俗称 stop the world)。
上面的例子我指定了-Xmx=1M,便于发现问题。
小组伙伴在压测试时,发现了一个问题:
old区的对象在多次full gc后发现内存释放不掉,越来越多。一定存在内存泄漏。
使用visualVM监测内存对象情况
使用命令jmap -dump:format=b,file=gateweyxxxxxx [pid] 导出了压测过程中的堆快照文件,下载到本地,使用visualVM打开:
count数字代表在堆内存中实际的类的对象个数。
我们认识的一个类:ParameterizedTypeImpl怎么这么多?
这个类是用在fastjson的工具类中的,同时看到fastjson.IdentityHashMap的Entry对象也非常多,初步断定是在使用fastjson的时候遭遇内存泄漏。
从源码入手查找内存泄漏根源
网关里,有一个实现是json转set:
实际上fastjson并没有提供json换set,但是提供了json转list,但是fastjson开放了API允许你转换成指定的类型。所以我们组开发伙伴就编写了这个方法,用于把json串转set。
为什么内存泄漏,跟代码吧:
跟,发现fastjson根据type拿了一个反序列化器。
继续跟进去,发生fastjson根据type缓存了反序列化器,缓存的容器是一个Map
好了,突然想到,如果fastjson缓存了反序列化器,但是我们的ParameterizedTypeImpl没有重写hashcode和equal方法。那么map判重时则无法判断是不是一个对象。因为工具类里使用时,每次调用都会new一个ParameterizedTypeImpl
好了,问题解决。但是,组内伙伴重写了hashcode和equal方法去测试,但仍然内存泄漏?嗯??????
内存泄漏原因
不得已,只能去翻这个IdentityHashMap的源码。发现,这是fastjson自己造的一个Map。最终发现,这个Map在判断key相等时直接比较的是地址值key==entry.key,问题解决。你重写hascode和equal也没用啊,人家直接比较地址值。
为什么fastjson自己造的map直接比较地址值而不是equal呢?我估计是因为fastjson内部的map用来缓存的东西key都是Type吧。和类型相关的,在运行时是唯一的,因此比较地址值更快,不然就不fast了。谁曾想到,我们自己每次都new了一个ParameterizedTypeImpl,不仅内存泄露,还影响性能:没有享受到缓存的红利。
总结
以上就是问题排查的始末。
经验:在使用一个类库或者工具时,我们一定要先搞清楚这些情况:是否单例,是否线程安全,是否不可变类,是否能定义成全局变量。(毕竟,有状态的对象不能这么干)
本文来自公众号: chen陈序猿