JVM 12:内存泄漏分析

1. 内存分析工具

使用 jmap –histo 这种命令可以分析哪些对象占据着我们的堆空间。但是那是比较容易分析的问题,如果是遇到内存情况比较复杂 的情况,命令的方式是看不出来的,这个时候我们必须要借助一下工具。当然前提是通过 jmap 命令把整个堆内存的数据 dump 下来。

1.1 MAT 简介

MAT(Memory Analyzer tool) 工具是基于 Eclipse 平台开发的,本身是一个 Java 程序,是一款很好的内存分析工具,所以如果你的堆快照比较大的话,则需要一台内存比较大的 分析机器,并给 MAT 本身加大初始内存,这个可以修改安装目录中的 MemoryAnalyzer.ini 文件。
下载地址https://www.eclipse.org/mat/downloads.php
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
柱状图
在这里插入图片描述

2. MAT 中的 Incoming/Outgoing References

在柱状图中,我们看到,其实它显示的东西跟 jmap –histo 非常相似的,也就是类、实例、空间大小。
但是 MAT 有一个专业的概念,这个可以显示对象的引入和对象的引出。
在 Eclipse MAT 中,当右键单击任何对象时,将看到下拉菜单。如果选择“ListObjects”菜单项,则会注意到两个选项:

  • with incoming references 对象的引入
  • with outgoing references 对象的引出

2.1 代码示例

package demo;

/**
 * Incoming Vs Outgoing References
 */

class A {
    private C c1 = C.getInstance();
}

class B {
    private C c2 = C.getInstance();
}

class C {
    private static C myC = new C();
    public static C getInstance() {
        return myC;
    }
    private D d1 = new D();
    private E e1 = new E();
}

class D {
}

class E {
}

public class MATRef {
    public static void main(String[] args) throws Exception {
        A a = new A();
        B b = new B();
        Thread.sleep(Integer.MAX_VALUE);//线程休眠
    }
}

代码中对象和引用关系如下:
对象 A 和对象 B 持有对象 C 的引用
对象 C 持有对象 D 和对象 E 的引用
在这里插入图片描述
我们具体分析对象 C 的 Incoming references 和 Outgoing references 。
1、 程序跑起来
在这里插入图片描述
2、 MAT 连接上(MAT 不单单只打开 dump 日志,也可以打开正在运行的 JVM 进程,跟 arthas 有点类似,效果是一样的,只是一个是动态的,一个是日志导出那个时刻的)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.2 incoming references

在这里插入图片描述
在这里插入图片描述
对象 C 的 incoming references 为对象 A、对象 B 和 C 的类对象(class)

2.3 outgoing references

在这里插入图片描述
在这里插入图片描述
对象 C 的 outgoing references 为对象 D、对象 E 和 C 的类对象(class)
这个 outgoing references 和 incoming references 非常有用,因为我们做 MAT 分析一般时对代码不了解,排查内存泄漏也好,排查问题也好,垃圾回收中有一个很重要的概念,可达性分析算法,那么根据这个引入和引出,我就可以知道这些对象的引用关系,在 MAT 中我们就可以知道比如 A,B,C,D,E,F 之间的引用关系图,便于做具体问题的分析。

3. MAT 中的浅堆与深堆

  • 浅堆(shallow heap)
    代表了对象本身的内存占用,包括对象自身的内存占用,以及“为了引用”其他对象所占用的内存。
  • 深堆(Retained heap)
    是一个统计结果,会循环计算引用的具体对象所占用的内存。但是深堆和“对象大小”有一点不同,深堆指的是一个对象被垃圾回收后,能够释放的内存大小,这些被释放的对象集合,叫做保留集(Retained Set)
    需要说明一下:JAVA 对象大小=对象头+实例数据+对齐填充
  • 非数组类型的对象的 shallow heap
    shallow_size=对象头+各成员变量大小之和+对齐填充
    其中,各成员变量大小之和就是实例数据,如果存在继承的情况,需要包括父类成员变量,8字节的整数倍。
  • 数组类型的对象的 shallow size
    shallow size=对象头+类型变量大小 * 数组长度+对齐填充,如果是引用类型,则是四字节或者八字节(64 位系统),如果是 boolean 类型,则是一个字节。
    注意:这里 类型变量大小 * 数组长度 就是实例数据,强调是变量不是对象本身。
    在这里插入图片描述

