热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

利用MAT工具分析内存泄漏

我们可以使用jmap–histo这种命令去分析哪些对象占据着我们的堆空间。但是那是比较容易分析的问题,如果是遇到内存情况比较复杂的情况,命令的方式是看不

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


MAT 简介

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


MAT 中的 Incoming/Outgoing References

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

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


MAT 中的浅堆与深堆

在这里插入图片描述

浅堆(shallow heap )代表了对象本身的内存占用,包括对象自身的内存占用,以及“为了引用”其他对象所占用的内存,一般根据对其填充,对象占据内存大小都是8字节的整数倍。
深堆 ( Retained heap )是一个统计结果,会循环计算引用的具体对象所占用的内存。但是深堆和“对象大小”有一点不同,深堆指的是一个对象被垃圾回收后,能够释放的内存大小,这些被释放的对象集合,叫做保留集(Retained Set)

数组类型的对象的 shallow size
shallow size=对象头+类型变量大小*数组长度+对齐填充,如果是引用类型,则是四字节或者八字节(64 位系统),
如果是 boolean 类型,则是一个字节
注意:这里 类型变量大小数组长度 就是实例数据,强调是变量不是对象本身。

在这里插入图片描述
再举一个栗子看看:


class A {private static byte[] b = new byte[10*1000];private B b1 = new B();private C c1 = new C();
}
class B {private D d1 = new D();private E e1 = new E();
}
class C {private F f1 = new F();private G g1 = new G();
}
class D {
}
class E {
}
class F {
}
class G {
}public class MATHeap {public static void main(String[] args) throws Exception {A a = new A();Thread.sleep(Integer.MAX_VALUE);//线程休眠}
}

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

对象 A 持有对象 B 和 C 的引用。
对象 B 持有对象 D 和 E 的引用。
对象 C 持有对象 F 和 G 的引用。
Shallow Heap 大小
请记住: 的 对象的 Shallow heap 是其自身在内存中的大小 。实际大小参照真实数据,上图画图的是虚拟数据举的例子方便计算。

引用变动的影响
在下面的示例中,让对象 H 开始持有对 B 的引用。注意对象 B 已经被对象 A 引用了。假设D的对象大小为8byte字节的10倍
在这里插入图片描述
在这种情况下,对象 A 的 的 Retained heap 的 大小将从之前的 70 到 减小到 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 = 40bytes.(数据是假设出来的,正常应该是8字节的倍数大小,这里便于计算理解)

总结:我们可以看到在进行内存分析时,浅堆和深堆是两个非常重要的概念,尤其是深堆,影响着回收这个对象能够带来的垃圾回收的效果,所以在内存分析中,我们往往会去找那些深堆比较的大的对象,尤其是那些浅堆比较小但深堆比较大的对象,这些对象极有可能是问题对象。


使用 MAT 进行内存泄漏检测

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 = &#39;\0&#39;;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(&#39;\0&#39;, (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 memoryThread.sleep(Integer.MAX_VALUE);} catch (InterruptedException e) {e.printStackTrace();}};void startKingThread() throws Exception {new Thread(runnable, "yang-thread").start();}public static void main(String[] args) throws Exception {ObjectsMAT objectsMAT = new ObjectsMAT();objectsMAT.startKingThread();}
}

在这里插入图片描述
这是上面代码的相关引用关系图。我们代码跑起来,MAT分析一波。

在这里插入图片描述

在这里插入图片描述
这里一个名称叫做 yang-thread 的线程,持有了超过 99% 的对象,数据被一个 HashMap 所持有。
这个就是内存泄漏的点,因为我代码中对线程进行了标识,所以像阿里等公司的编码规范中为什么一定要给线程取名字,这个是有依据的,如果不取名字的话,这种问题的排查将非常困难。


支配树视图

支配树列出了堆中最大的对象,第二层级的节点表示当被第一层级的节点所引用到的对象,当第一层级对象被回收时,这些对象也将被回收。这个工具可以帮助我们定位对象间的引用情况,以及垃圾回收时的引用依赖关系。
支配树视图对数据进行了归类,体现了对象之间的依赖关系。我们通常会根据“深堆”进行倒序排序,可以很容易的看到占用内存比较高的几个对象,点击前面的箭头,即可一层层展开支配关系(依次找深堆明显比浅堆大的对象)。
在这里插入图片描述
在这里插入图片描述
从上图层层分解,我们也知道,原来是 yang-thread 的深堆和浅堆比例很多(深堆比浅堆多很多、一般经验都是找那些浅堆比较小,同时深堆比较大的对象)
1、 一个浅堆非常小的 yang-thread 持有了一个非常大的深堆
2、 这个关系来源于一个 HashMap
3、 这个 map 中有对象 A,同时 A 中引用了 B,B 中引用了 C
4、 最后找到 C 中里面有一个 ArrayList 引用了一个大数据的数组。
经过分析,内存的泄漏点就在此。一个线程长期持有了 200 个这样的数组,有可能导致内存泄漏。


