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

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

738009

故障案发缘起

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

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

为你推荐

关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
字符串字面量长度是有限制的
前言 偶然在一次单元测试中写了一个非常长的字符串字面量。 正文 在一次单元测试中,我写了一个很长的字符串字面量,大概10万个字符左右,编译时,编译器给出了异常告警 `java: constant
多次字符串相加一定要用StringBuilder而不用-吗?
今天在写一个读取Java class File并进行分析的Demo时,偶然发现了下面这个场景(基于oracle jdk 1.8.0_144): ``` package test; public c
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得
谨防JDK8重复类定义造成的内存泄漏
概述 如今JDK8成了主流,大家都紧锣密鼓地进行着升级,享受着JDK8带来的各种便利,然而有时候升级并没有那么顺利?比如说今天要说的这个问题。我们都知道JDK8在内存模型上最大的改变是,放弃了Perm
高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和
「每日五分钟,玩转 JVM」:久识你名,初居我心
聊聊 JVMJVM,一个熟悉又陌生的名词,从认识Java的第一天起,我们就会听到这个名字,在参加工作的前一两年,面试的时候还会经常被问到JDK,JRE,JVM这三者的区别。JVM可以说和我们是老朋友了
JVM垃圾回收与一次线上内存泄露问题分析和解决过程
本文转载自:花椒技术微信公众号 前言内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。Ja