JVM系列(二十四) JVM调优实战-Jprofiler内存泄漏问题定位

1,726 阅读3分钟

1.内存泄漏

上一篇文章,我们已经从Dump文件,已经能从多种方法定位到可能产生问题的对象,并且对问题对象产生的代码位置进行了定位

  • IncomingReferences 入引用,分析被谁调用 (总是记不住的就记 入谁)
  • OutgoingReferences 出引用,分析他引用的其他对象
  • Biggest Objects 大对象分析
  • Graph 对象调用图分析
  • Show Path To GC Root 根追踪

今天我们来分析下内存泄漏,首先什么是内存泄漏?

  • 本来该回收的对象,没有被回收
  • 对象可达性分析可达,但是对象是无用对象
  • 经过很多轮,没有回收后,一直停留在内存中,占用内存空间
  • GC垃圾回收永远不会回收该对象

内存泄漏会有什么影响?

  • 内存泄漏导致对象无法被回收,日积月累就导致内存空间不足,OOM
  • 程序无法正常运行
1.1 Java程序内存泄漏原因

Java内存泄漏的根本原因是长生命周期的对象持有短生命周期对象的引用,很可能发生内存泄漏

  • 长生命周期不会很快被销毁,那么短生命周期短时间也不会销毁
  • 尽管短生命周期对象已经不再需要,但是被长生命周期持有它的引用
  • 可达性分析该对象可达,从而导致不能 短生命周期不会被回收
  • 从而发生内存泄漏

2.静态集合类-内存泄漏

Java中非常经典的内存溢出就是 静态集合类的使用,造成内存泄漏,为什么静态集合类会容易出现内存泄漏呢?

因为静态变量的周期和程序运行的周期一致,程序运行多久,静态变量就会存活多久,他是不会被销毁的,这些变量持有的引用对象就无法被回收释放,所以他们一直被静态集合引用,就会比较容易导致内存泄漏

2.1 集合HashMap,Vector,List 泄漏

下面我们看下应用程序,分别列举了3种静态对象的内存泄漏的场景

  • HashMap内存泄漏
  • Vector内存泄漏
  • List内存泄漏

程序代码, 先构造一个大对象

package com.jzj.jvmtest.leaktest;

public class LeakObj {
    /**
     * 构造一个大对象
     */
    private byte[] myobj = new byte[2 * 1024 * 1024];

    public LeakObj() {
    }
}

然后启动Main方法,我们用HashMap来 测试,Main方法如下

package com.jzj.jvmtest.leaktest;

import java.util.*;

public class MemoryLeakTest {

    private static Map<Integer, LeakObj> objMap = new HashMap<>();
    private static Vector<LeakObj> objVector = new Vector<>();
    private static List<LeakObj> objList = new ArrayList<>();

    public static void main(String[] args) {
        testMap();

    }

    public static void testMap() {
        for (int i = 0; i < 100; i++) {
            LeakObj obj = new LeakObj();
            //把对象放到 map集合种
            objMap.put(i, obj);

            //然后把对象置为null
            obj = null;
        }
    }

    public static void testVector() {
        for (int i = 0; i < 100; i++) {
            LeakObj obj = new LeakObj();
            //把对象放到 map集合种
            objVector.add(obj);

            //然后把对象置为null
            obj = null;
        }
    }

    public static void testList() {
        for (int i = 0; i < 100; i++) {
            LeakObj obj = new LeakObj();
            //把对象放到 map集合种
            objList.add(obj);

            //然后把对象置为null
            obj = null;
        }
    }
}
2.2 单例模式内存泄漏

非正确方式使用单例模式会出现内存泄漏, 为什么会出现内存泄漏

  • 因为单例模式使用了静态方法,静态方法的生命周期和JVM程序的生命周期一样长,所以不会被回收,导致内存泄漏
  • 单例对象持有外部对象的引用
  • 外部对象不会被回收

下面我们看下单例模式的内存泄漏

  • B对象采用单例模式
  • B对象中有一个A 复杂对象的属性
  • B对象单例静态初始化的时候,设置了A
  • B对象就持有了A对象的引用
  • A对象生命周期就和B静态类一致,不会被回收
  • A就是内存泄漏

对象A Class信息

package com.jzj.jvmtest.leaktest;

public class A {

    public A() {
        //A的构造函数种 用B的单例模式去初始化
        B.getInstance().setA(this);
    }
}

对象B Class信息

package com.jzj.jvmtest.leaktest;

public class B {
    private A a;

    //B采用单例模式, 持有了A对象的引用, 那么A对象的生命周期就是和静态类B的生命周期一样
    //只有程序消亡,A对象才会被回收,否则A对象永远不会被回收
    //A对象就是内存泄漏
    private static B instance = new B();

