zoukankan      html  css  js  c++  java
  • 线上内存泄露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()方法。

  • 相关阅读:
    LINUX 内核守护进程
    LINUX 内核 API
    LINUX IO 图解
    doxygen
    xtrace
    Dapper-translation 分布式监控系统
    矩表
    最流行的5个前端框架对比
    2017年前端框架、类库、工具大比拼
    Top 10 JavaScript编辑器,你在用哪个?
  • 原文地址:https://www.cnblogs.com/UniqueColor/p/13577355.html
Copyright © 2011-2022 走看看