JVM调优之玩转MAT分析内存泄漏

JVM系列文章目录

初识JVM

深入理解JVM内存区域

玩转JVM对象和引用

JVM分代回收机制和垃圾回收算法

细谈JVM垃圾回收与部分底层实现

Class文件结构及深入字节码指令

玩转类加载和类加载器

方法调用的底层实现

Java语法糖及底层实现

GC调优基础知识工具篇之JDK自带工具

GC调优基础知识工具篇之Arthas与动态追踪技术

JVM调优之内存优化与GC优化

JVM调优之预估调优与问题排查

JVM调优之玩转MAT分析内存泄漏

直接内存与JVM源码分析

JVM及时编译器



前言

本文基于JDK1.8,Hotspot版本的JVM。
仅仅通过jdk自带的命令有时候是很难排查生产环境的内存泄漏问题,这个时候我们需要一些分析工具去分析dump日志,本文将介绍如何通过MAT去分析内存泄漏。



MAT

MAT官网

MAT全称是Memory Analyzer(Tool),是eclipse开发用来分析JVM堆栈内存的工具。(相比之下JDK自带的VisualVM内存分析还是差点意思)。在使用之前你可能需要根据你导出来的dump文件的堆大小调整MAT的内存大小,(MAT默认大小是1G,当然如果你的堆内存特别大的话,MAT用起来可能会有点卡顿),直接在配置文件MemoryAnalyzer.ini中修改即可。
在这里插入图片描述
这是MAT的进行内存分析的主页面。
在这里插入图片描述

概要

这里主要描述内存的占用,还有MAT对代码可能出现某些内存问题的猜想。
在这里插入图片描述

柱状视图

介绍柱状图之前得先介绍几个概念:

  1. Incoming/Outgoing References
    Incoming References:对象的引入。
    Outgoing References:对象的引出。
    这个描述看起来可能有点抽象,我这里举个例子就很好理解了。
    现在有这样的代码。

    /**
     * @author Abfeathers
     * @date 2021/3/24 
     * @Description 对象的引入、引出(Incoming / 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对象都引用;
    对象D和对象E的引用都被C对象持有。
    在这里插入图片描述
    现在我们先把程序跑起来,用MAT来查看。
    在这里插入图片描述
    筛选我们自己的写到package在这里插入图片描述
    找到我们写到类,右键->List Objects ->Incoming Reference
    在这里插入图片描述
    在这里插入图片描述
    这里我们就可以看出来以C为基准,它的对象的引入是A对象,B对象,C对象的Class(我们可以简单的理解成谁持有本对象的引用)。

    我们在看Outgoing Reference
    在这里插入图片描述
    同样以C对象为基准,它的对象的引出为D对象、E对象和C对象的Class。(我们可以简单的理解为它引用了谁)

    1. 深堆和浅堆
      浅堆(shallow heap):表示对象本身的内存占用, 包括对象自身的内存占用,以及“为了引用”其他对象所占用的内存。
      深堆(Retained heap):一个统计结果,会循环计算引用的具体对象所占用的内存。但是深堆和“对象大小”有一点不同,深堆指的是一个对象被垃圾回收后,能够释放的内存大小,这些被释放的对象集合,叫做保留集(Retained Set)。
      (对象大小相关的信息,请各位移步——玩转JVM对象和引用)

      这里我也举一个例子帮助理解:
      有这样一个引用关系图:
      对象 A 持有对象 B 和 C 的引用。
      对象 B 持有对象 D 和 E 的引用。
      对象 C 持有对象 F 和 G 的引用。
      那它们的深堆浅堆数据如下图所示:在这里插入图片描述
      这里有个比较有意思的,如果我有加入一个对象H,它持有B对象的引用,那它的深堆、浅堆大小是这样的:
      在这里插入图片描述
      为什么H的深堆也是10呢?因为就算H被回收了,他引用的B对象也不会被回收。


MAT内存泄漏分析

先运行这样一段代码。

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

/**
 * @author Abfeathers
 * @date 2021/3/24
 * @Description MAT内存泄漏分析
 *
 */

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 startAbfeathersThread() throws Exception {
        new Thread(runnable, "abfeathers-thread").start();
    }

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

通过MAT进行分析,查看default_report。在这里插入图片描述
这里一个名称叫做 abfeathers-thread 的线程,持有了超过 99% 的对象,数据被一个 HashMap 所持有。 这个就是内存泄漏的点。根据这个我们就可以直接定位代码了。

我们直接找到问题代码
在这里插入图片描述
在这里插入图片描述
这里内存泄漏的问题很明显,通过MAT也很好排查,当然生产环境更复杂,需要更多的步骤来排查。


支配树视图

