22

记一次线上服务内存泄漏问题排查始末

 3 years ago
source link: https://club.perfma.com/article/2055798
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Java小能手21小时前

故障案发缘起

网关上线一周以来,运行一直稳定,从未出现CPU飙高的情况。发生故障的当天,CPU开始缓慢上升,但是上升的过程并不是直线,而是有下降,CPU下降一些后突然继续上升,到后期,CPU飙高速度开始加快,然后导致了其他连锁反应。
为什么之前七天程序运行正常,最后一天CPU突然飙高?
是程序运行逻辑有什么死循环吗?
没有,查看了小组伙伴新增的网关逻辑,代码都中规中矩,没有什么死循环。也没有使用线程不安全的并发容器。刚开始会怀疑,是不是使用了线程不安全的HashMap,触发了多线程下的Map链表死循环,但很快排除了这个问题,这是JDK1.8以前会遇到的。线上环境均使用1.8的JDK。
另外,如果程序真的有死循环逻辑,CPU从一开始就应该高啊,因为中途并没有改动过配置和组件集,因此,不存在这个问题。
但也实现没有想通,为什么程序的CPU会在多天以后突然上升。
当天小组的伙伴也在本地压测,通过jstack捕捉耗时线程来发现问题,但未有所获。

怀疑GC引起了CPU飙高

经过了长达半天的压力测试一无所获后,小组伙伴开始怀疑是否是GC造成的CPU飙高。因为有可能是程序执行一段时间后,某些原因引起了高频GC吃掉了机器的CPU。GC频繁可能是代码的对象分配不合理,也有可能是内存泄漏。

实时监测gc情况

image.png

找到正在进行压测的网关进程号,执行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数字代表在堆内存中实际的类的对象个数。

image.png

我们认识的一个类:ParameterizedTypeImpl怎么这么多?

这个类是用在fastjson的工具类中的,同时看到fastjson.IdentityHashMap的Entry对象也非常多,初步断定是在使用fastjson的时候遭遇内存泄漏。

从源码入手查找内存泄漏根源

网关里,有一个实现是json转set:

image.png

实际上fastjson并没有提供json换set,但是提供了json转list,但是fastjson开放了API允许你转换成指定的类型。所以我们组开发伙伴就编写了这个方法,用于把json串转set。

为什么内存泄漏,跟代码吧:

image.png

跟,发现fastjson根据type拿了一个反序列化器。

image.png

继续跟进去,发生fastjson根据type缓存了反序列化器,缓存的容器是一个Map

image.png

好了,突然想到,如果fastjson缓存了反序列化器,但是我们的ParameterizedTypeImpl没有重写hashcode和equal方法。那么map判重时则无法判断是不是一个对象。因为工具类里使用时,每次调用都会new一个ParameterizedTypeImpl

好了,问题解决。但是,组内伙伴重写了hashcode和equal方法去测试,但仍然内存泄漏?嗯??????

内存泄漏原因

不得已,只能去翻这个IdentityHashMap的源码。发现,这是fastjson自己造的一个Map。最终发现,这个Map在判断key相等时直接比较的是地址值key==entry.key,问题解决。你重写hascode和equal也没用啊,人家直接比较地址值。

image.png

为什么fastjson自己造的map直接比较地址值而不是equal呢?我估计是因为fastjson内部的map用来缓存的东西key都是Type吧。和类型相关的,在运行时是唯一的,因此比较地址值更快,不然就不fast了。谁曾想到,我们自己每次都new了一个ParameterizedTypeImpl,不仅内存泄露,还影响性能:没有享受到缓存的红利。

image.png

以上就是问题排查的始末。

经验:在使用一个类库或者工具时,我们一定要先搞清楚这些情况:是否单例,是否线程安全,是否不可变类,是否能定义成全局变量。(毕竟,有状态的对象不能这么干)

本文来自公众号: chen陈序猿


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK