线上内存泄露bug分析记录

一、问题出现

pod内存与cpu几乎同时开始飙升,同时到限制的峰值:

 

二、假想

可能是主键加密与雪花ID的新增特性导致CPU和内存不够?

 

由于上图报错可追溯到的自己的代码最近一行就是hzero抛出,一度怀疑是主键加密导致的性能问题,于是把服务器配置从2c8G加到8c16G,CPU无峰值限制,结果不到1小时还是出现内存和CPU使用同时达到峰值。

对主键加密压测后得到:100的线程组循环100次,CPU耗费在30%左右。所以基本上排除了主键加密。

刚开始走的弯路很多,一直以为CPU是由于用户操作产生,所以一直在分析接口占用CPU的损耗。但是这里很特殊,内存和CPU几乎同时达到峰值,而接口(比如导出Excel,优化了jvm内存占用后)虽然消耗CPU,但是并不吃内存,很明显不会同时飙升。

 

三、正确的思路

使用jmap分析jvm具体信息

jmap -histo[:live]  [pid]
// 打印每个class的实例数目,内存占用,类全名信息. VM的内部类名字开头会加上前缀”*”. 如果live子参数加上后,只统计活的对象数量. 

 

 

 

 拉取heap信息,由于.phd文件太大,可借助工具IBM HeapAnalyzer打开

 

 

 

使用IBM HeapAnalyzer分析heap情况

 

 

 

 最强大之处在于,Reference Tree->Leak Suspect,找到泄露嫌疑最大的那个对象,即上图的HashMap对象。层层展开后,找到最底层的对象,就可以找到你们熟悉的类了。

 

定位到这个类后,代码排除到这个类的所有接口,就得到了以下优美的图形

 最终得出的结论:hashMap内存泄露2个G,导致jvm疯狂gc,cpu消耗也要跟着涨。

 

四、查找代码原因

查看代码,分析HashMap泄露的真正原因:

当temp>now时,相当于写了while(true),HashMap实例化越来越多,最终导致崩盘。

找到代码原因后,在测试环境重现,访问有问题的项目,得到如下信息:

pod各线程CPU、内存消耗:

top -Hp [Pid]

 

可以看到jvm gc尤为醒目。

再次通过jmap查看java对象占用内存信息,可清晰查看到HashMap内存占用异常情况。

查看pod内存与cpu,几乎要到峰值,而且还在不断上升,验证了上面的猜想。 

$ kubectl top po -n namespaces

    podName                               cpu           mem

  agile-service-ede46-59d95cdc86-lvmrz   2901m        3365Mi

 

五、jvm具体监控

监控方法:

使用jstat命令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控;

Jstat –class <pid>:显示加载class的数量,及所占空间等信息;

Jstat –gc <pid>:显示gc相关的堆信息,查看gc的次数,及时间;

Jstat -gcutil <pid>:统计gc信息;

Jstat –gccause <pid>:统计gc信息(同gcutil),同时显示最后一次或当前正在发生的垃圾回收的诱因;

Jstat –gc –h2 <pid> 3000 10:每2次打印一次表头,每3秒输出一次结果,一共输出10次;

如:

 

参数解释:

S0C:年轻代中第一个survivor(幸存区)的容量 (字节)

S1C:年轻代中第二个survivor(幸存区)的容量 (字节)

S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (字节)

S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (字节)

EC:年轻代中Eden(伊甸园)的容量 (字节)

EU:年轻代中Eden(伊甸园)目前已使用空间 (字节)

OC:Old代的容量 (字节)

OU:Old代目前已使用空间 (字节)

PC:Perm(持久代)的容量 (字节)

PU:Perm(持久代)目前已使用空间 (字节)

YGC:从应用程序启动到采样时年轻代中gc次数

YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)

FGC:从应用程序启动到采样时old代(全gc)gc次数

FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)

GCT:从应用程序启动到采样时gc用的总时间(s)

NGCMN:年轻代(young)中初始化(最小)的大小 (字节)

NGCMX:年轻代(young)的最大容量 (字节)

NGC:年轻代(young)中当前的容量 (字节)

OGCMN:old代中初始化(最小)的大小 (字节)

OGCMX:old代的最大容量 (字节)

OGC:old代当前新生成的容量 (字节)

PGCMN:perm代中初始化(最小)的大小 (字节)

PGCMX:perm代的最大容量 (字节)

PGC:perm代当前新生成的容量 (字节)

S0:年轻代中第一个survivor(幸存区)已使用的占当前容量百分比

S1:年轻代中第二个survivor(幸存区)已使用的占当前容量百分比

E:年轻代中Eden(伊甸园)已使用的占当前容量百分比

O:old代已使用的占当前容量百分比

P:perm代已使用的占当前容量百分比

S0CMX:年轻代中第一个survivor(幸存区)的最大容量 (字节)

S1CMX :年轻代中第二个survivor(幸存区)的最大容量 (字节)

ECMX:年轻代中Eden(伊甸园)的最大容量 (字节)

DSS:当前需要survivor(幸存区)的容量 (字节)(Eden区已满)

TT: 持有次数限制

MTT : 最大持有次数限制

 

六、HashMap典型内存泄漏案例

 1 /**
 2  * HashMap的内存泄露
 3  */
 4 public class HashMapLeakTest {
 5  6     public static void main(String[] args) {
 7         Map<HashKey, Integer> map = new HashMap<HashKey, Integer>();
 8         HashKey p = new HashKey("zhangsan","12333-suu-1232");
 9 10         map.put(p, 1);
11         p.setName("lisi"); // 因为p.name参与了hash值的计算,修改了之后hash值发生了变化,所以删除不掉
12         map.remove(p);
13 14         System.out.println(map.size());
15     }
16 }
17 18 /**
19  * key 类
20  */
21 class HashKey {
22     private final String id;
23     private String name;
24 25     public HashKey(String name, String id) {
26         this.name = name;
27         this.id = id;
28     }
29 30     public void setName(String name) {
31         this.name = name;
32     }
33 34     @Override
35     public int hashCode() {
36         return name.hashCode()+id.hashCode();
37     }
38 }
从存储原理我们可以分析出,数据在数组中的存放位置,是取决于Key对象 hashCode() 方法的。一个对象的hashCode() 方法的值,一般来说都是和对象的内容相关的。那么,如果Key对象的成员取值变化了,它的hashCode() 基本上也会变化。
由上可分析:这个对象hashCode在存放到HashMap之后发生了变化,我们去remove时按照新的hash值进行查找,而该对象是按照之前的旧的hash值存放在HashMap中,hash值发生了变化,导致查找的索引位置时不一样的,于是在HashMap中就无法这个对象,因此也就没有办法删除该节点。
解决:重写hashCode或equals方法
 @Override
 public int hashCode() {
   return id.hashCode();
 }

所以自定义Object作为HashMap的KEY一定注意重写hashCode()或equals()方法。

posted @ 2020-08-28 14:41  UniqueColor  阅读(404)  评论(0编辑  收藏  举报