3.1 案例分析

在这里插入图片描述
对象 A 持有对象 B 和 C 的引用。
对象 B 持有对象 D 和 E 的引用。
对象 C 持有对象 F 和 G 的引用。
Shallow Heap 大小 :
对象的 Shallow heap 是其自身在内存中的大小。

3.2 引用变动的影响

在下面的示例中,让对象 H 开始持有对 B 的引用。注意对象 B 已经被对象 A 引用了。
在这里插入图片描述

  • 在这种情况下,对象 A 的 Retained heap 大小将从之前的 70 减小到 40 个字节(因为A 被垃圾回收后,可以释放40字节的内存空间)。
  • 如果对象 A 被垃圾回收了,则将仅会影响 C、F 和 G 对象的引用。因此,仅对象 C、F 和 G 将被垃圾回收。另一方面,由于 H 持有对 B 的活动引 用,因此对象 B、D 和 E 将继续存在于内存中。因此,即使 A 被垃圾回收,B、D 和 E 也不会从内存中删除。因此,A 的 Retained heap 大小为:= A 的 shallow heap 大小 + C 的 shallow heap 大小 + F 的 shallow heap 大小 + G 的 shallow heap 大小 = 10 bytes + 10 bytes + 10 bytes + 10 bytes = 40 bytes.
  • 总结: 我们可以看到在进行内存分析时,浅堆和深堆是两个非常重要的概念,尤其是深堆,影响着回收这个对象能够带来的垃圾回收的效果,所以在内存分析中,我们往往会去找那些深堆比较的大的对象,尤其是那些浅堆比较小但深堆比较大的对象,这些对象极有可能是问题对象。

4. 使用 MAT 进行内存泄漏检测

  • 如果问题特别突出,则可以通过 Find Leaks 菜单快速找出问题。
  • 运行以下代码
package demo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;

public class ObjectsMAT {


    static class A {
        B b = new B();
    }

    static class B {
        C c = new C();
    }

    static class C {
        List<String> list = new ArrayList<>();
    }

    static class Demo1 {
        Demo2 Demo2;

        public void setValue(Demo2 value) {
            this.Demo2 = value;
        }
    }

    static class Demo2 {
        Demo1 Demo1;

        public void setValue(Demo1 value) {
            this.Demo1 = value;
        }
    }

    static class Holder {
        Demo1 demo1 = new Demo1();
        Demo2 demo2 = new Demo2();

        Holder() {
            demo1.setValue(demo2);
            demo2.setValue(demo1);
        }


        private boolean aBoolean = false;
        private char aChar = '\0';
        private short aShort = 1;
        private int anInt = 1;
        private long aLong = 1L;
        private float aFloat = 1.0F;
        private double aDouble = 1.0D;
        private Double aDouble_2 = 1.0D;
        private int[] ints = new int[2];
        private String string = "1234";
    }

