JVM系列文章目录
前言
本文基于JDK1.8,Hotspot版本的JVM。
仅仅通过jdk自带的命令有时候是很难排查生产环境的内存泄漏问题,这个时候我们需要一些分析工具去分析dump日志,本文将介绍如何通过MAT去分析内存泄漏。
MAT
MAT全称是Memory Analyzer(Tool),是eclipse开发用来分析JVM堆栈内存的工具。(相比之下JDK自带的VisualVM内存分析还是差点意思)。在使用之前你可能需要根据你导出来的dump文件的堆大小调整MAT的内存大小,(MAT默认大小是1G,当然如果你的堆内存特别大的话,MAT用起来可能会有点卡顿),直接在配置文件MemoryAnalyzer.ini中修改即可。
这是MAT的进行内存分析的主页面。
概要
这里主要描述内存的占用,还有MAT对代码可能出现某些内存问题的猜想。
柱状视图
介绍柱状图之前得先介绍几个概念:
-
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。(我们可以简单的理解为它引用了谁)-
深堆和浅堆:
浅堆(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源码分析