    public B() {
    }

    /**
     * 调用getInstance 就会构造一个B ,但是B有一个属性a ,要怎么给a设置值?
     */
    public static B getInstance() {
        //静态方法 单例模式
        return instance;
    }

    public void setA(A a) {
        this.a = a;
    }
}
2.3 资源类未关闭Connect,IO链接,网络链接等

早期采用JDBC的方式连接Mysql数据库的时候,当不再使用链接的时候就要手动的释放Close掉数据库链接,只有链接关闭后,垃圾回收器才会对这部分对象进行回收

  • Mysql建立的Connection链接对象,需要手动的关闭
  • 用来执行SQL的Statemnt对象,需要手动的关闭
  • ResultSet用来接收结果的对象 ,需要手动的关闭
  • Mybatis的OpenSession获取SqlSession,需要手动的close
  • 文件流 InputStream输入输出流,使用完要关闭Close
2.4 内部类和外部模块的引用

内部类的引用是比较容易遗忘的一种,如果内部类没有释放,就可能导致一系列的后继类对象没有释放。从而产生内存泄漏

  • 内部类被外部类引用,也就是内部类调用了外部类
  • 如果外部对象不再使用,但是由于内部类持有外部类实例对象
  • 就导致外部类对象即使不使用,也不会被回收
  • 从而导致内存泄漏
  • 外部类就是泄漏的垃圾
2.5 缓存泄漏

经典问题就是ThreadLocal 没有释放资源导致的内存泄漏,因为ThreadLocal底层寸尺使用的是WeakHashMap来存储对象

  • ThreaLocal本身是不存储值的,我们在使用其对应的set、get方法时,都是操作的其对应的ThreadLocalMap对象
  • 弱引用在发生GC时,就会被垃圾回收掉
  • 在当前线程正在运行的时候,发生GC时,在ThreadLocal对象没有被其它地方强引用
  • 这个key指向ThreadLocal的虚引用就会立即断开(被垃圾回收掉),就会出现ThreadLocalMap中存在key为null的Entry对象
  • 只要当前线程不结束,该ThreadLocalMap对象就会一直存在,永远无法回收
  • 导致内存泄漏
  • 使用线程池的情况下,使用完ThreadLocal一定要使用remove方法即时清理

3.Jprofiler 定位内存泄漏

我们就是用第一个例子HashMap的泄漏用例来实战下,如何定位内存泄漏

首先启动程序, 设置JVM参数

-verbose:gc -XX:+UseG1GC -Xms20M -Xmx20M  -XX:+PrintGC -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:SurvivorRatio=8

然后找到OOM 内存溢出时后,生成的堆栈快照

  • 堆栈文件 java_pid12780.hprof
  • 进程PID=12780, Dump 就在项目文件夹
  • 找到日志输出文件 ,我的日志路径是项目路径下面 E:\myworkspace\distribute\jvmtest
  • 然后用Jprofiler打开 hprof文件
3.1 Biggest Objects 大对象定位

我们采用Biggest Objects对大对象进行定位 image.png

一层一层往下分析 image.png

可以分析得出 对象在HashMap中, Map中存储的是LeakObj对象, 该对象的byte[] 占用内存最大,就能定位到问题

3.2 使用Incoming References分析

选择Use Selected Objects -> incoming References 分析 image.png

找到Roo的根引用关系,点击Show PathS To GC Root image.png

然后会看到很多类的引用 找到自己定义的类 , 一般系统类不会出现问题,有问题大部分都是自己代码的问题

image.png

结果同样也可以发现, 是LeakObj中的HashMap的属性 objMap出现的问题

3.3 使用Outgoing References 分析出引用,他引用了哪些属性

看下这个大对象的出引用

  • 他引用了谁, 导致他为什么他占用这么大,
  • 内存这么大, 到底存储的是什么东西
  • 详细分析下大对象信息

首先 Use Selected Objects,选择Outgoing References image.png

可以看到 对象中存在3个属性

  • objList 里面 0个对象
  • objVector 里面0个对象
  • objMap 里面有6个对象,存放在table中,size=6
  • 剩余的就是ClassLoader类加载器 基本属性
  • superclass父对象Object 基本属性
  • 只有objMap有东西, 所以有问题一定是 objMap出了问题

image.png

看下objMap种存的啥,看下table对象中的信息

  • Map的 Key:Integer类型
  • Map的 value:LeakObj对象
  • LeakObj的对象有个属性 myobj
  • myobj属性就是 byte[] 数组,占用最多的东西

image.png

从而分析出 LeakObj的myobj在加入objMap的时候出现的问题, 只需要找到代码中加入的地方,就能定位内存溢出的问题


至此我们可以使用jprofile分析定位内存溢出的问题