支配树视图列举出对中最大的对象,第二层级的节点表示当被第一层级的节点所引用到的对象,当第一层级对象被回收时,这些对象也将被回收。这个工具 可以帮助我们定位对象间的引用情况,以及垃圾回收时的引用依赖关系。
还是上面的代码,我们这里根据深堆来进行倒序排序。
在这里插入图片描述
我们进行点击查看(点到深堆和浅堆一样大的时候就算是就到底了)
在这里插入图片描述

  • 一个浅堆非常小的 abfeathers-thread 持有了一个非常大的深堆
  • 这个关系来源于一个 HashMap
  • 这个 map 中有对象 A,同时 A 中引用了 B,B 中引用了 C
  • 最后找到 C 中里面有一个 ArrayList 引用了一个大数据的数组。
    这样就分析出了内存泄漏的,一般做这种分析的时候很小的一个浅堆有一个很大的深堆的这个对象,我们一定要注意,看看是不是会导致内存泄漏。

MAT内存对比

堆快照毕竟是一个瞬时状态,有可能一个dump文件难以分析出问题,这个时候我们可以再导出一份快照,进行内存对比(比如说一个电商平台,在正常时候和秒杀高峰时刻的快照对比)。
这里我们还是上面的代码,我们分别将循环次数设置成10、100来进行对照。(内存比较一定是相同程序的不同dump对比,不同程序是不行的哈。)
在这里插入图片描述
将两份dump文件都导入MAT,然后选择一个与另外一个进行内存对照。
在这里插入图片描述
我这里是以循环10次为基准与循环100次进行比较,发现A、B、C三个对象都在循环次数增加的时候都增长了不少,那这一块就有可能会出现内存泄漏。

在这里插入图片描述


线程视图

如果你想看具体的引用关系的话,就可以看线程视图。
线程在运行中是可以作为 GC Roots 的。我们可以通过线程视图展示了线程内对象的引用关系,以及方法调 用关系,相对比 jstack 获取的栈 dump, 我们能够更加清晰地看到内存中具体的数据。
这里还是上面的代码,我们找“abfeathers-thread"线程的引用。我们一样最后可以排查到C持有大量的对象。
在这里插入图片描述
不过线程视图有一个小问题,如果出现循环引用(循环依赖也一样)的话,你就可以无限点下去,没有尽头,这个时候你就要住手了,除非你想在排查问题的时候摸一天的鱼。
像我上面代码中的Holder
在这里插入图片描述
在这里插入图片描述

Path To GC Roots

其实这个从名字上就可以理解,一条到GC Roots的引用链,也就是我们说的根可达分析中的引用链。通过分析Path To GC Roots一样可以排查内存泄漏问题。

我们在柱状图中选择要查询的对象,右键->Merge shortest Path To GC Roots->(我这里选全部引用,你也可以根据需求筛选)all reference。

在这里插入图片描述


OQL

OQL(Object Query Language)有点类似于SQL语言,它也是一种查询语言。
比如像这样查询所有的A对象。
在这里插入图片描述



案例展示

我这里模拟一个生产环境的OOM,我写了一个简单的spring boot工程,核心代码如下


import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


/**
 * @author Abfeathers
 * @date 2021/3/24 
 * @Description OOM问题排查
 * 
 */
@RestController
@RequestMapping("/jvm")
public class MatController {
    @RequestMapping("/mat")
    public String mat(){
        ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();
        localVariable.set(new Byte[4096*1024]);// 为线程添加变量
        return "success";
    }
}

我这里限制一下堆大小,方便快速出结果,然后在增加OOM是导出dump文件输出。

启动程序
在这里插入图片描述

通过ab进行压测

ab -c 10 -n 10000 http://localhost:8080/jvm/mat

发生了OOM自动导出dump日志
在这里插入图片描述
现在通过MAT分析日志

这里直接给我们分析出了8个问题
在这里插入图片描述

在这里插入图片描述
这里其实已经知道问题了(ThreadLocal的内存泄漏,导致内存溢出),我们再看看引入
在这里插入图片描述
定位出引用层级关系,ThreadLocal里有大量Byte[]的引用,然后我们就可以去代码里面找了。
在这里插入图片描述
在这里插入图片描述
为什么这个ThreadLocal会造成内存泄漏呢?
我们来看看源码:
发现ThreadLocal是通过ThreadLocalMap来实现的。
在这里插入图片描述
我们再看看ThreadLocalMap的实现,发现它的Entry居然继承弱引用,Entry中的key是用弱引用实现的。
在这里插入图片描述
那上面的代码问题就大条了。弱引用只要发生GC就会被回收,也就是说这个Map中的key会在GC的时候就被回;而value是强引用,那value就永远也访问不到了,如果当前线程一直不结束,那内存泄漏一直堆积,最后就会OOM(至于为什么线程没有被回收,这跟JVM复用线程有关,常见的比如我们的Tomcat服务器里存在一个线程池, 对于每一个http请求, 都会从线程池中取出一个空闲线程, 但这个线程池的默认线程只有75个, 超过后一样会出现线程复用。)

当然ThreadLocal的作者针对内存溢出也做了一些优化,比如说在get的时候遍历map,把那些key为null的移除掉,但是如果你不调用这些方法的话,一样会内存溢出。所以使用ThreadLocal一定要remove或者set(null)
在这里插入图片描述



上一篇:JVM调优之预估调优与问题排查

下一篇:直接内存与JVM源码分析

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值