内存分析工具
- 上一节中是通过jmap -histo来查看堆内存空间及对象实例情况,但是前提是程序必须是存活的,如果程序关闭了就不能查看之前的内存情况了,就需要通过以下的工具来查看
内存分析工具
- VisualVM
- 也是可以查看dump文件的
- 文件->装入->选择指定的dump文件,就可以打开dump文件了
- 上一节的cpu%项目中,虚拟机开启了-XX:+HeapDumpOnOutOfMemoryError,就是内存溢出后会自动保存dump文件,文件名形如java_pid15032.hprof
- 控制面板的显示内容
- 概要
- 类
- 实例数
- OQL控制台
- 排查问题时一般不会使用,因为显示的内容与命令行工具相差不大
- MAT
- 是eclipse公司做的,基于eclipse平台开发的java程序
- 一般几个g的内存是适合查看的,内存太大了这个工具会很卡,同时要修改ini的配置文件,把其中的堆内存最大大小改大
MAT控制面板
-
overview-概要
-
actctions
-
histogram-柱状图,实体大小
点击某个类,list objects,
-
-
MAT中的重要概念
-
incoming references-对象的引入
-
outgoing references-对象的引出
-
概念解释实例
-
package ex14; /** * 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);//线程休眠 } }
c这个类
- 对象的引入
- c1
- c2
- myc
- 对象的引出
- d1
- e1
- c类的class
为什么要有对象的引入和对象的引出这两个概念
- 为了分析内存泄漏问题,内存泄漏是与垃圾回收相关的,而在垃圾回收中怎么判断垃圾?是通过可达性分析来判定的,通过gcroots来查它的引出,所以可以通过对象的引入和对象的引出来分析对象的引用关系
MAT中的浅堆与深堆
-
Shallow Heap(浅堆)
- 对象自身占用的内存
- 对象占用内存包括对象头、实例数据、对齐填充,这三部分合起来要是8字节的整数倍
- 一般对象和数组对象是不同的,数组对象内存包括对象头+类型变量大小*数组的长度+对齐填充
-
Retained Heap(深堆)
- 是一个统计结果,如果自身对象被gc了,能够释放的内存大小
- 深堆释放的内存肯定是包含浅堆的内存的
-
-
SH表示浅堆,RH表示深堆,后面的数字表示大小,连线表示引用关系
-
假设把A干掉,其余的全部都能释放,因为A干掉,根据可达性分析算法,上面的都被干掉了,后面的都能被回收
-
package ex14.heap; /** * Shallow Heap Vs Retained Heap */ 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);//线程休眠 } }
-
新增引用的影响
-
-
新增一个H引用,H引用的浅堆和深堆相等,就是它自己的对象内存
-
除此之外,A的深堆大小发生了变化,由70变为40
使用MAT分析内存泄漏
案例代码
-
package ex14; 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, "king-thread").start(); } public static void main(String[] args) throws Exception { ObjectsMAT objectsMAT = new ObjectsMAT(); objectsMAT.startKingThread(); } }
-
引用关系
-
-
map是一个hashmap,map里面有100个A,但是A又引用了B,B又引用了C,C中存在一个list,这个list又引用了1MB的数组
-
holder类中有两个对象demo1和demo2,它们相互引用
-
内存泄漏检查
- 启动上述程序后,用mat分析内存问题
- problem suspect 1
- king-thread持有了99.68%的资源,同时解释到是一个HashMap&Node的实例
- 阿里有一个代码规范,线程必须取名字,也就是此处的king-thread
- problem suspect 1
使用MAT分析内存泄漏
支配树视图
-
open dominator tree for entire heap
-
能够展示堆当中最大的对象,是以深堆大小倒序排列的
- king-thread Thread
- HashMap
- HashMap$Node—一共有100个
- A
- …
- A
- HashMap$Node—一共有100个
- HashMap
- king-thread Thread
-
怎么算到底了呢?深堆和浅堆相等时
总结
怎么寻找内存泄漏的位置
- 不论是通过mat中自带的猜想,还是通过支配树层层分析,内存泄漏的点就是那些深堆比较大,而浅堆比较小的对象
MAT中内存对比
-
jmap -dump:live,format=b,file=heap100.bin 进程号
-
jmap -dump:live,format=b,file=heap10.bin 进程号
-
上述案例中考虑hashmap有10个节点和有100个节点的内存对比
- 1.查看对象数量,打开柱状图,此时控制面板最后一个图标open another heap dump
- 通过package分组,可以看到相同类的相对数量变化,在10中打开,显示A、B、C都是负数,说明相比100个节点时要少多少个此类的实例对象
- 1.查看对象数量,打开柱状图,此时控制面板最后一个图标open another heap dump
线程视图
- 每个dump视图的控制面板,if available,show each thread’s name, stack, frame locals, retained heap, etc
- 通过name找到king-thread,它的浅堆只有120,但是深堆有20974824
- lambda$new
- HashMap
- HashMap$Node
- value
- …
- value
- HashMap$Node
- HashMap
- lambda$new
- 一直向下找到c类中的list
- 通过name找到king-thread,它的浅堆只有120,但是深堆有20974824
柱状图视图
- overview中的histogram,可以展示所有的类,同时再次点击具体的类,可以找出它的对象的引入和它的对象的引出
Path To GC Roots
- 在柱状图中,点击某个对象,list object,然后选择with incoming references,这里就是具体的对象实例了,是有实际物理地址的,然后点击单个的实例,选择path to gc roots,然后可以选择引用的强弱类型
- 例如选择上面例子中的c类,然后path to gc roots,最后还是回到了king-thread
高级功能—OQL
- object query language,对象查询语言,官网(http://tech.novosoft-us.com/products/oql_book.htm)
- select * from ex14.ObjectsMAT$A,然后点!,就可以找到所有的实例对象
使用MAT分析内存泄漏
实战演练
-
package cn.enjoyedu.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; /** * 类说明: */ @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"; } @RequestMapping("/mat1") public String mat1() { ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>(); localVariable.set(new Byte[4096 * 1024]);// 为线程添加变量 localVariable.remove(); return "success"; } }
-
java -jar -XX:+HeapDumpOnOutOfMemoryError jvm-1.0-SNAPSHOT.jar
-
ab -c 10 -n 1000 http://127.0.0.1:8080/jvm/mat
结果
-
probleam suspect 1
-
the thread org.apache.tomcat.util.thread.TaskThread @ 0xe3b69de8 http-nio-8080-exec-6 keeps local variables with total size 67,115,432(15.06%)bytes.
The memory is accumulated in one instance of “java.lang.ThreadLocal T h r e a d L o c a l M a p ThreadLocalMap ThreadLocalMapEntry[]” loaded by “”.
-
是tomcat里面一个taskThread,持有的局部变量,占有了15.06%的内存
-
-
下面还有问题猜想,但是都是tomcat的taskthread
具体分析
-
打开柱状图
-
找出深堆大小排名前几的类,然后去找响应的对应的引入,再找path to gc roots,找到是http-nio-8080-exec-3的线程中的threadlocals
-
这个时候就要去代码中找哪些地方用了threadlocal
-
为什么要用threadlocal?
在并发编程中,为了线程安全,在中间一段程序中get出set进去的东西
-
ThreadLocal内存泄漏
-
基于ThreadLocalMap实现
-
entry,这个类是继承了WeakReference的,是弱引用,熬不过gc,只要发生垃圾回收,就会被回收
- key,其中的key是一个弱引用
- value
-
-
- 由于ThreadLocal的key是弱引用,所以ThreadLocal指向entry中的key是一条虚线
-
因此如果key被回收了,就找不到value了,就发生了内存泄漏
怎么解决泄漏问题呢?
- 每次ThreadLocal.get()方法之后,都要继续执行ThreadLocal.remove()方法