性能文章>记一次线上服务内存泄漏问题排查始末>

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

9752010

故障案发缘起

网关上线一周以来,运行一直稳定,从未出现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陈序猿

点赞收藏
Java小能手
请先登录,感受更多精彩内容
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

随机一门技术分享之Netty

随机一门技术分享之Netty

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

10
0