    Runnable runnable = () -> {
        Map<String, A> map = new HashMap<>();

        IntStream.range(0, 100).forEach(i -> {
            byte[] bytes = new byte[1024 * 1024];
            String str = new String(bytes).replace('\0', (char) i);
            A a = new A();
            a.b.c.list.add(str);

            map.put(i + "", a);
        });

        Holder holder = new Holder();

        try {
            //sleep forever , retain the memory
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    };

    void startKingThread() throws Exception {
        new Thread(runnable, "fisher-thread").start();
    }

    public static void main(String[] args) throws Exception {
        ObjectsMAT objectsMAT = new ObjectsMAT();
        objectsMAT.startKingThread();
    }
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 这里一个名称叫做 fisher-thread 的线程,持有了超过 99% 的对象,数据被一个 HashMap 所持有。
  • 这个就是内存泄漏的点,因为我代码中对线程进行了标识,这个是有依据的,所以可以很快定位问题点,如果不取名字的话,这种问题的排查将非常困难。

在这里插入图片描述
在这里插入图片描述

  • 所以,如果是对于特别明显的内存泄漏,在这里能够帮助我们迅速定位,如果是比较隐蔽的问题,需要做更加复杂的分析。

5. 支配树视图

  • 支配树列出了堆中最大的对象,第二层级的节点表示当被第一层级的节点所引用到的对象,当第一层级对象被回收时,这些对象也将被回收。这个工具可以帮助我们定位对象间的引用情况,以及垃圾回收时的引用依赖关系。
  • 支配树视图对数据进行了归类,体现了对象之间的依赖关系。我们通常会根据“深堆”进行倒序排序,可以很容易的看到占用内存比较高的几个对象, 点击前面的箭头,即可一层层展开支配关系(依次找深堆明显比浅堆大的对象)。

在这里插入图片描述
在这里插入图片描述

  • 从上图层层分解,我们也知道,原来是 fisher-thread 的深堆和浅堆比例很多(深堆比浅堆多很多、一般经验都是找那些浅堆比较小,同时深堆比较大的对象)
    1、 一个浅堆非常小的 fisher-thread 持有了一个非常大的深堆;
    2、 这个关系来源于一个 HashMap;
    3、 这个 map 中有对象 A,同时 A 中引用了 B,B 中引用了 C;
    4、 最后找到 C 中里面有一个 ArrayList 引用了一个大数据的数组。
  • 经过分析,内存的泄漏点就在此。一个线程长期持有了 200 个这样的数组,有可能导致内存泄漏。

6. MAT 中内存对比

  • 我们对于堆的快照,其实是一个“瞬时态”,有时候仅仅分析这个瞬时状态,并不一定能确定问题,这就需要对两个或者多个快照进行对比,来确定一个增长趋势。
  • 我们导出两份 dump 日志,分别是上个例子中循环次数分别是 10 和 100 的两份日志
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 将2份dump文件导入
    在这里插入图片描述
    在这里插入图片描述
  • 对比

在这里插入图片描述

  • 分组

在这里插入图片描述
在这里插入图片描述

  • 经过内存日志的对比,分析出来这个类的对象的增长,也可以辅助到问题的定位(快速增加的地方有可能存在内存泄漏)。

7. 线程视图

  • 想要看具体的引用关系,可以通过线程视图。线程在运行中是可以作为 GC Roots 的。我们可以通过线程视图展示了线程内对象的引用关系,以及方法调用关系,相对比 jstack 获取的栈 dump,我们能够更加清晰地看到内存中具体的数据。
  • 我们找到了 fisher-thread,依次展开找到 holder 对象,可以看到内存的泄漏点。

在这里插入图片描述

  • 还有另外一段是陷入无限循环,这个是相互引用导致的(进行问题排查不用被这种情况给误导了,这样的情况一般不会有问题—可达性分析算法的解决了相互引用的问题)。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8. 查看GC Roots

  • 柱状图视图,可以看到除了对象的大小,还有类的实例个数。结合 MAT 提供的不同显示方式,往往能够直接定位问题。
  • 可以看到,创建的这些自定义对象,正好一百个(线程是100)。

在这里插入图片描述

  • 右键点击类,然后选择 incoming,这会列出所有的引用关系。

在这里插入图片描述
在这里插入图片描述

  • 被 JVM 持有的对象,如当前运行的线程对象,被 systemclass loader 加载的对象被称为 GC Roots,从一个对象到 GC Roots 的引用链被称为 Path to GC Roots, 通过分析 Path to GC Roots 可以找出 JAVA 的内存泄露问题,当程序不在访问该对象时仍存在到该对象的引用路径(这个对象可能内存泄漏)。
  • 再次选择某个引用关系,然后选择菜单“Path To GC Roots”,即可显示到 GC Roots 的全路径。通常在排查内存泄漏的时候,会选择排除虚弱软等引用。

在这里插入图片描述

  • 使用这种方式,即可在引用之间进行跳转,方便的找到所需要的信息(这里从对象反推到了线程 fisher-thread),也可以快速定位到有内存泄漏的问题代码。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值