MAT 中内存对比

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

然后用jmap命令行工具导出两份不同代码运行的dump文件。
命令 : jmap -dump:live,format=b,file=heap100.bin pid
再通过MAT对比工具进行对比分析两份dump日志文件
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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


线程视图

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

加粗样式在这里插入图片描述
还有另外一段是陷入无限循环,这个是相互引用导致的(进行问题排查不用被这种情况给误导了,这样的情况一般不会有问题—可达性分析算法的解决了相互引用的问题)
在这里插入图片描述
柱状图视图
柱状图视图,可以看到除了对象的大小,还有类的实例个数。结合 MAT 提供的不同显示方式,往往能够直接定位问题。也可以通过
正则过滤一些信息,我们在这里输入 MAT,过滤猜测的、可能出现问题的类,可以看到,创建的这些自定义对象,不多不少正好一百个。
在这里插入图片描述


Path To GC Roots

被 JVM 持有的对象,如当前运行的线程对象,被 systemclass loader 加载的对象被称为 GC Roots,从一个对象到 GC Roots 的引用链被称为 Path to GC Roots,通过分析 Path to GC Roots 可以找出 JAVA 的内存泄露问题,当程序不在访问该对象时仍存在到该对象的引用路径(这个对象可能内存泄漏)。再次选择某个引用关系,然后选择菜单“Path To GC Roots”,即可显示到 GC Roots 的全路径。通常在排查内存泄漏的时候,会选择排除虚弱软等引用。
在这里插入图片描述
使用这种方式,即可在引用之间进行跳转,方便的找到所需要的信息(这里从对象反推到了线程 yang-thread),也可以快速定位到有内存泄漏的问题代码。
在这里插入图片描述


高级功能 —OQL

MAT 支持一种类似于 SQL 的查询语言 OQL(Object Query Language),这个查询语言 VisualVM 工具也支持。

在这里插入图片描述
查询 A 对象:
select * from ex14.ObjectsMAT$A
查询包含 java 字样的所有字符串:
select * from java.lang.String s where toString(s) like “.java.
OQL 有比较多的语法和用法,若想深入了解,可以了解这个网址 http://tech.novosoft-us.com/products/oql_book.htm
案例分析

在这里插入图片描述

ThreadLocal 是基于 ThreadLocalMap 实现的,这个 Map 的 Entry 继承了 WeakReference,而 Entry 对象中的 key 使用了 WeakReference 封装,也就是说 Entry中的 key 是一个弱引用类型,而弱引用类型只能存活在下次 GC 之前。
当发生一次垃圾回收,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话(肯定不会结束),这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块 value永远不会被访问到了,所以存在着内存泄露。如下图:
在这里插入图片描述
只有当前 thread 结束以后,current thread 就不会存在栈中,强引用断开,Current Thread、Map value 将全部被 GC 回收(但是这
种情况很难)。最好的做法是不在需要使用 ThreadLocal 变量后,都调用它的 remove()方法,清除数据。
在这里插入图片描述


推荐阅读
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • PHP中的单例模式与静态变量的区别及使用方法
    本文介绍了PHP中的单例模式与静态变量的区别及使用方法。在PHP中,静态变量的存活周期仅仅是每次PHP的会话周期,与Java、C++不同。静态变量在PHP中的作用域仅限于当前文件内,在函数或类中可以传递变量。本文还通过示例代码解释了静态变量在函数和类中的使用方法,并说明了静态变量的生命周期与结构体的生命周期相关联。同时,本文还介绍了静态变量在类中的使用方法,并通过示例代码展示了如何在类中使用静态变量。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • Java SE从入门到放弃(三)的逻辑运算符详解
    本文详细介绍了Java SE中的逻辑运算符,包括逻辑运算符的操作和运算结果,以及与运算符的不同之处。通过代码演示,展示了逻辑运算符的使用方法和注意事项。文章以Java SE从入门到放弃(三)为背景,对逻辑运算符进行了深入的解析。 ... [详细]
  • 图像因存在错误而无法显示 ... [详细]
author-avatar
天天火火红红
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有