提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!!
使用上一章命令行工具或组合能帮您获取目标Java应用性能相关的基础信息,但它们存在
下列局限:
为此,JDK提供了一些内存泄漏的分析工具,如jConsole, jvisualvm等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求。所以这里我们介绍的工具相对多一些、丰富一些
图形化综合诊断工具
1. JDK自带的工具
位置:jdkibinljconsole.exe
Visual VM:Visual VM是一个工具,它提供了一个可视界面,用于查看Java虚拟机上运行的基于Java技术的应用程序的详细信息。
JMC:Java Mission Control,内置Java Flight Recorder。能够以极低的性能开销收集Java虚拟机的性能数据。
2. 第三方工具
Eclipse的插件形式
与visualvM类似
jdk/bin目录下,启动jconsole.exe命令即可,不需要使用jps命令来查询。
mac下执行jconsole即可
Local:使用jConsole连接一个正在本地系统运行的JVM,并且执行程序和运行jConsole的需要时同一个用户。jConsole使用文件系统的授权是通过RMI连接器连接到平台的MBean服务器上。这种从本地连接的监控能力只有Sun的JDK具有。
Remote:使用下面的URL通过RMI连接器连接到一个JMX代理service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi, hostName填入主机名称, portNum为JMX代理启动时指定的端口。jConsole为了建立连接,需要在环境变量中设置mx.remote.credentials来指定用户名和密码,从而进行授权。
参考资料:https://blog.csdn.net/qq_41633199/article/details/120777327
测试代码
package jvm.classloader.jvm;import java.util.ArrayList;/*** -Xms60m -Xmx60m -XX:SurvivorRatio=8*/
public class GCTest {public static void main(String[] args) {ArrayList list = new ArrayList<>();for (int i = 0; i < 1000; i++) {byte[] arr = new byte[1024 * 100];//100kblist.add(arr);try {Thread.sleep(120);} catch (Exception e) {e.printStackTrace();}}}
}
死锁测试代码
package jvm.classloader.jvm;/*** 死锁测试*/
public class ThreadDeadLock {public static void main(String[] args) {StringBuilder s1 = new StringBuilder();StringBuilder s2 = new StringBuilder();new Thread(() -> {synchronized (s1) {s1.append("a");s2.append("1");try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (s2) {s1.append("b");s2.append("2");System.out.println(s1);System.out.println(s2);}}}).start();new Thread(() -> {synchronized (s2) {s1.append("c");s2.append("3");try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (s1) {s1.append("d");s2.append("4");System.out.println(s1);System.out.println(s2);}}}).start();}
}
中文界面
英文界面
采样器
Visual GC
监视
生成Dump文件
保存Dump文件
读取Dump文件
堆Dump文件比较
2. 查看JVM参数和系统属性
概述
查看运行中的虚拟机进程
生成/读取线程快照
线程
生成线程Dump文件
保存线程Dump文件与读取线程Dump文件和堆Dump类似。
MAT(Memory Analyzer Tool)工具是一款功能强大的Java堆内存分析器。可以用于查询内存泄露以及查看内存消耗情况。
MAT是基于Eclipse开发的,不仅可以单独使用,还可以作为插件的形式嵌入到Eclipse中使用。是一款免费的性能分析工具,使用起来非常方便。
下载地址:http://www.eclipse.org/mat/downloads.php
只要确保机器上装有JDK并配置好相关的环境变量,MAT就可以正常启动,还可以在Eclipse中以插件的方式安装。
MAT可以分析heap dump文件,在进行内存分析的时候,只要获取了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观的看到当前的内存信息。
一般来说,这些内容包含:
-XX:+HeapDumpOnOutOfMemoryError 或 -XX:+HeapDumpBeforeFullGC
-XX:HeapDumpPath=< 文件名.hprof >
对比:考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用jmap+MAT根据是最常见的组合。
import java.util.ArrayList;
import java.util.Random;/*** -Xms600m -Xmx600m -XX:SurvivorRatio=8*/
public class OOMTest {public static void main(String[] args) {ArrayList list = new ArrayList<>();while(true){try {Thread.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}list.add(new Picture(new Random().nextInt(100 * 50)));}}
}class Picture{private byte[] pixels;public Picture(int length) {this.pixels = new byte[length];}
}
启动上面的代码,
导出hprof文件
用eclipse的MAT插件打开,会出现下面的界面,直接点cancel就可以了,点finish也行
根据进程号选择你要查看的进程
点击finish即可
这个是mat会自动的检测dump文件,用于查看哪些是泄露的疑点,报告中会说明哪些对象还在存活,以及为什么没有被垃圾回收器收集。
内存泄露的问题非常关注这个,因为内存泄露就是对象不用了,但是为什么不能被垃圾回收器回收。
会分析对象的集合,找到相关的可疑的内存空间,比如说重复的字符串,空的集合容器,弱引用等等,这些都是我们认为可疑的内存。
之前存在的reports会和dump文件在同一个目录下的zip文件中
下面会依次介绍下面图片的意思
Size是当前dump文件的大小,
Classes是dump文件中加载的类的个数是708个,
Objects是dump文件中创建了46000个对象,46K就是46*1000,也就是46000
Class Loader是dump文件中使用的类加载器有四个
Unreadchable Objects Histogram 是不可达的对象的直方图
这个是保留的堆快照中内存中最大的对象是多少,你把鼠标点在哪里,然后左边Inspector就显示啥
第一行: 是当前线程的编号
第二行: 不知道
第三行: class java.lang.Thread 就是当前的线程,0xf3801c08是当前线程的编号
第四行: java.lang.Object是父类的信息
第五行:
第六行: shallow 是浅堆
第七行: retained 是深堆
第八行:
堆文件的Overview:
第一行: dump文件的大小
第二行:对象的个数
第三行:类的个数
第四行:类加载器的个数
第五行:Gc roots 对象个数
第六行: 文件的类型
一般分析饼状图的时候先从最大的查看,当然也有可能这个最大的就是我们希望保存在堆里面的数据,那么我们就接着看饼状图第二大的数据,或者第三大的数据,这样就根据自己的项目情况看看哪个是有内存泄露的疑点的,分析出来之后,然后解决一下就完了。
具体内容:
内存中加载的类,Objects是对象的个数,Shallow Heap是占用的浅堆大小,Retained Heap 是占用的深堆的大小。
如果显示的类过多的话,也可以用根据包名来分组,分组完了查看就更方便看了。
假如说我想看 picture 的gc root ,就可以用 Merge Shortest Paths to GC Roots,最右面可以排除 引用选项,点击查看,就能看到下面 ArrayList对象 0xf663aab8的对象引用了那么多Picture对象,然后你就需要进行分析这些引用是不是合理的,如果不合理的话就尝试着优化它。
在b.hprof文件里面点击上面的操作,选择a.hprof文件,然后点ok。
然后就基于b.hprof文件去看和a.hprof的区别,可以看到b.hprof比a.hprof多了很多对象信息 +号代表b.hprof比a.hprof多了。反之 减号 代表少。
比如说第一行byte[] +27884 的意思就是 b.hprof的byte[] 对象比 a.hprof的byte[] 的Objects多了27884个。
通过这个功能你就可以观察指定时间内哪个对象增长了多少,比如说下午三点导出a.hprof文件,然后下午四点导出b.hprof文件,然后通过对比来看看下午四点导出的hprof文件比下午三点导出的hprof文件里面哪个Class Name的指标增加的比较快,这个有可能就是我们要担心的可能会出现oom的类了。
展示线程的概述情况,我们可以查看当前进程中所有的Java的线程的情况,还可以查看当前的线程中栈祯里面保存的局部变量的信息。
点击图标:
点击之后显示当前dump文件有六个线程
main方法下面的就是main方法在执行的时候的局部变量,局部变量有可能是对象也有可能是基本数据类型,我们就可以怀疑这些对象是否存在内存泄露的问题。
上图String 就是下面这个main方法的形参,ArrayList就是list对象
再点看ArrayList 就能看到ArrayList里面有个elementData ,这个elementData 是ArrayList源码里面存数据的数组容器,然后你就会发现elementData类型是Object[]类型,并且elementData里面有总共25个Picture对象。
我们读线程局部变量数据的目的就是为了分析哪些手可能会有泄露的问题。
第一个是出引用(就是这个对象都引用过谁) 第二个是入引用(哪些对象引用过来的)
下面展示的是ArrayList引用了哪些对象
还可以接着看哪些对象引用了这个对象了,来确定为什么他还没被回收,我们就看看这个指针有没有必要存在,如果没有必要存在的话,就改成弱引用等等。
然后查看,只有ArrayList里面的elementData数组引用了这个Picture 0xf399eb18 指针了,这样就没事儿了,当然如果我们发现这个Picture对象除了被ArrayList引用了,还被别的对象引用了,那么我们就要担心了,可能会有内存泄露的问题了。
因为案例代码的ArrayList的生命周期挺短的,main方法执行完了,ArrayList就可以被回收了,只要ArrayList挂掉,那么ArrayList引用的这个Picture也会被挂掉。
当然下面的代码有while (true )死循环,实际情况下代码基本不会有while(true)这种缺心眼的写法,所以main方法结束了ArrayList就可以被回收了。
但是,如果你Picture对象还是被一个生命周期比较长的对象A引用了,ArrayList销毁了,但是这个对象A还在引用Picture对象,导致Picture无法被销毁回收,就会存在泄露。
如果必须要被对象A引用,那么你就可以考虑把这个生命周期比较长的对象A引用Picture对象的引用改成弱引用就可以了。
概念
MAT提供了一个称为支配树的对象图。支配树体现了对象实例之间的支配关系(支配关系就是我支配你,你是我管辖的,你做什么都要先经过我,那么就是我支配你。)在对象的引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。
如果对象A是离对象B最近的一个支配对象,就认为对象A为对象B的直接支配者。
比如说这个H,我要访问H的话,必须要通过C,那么C就是支配者 ,F和G不是,因为你访问H的时候,你可以不经过F,直接去经过G,你访问H的时候,你也可以不经过G,直接经过F。
支配树是基于对象间的引用图所建立的,有下面的基本性质:
对象A的子树(所有被对象A支配(只有通过对象A才能访问的到)的对象集合)表示对象A的保留集(retained set),即深堆。
如果对象A支配对象B,那么对象A的直接支配者也支配对象B,因为对象A支配对象B,那么说明要想经过对象B,就必须要走对象A的链路,那么如果有对象C是对象A的直接支配者,走链路只走对象A,那么这个对象C也是只走对象B。
支配树的边与对象引用图的边不直接对应的。
上图所示:左边图标识对象引用图,右边图标识左图所对应的支配树。对象A和B由根对象直接支配,由于在到对象C的路径中,可以经过A,也可以经过B。因此对象C的直接支配者也是根对象。对象F与对象D互相引用,因为到对象F的所有路径必然经过对象D,因此,对象D是对象F的直接支配者。而到对象D的所有路径中,必然经过对象C,即使是从对象F到对象D的引用,从根节点触发,也要经过对象C的,所以对象D的直接支配者为对象C。
同理,对象E支配对象G,到达对象Hd可以通过对象D,也可以通过对象E,因此对象D和E都不能支配对象H,而经过对象C既可以到达D也可以到达E,因此对象C为对象H的直接支配者。
注意:
跟随我一起来理解如何从“对象引用图 —> 支配树”,首先需要理解支配者(如果要到达对象B,毕竟经过对象A,那么对象A就是对象B的支配者,可以想到支配者大于等于1),然后需要理解直接支配者(在支配者中距离对象B最近的对象A就是对象B的直接支配者,你要明白直接支配者不一定就是对象B的上一级,然后直接支配者只有一个),然后还需要理解支配树是怎么画的,其实支配树中的对象与对象之间的关系就是直接支配关系,也就是上一级是下一级的直接支配者,只要按照这样的方式来作图,肯定能从“对象引用图—》支配树”.
在Eclipse MAT工具中如何查看支配树:
上图中Object数组里面只有8个对象,原因是因为8个对象是这个Student对象自己独有的,如果这个Student对象被垃圾回收了,那么支配树下面的Object数组里面的8个对象也会被垃圾回收掉。
启动的时候执行,虚拟机参数添加: -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=d:\student.hprof
/*** 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。* 它由三个部分组成:Student、WebPage和StudentTrace三个类* 启动的时候执行,虚拟机参数添加: -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=d:\student.hprof*/
public class StudentTrace {static List webpages = new ArrayList();public static void createWebPages() {for (int i = 0; i < 100; i++) {WebPage wp = new WebPage();wp.setUrl("http://www." + Integer.toString(i) + ".com");wp.setContent(Integer.toString(i));webpages.add(wp);}}public static void main(String[] args) {createWebPages();//创建了100个网页//创建3个学生对象Student st3 = new Student(3, "Tom");Student st5 = new Student(5, "Jerry");Student st7 = new Student(7, "Lily");for (int i = 0; i < webpages.size(); i++) {if (i % st3.getId() == 0)st3.visit(webpages.get(i));if (i % st5.getId() == 0)st5.visit(webpages.get(i));if (i % st7.getId() == 0)st7.visit(webpages.get(i));}//清理掉webpages.clear();//触发gc垃圾回收System.gc();}
}class Student {private int id;private String name;private List history = new ArrayList<>();public Student(int id, String name) {super();this.id = id;this.name = name;}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public List getHistory() {return history;}public void setHistory(List history) {this.history = history;}public void visit(WebPage wp) {if (wp != null) {history.add(wp);}}
}class WebPage {private String url;private String content;public String getUrl() {return url;}public void setUrl(String url) {this.url = url;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}
}
选择第一个,生成怀疑泄漏的报告
打开hprof文件后点击下面图片的图标来查看线程概述
能看到main方法方法里面3个Student对象,三个student的shallow heap(浅堆)大小都是24,但是三个对象的retained heap (深堆)都是不一样的。
第一个student对象如果被回收的话,就会回收3784个字节
第二个student对象如果被回收的话,就会回收1872个字节
第三个student对象如果被回收的话,就会回收1384个字节
为什么从第一个student对象到第三个student对象深堆会越来越少呢???,,因为示例代码往三个student对象里面设置值,因为不同的分支条件不一样,进入第一个分支的多进入第二个分支的稍微少点,进入第三个分支的更少,所以第一个student对象深堆就大, 第二个student对象就稍微少点,第三个student对象就更少了。
以最下面的student对象为例子,history深堆大小是1288,打开history这个ArrayList之后,发现ArrayList内部的elementData数组有一堆元素,再打开elementData数组之后,就能看到elementData这个数组里面有一堆WebPage对象了。
elementData数组的浅堆是80个字节,而elementData数组中的所有WebPage对象的深堆之和是1208个字节,所以加在一起就是elementData数组的深堆之和,也就是1288个字节。
如果elementData数组要是被回收的话,只能回收掉1288个字节,原因是有一些WebPage还被别的student所引用,15个WebPage对象,每个对应152字节, 15*152=2280字节,即2280位elementData的实际大小,但是为什么elementData的深堆是1288呢?
因为能被7整除且能被3整除,以及能被5整除的数值有: 0 21 35 42 63 84 70 ,总共七个数,这七个数儿对应的WebPage不只是被这个student独享,还被其它的student对象引用了,这些是不能计算深堆里面的,,7*152就是1064个字节 。
2280-1064 =1216 , 是1216这个数字,但是1216和显示的1288还是不一样,为什么这样呢?还差72个字节,这72个字节是什么呢? 因为elementData这个Object的数组引用也要占据一些空间
虽然elementData存了15个元素,但是不代表elementData数组长度就是15。数组默认长度是10,如果装不下了就扩容成1.5倍,就变成了15了,数组长度15就能被第三个student对象装得下。
15个elementData的元素,每个元素占用4个字节,15*4 = 60字节。
60+8个对象头的字节数+数组自己长度计数(4个字节) = 72字节
所以这就匹配上了。
因为第三个student对象在触发gc的时候只能有8个对象被回收掉,每个占用152个字节,8个就是1216。
1216再加上elementData元素的字节+对象头的字节数+数组自己长度计数 就是 1288个字节。
通过这个功能可以查看某个对象是否能被GC清理掉,是否存在内存泄露问题
结果发现这个对象被三个对象引用了。
那么我们查看另外一个对象还有没有被其它对象引用
下图发现只有他自己引用了这个对象,那么当他自己被回收了之后,这个对象也会被回收
说明
tomcat是最常用的Java Servlet 容器之一,同时也可以单独做为web服务器使用,tomcat本身是用Java语言实现的,并且运行在Java虚拟机之上,在大规模请求时,tomcat有可能会因为无法承受压力而发生内存溢出错误。
这里根据一个被压垮的tomcat堆快照文件,来分析tomcat在奔溃时候的内部情况。
导入hprof文件
堆大小有29.7MB,类一共有3.2K个,对象个数有833.8K个,类加载器有33个。
org.apache.catalina.session.StandardManager@0x6d2cfa8 这对象占用了16.4MB空间,0x6d2cfa8 的对象这个就是我们所谓的最大的对象
分析oom内存溢出的话,首先就是要分析占用最大对象,
鼠标左键查看这个最大的对象引用了哪些结构
发现 StandardManager里面有个sessions占用了17mb大概,占用很大
发现有那么多StandardSession,怀疑是tomcat短期之内收到了大量的Session导致的
随便选一个session对象,查看左边的信息,上面是创建时间,下面是结束的时间,这两个时间做一个差值,就相当于这个session对象在内存中出现的时间了。
这两个时间就相差了一毫秒,通过这两个时间就能获取到相关的信息了。
写OQL语句,查看StandardSession,发现第一个StandardSession的创建时间和最后一个StandardSession的创建时间的差值,右边的减去左边的,再 *1000 , 然后再被9941个session对象除掉,结果发现一秒得到311个对象。
由此推断,tomcat发生堆溢出时,tomcat在连续的30秒内,平均每秒接收了大概311次不同的客户端请求,导致创建了9941个session,结果导致tomcat发生了堆溢出了。
何为内存泄露?
可达性分析算法来判断对象是否是不在使用的对象,本质都是判断对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多内存泄露问题。(让JVM误以为此对象还在引用中,无法回收,造成内存泄露)
内存泄露的理解
严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄露。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做广泛意义上的内存泄露。
内存泄露与内存溢出的关系
申请了内存用完了不释放,通俗的讲就是【占着茅坑不拉屎】
申请内存时,没有足够的内存使用。
泄露的分类
静态集合类,如HashMao、linkedList等,如果这些容器是静态的,那么他们的生命周期和JVM是一致的,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄露。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
package jvm.classloader.jvm;import java.util.ArrayList;
import java.util.List;public class MemoryLeak1 {private static List list = new ArrayList();public void oomTest() {Object obj = new Object();list.add(obj);}
}
单例模式和静态集合导致的内存泄漏类似,因为单例的静态特性,它的生命周期和JVM的生命周期一样长,所以如果单例对象持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄露。
内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但是由于内部类持有外部类的实例对象,这个外部类对象就不会被垃圾回收,也会造成内存泄露。
在对数据库进行操作的过程中,首先要建立与数据库之间的连接,需要调用close方法来释放与数据库之间的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。
否则如果在访问数据库的过程中,对Connection、Statement等不显性的关闭,将会造成大量对象无法被回收,从而造成内存泄露。
内存泄露的另一个常见的来源是缓存,一旦你把对象引用放入到缓存中,他就很容易被遗忘。比如:之前项目在一次上线的时候,应用启动极慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题,可以使用WeakHashMap代替缓存,此Map的特点是,当除了自身有对key的引用外,此key没有其他引用,那么此map会自动丢弃此值。
package jvm.classloader.jvm;import java.sql.Time;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;public class MapTest {static Map wMap = new WeakHashMap<>();static Map map = new HashMap();public static void main(String[] args) {init();testWeakHashMap();testHashMap();}private static void init() {String ref1 = new String("object1");String ref2 = new String("object2");String ref3 = new String("object3");String ref4 = new String("object4");wMap.put(ref1, "cacheObject1");wMap.put(ref2, "cacheObject2");map.put(ref3, "cacheObject3");map.put(ref4, "cacheObject4");System.out.println("String 引用ref1,ref2,ref3,ref4 消失");}private static void testWeakHashMap() {System.out.println("WeakHashMap GC前");for (Object o : wMap.entrySet()) {System.out.println(o);}try {System.gc();TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("WeakHashMap GC后");for (Object o : wMap.entrySet()) {System.out.println(o);}}private static void testHashMap() {System.out.println("HashMap GC前");for (Object o : map.entrySet()) {System.out.println(o);}try {System.gc();TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("HashMap GC后");for (Object o : map.entrySet()) {System.out.println(o);}}}
上面代码合图示主要演示了WeakHashMap如何自动释放缓存对象,当init函数执行完成后,局部变量字符串weakd1,weakd2,d1,d2都会消失,此时只有静态map中保存对字符串对象的引用,可以看到,GC之后,HashMap的没有被回收,而WeakHashMap里面的缓存被回收了。
内存泄露另外常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会聚集。
需要确保回调立即被当做垃圾回收的最佳方法是只保存它的弱引用,例如将它们保存成为WeakHashMap中的键。
package jvm.classloader.jvm;import java.util.Arrays;
import java.util.EmptyStackException;public class Stack {private Object[] elements;private int size = 0;private static final int DEFAULT_INITIAL_CAPACITY = 16;public Stack() {elements = new Object[DEFAULT_INITIAL_CAPACITY];}/*** 入栈** @param e*/public void push(Object e) {ensureCapacity();elements[size++] = e;}private void ensureCapacity() {if (elements.length == size) {elements = Arrays.copyOf(elements, 2 * size + 1);}}/*** 出栈** @return*/public Object pop() {if (size == 0) {throw new EmptyStackException();}return elements[--size];}/*** 出栈** @return*/
// public Object pop() {
// if (size == 0) {
// throw new EmptyStackException();
// }
// Object result = elements[--size];
// elements[size] = null;
// return result;
// }}
分析:
上述程序没有明显的错误,但是下面这段程序有一个内存泄露,随着GC活动的增加,或者内存占用的不断增加,程序性能的降低就会表现出来,严重时可能导致内存泄露,但是这种失败情况相对较少。
当进行大量的pop操作时,由于引用未进行置空,gc是不会释放的。如果栈先增长,再收缩,那么从栈中弹出的对象将不会被当做垃圾回收,即使程序不再使用栈中的这些对象,他们也不会回收,因为栈中仍然保留着这些对象的引用,俗称过期引用,这个内存泄露很隐蔽。
MAT支持一种类似SQL的查询语句OQL(Object Query Language)。OQL使用类SQL语法,可以从堆内进行对象的查找和筛选。
在MAT中,select子句的格式和SQL基本一致,用于指定要显示的列。select子句中可以使用*,查看结果对象的引用实例(相当于outgoing references)。
SELECT * FROM java.util.Vector v
使用 OBJECTS 关键字,可以讲返回结果集中的项以对象的形式显示。
SELECT OBJECTS v.elementData FROM java.util.Vector v
在select子句中,使用 AS RETAINED SET 关键字可以得到所有对象的保留集。
SELECT AS RETAINED SET * FROM java.util.Vector
DISTINCT 关键字用于在结果集中去除重复对象。
SELECT DISTINCT OBJECTS classof(v) FROM java.util.Vector v
From子句用于执行查询范围,可以指定类名、正则表达式或者对象地址。
使用正则表达式,限定搜索范围,输出所有com.test包下的所有类的实例
SELECT * FROM “com \ .test \ …*”
使用类的地址进行搜索,好处是可以区分被不同类加载器加载的同一类型。
SELECT * FROM 0x37a0b4d
Where子句用于指定OQL的查询条件。OQL查询将只返回满足Where子句指定条件的对象。
where子句的格式与传统SQL极为相似。
下例返回长度大于10的char数组。
SELECT * FROM char[] s WHERE s.@length>10
下例返回包含“java”子字符串的所有宇符串,使用“LIKE”操作符,“LIKE”操作符的操作参数为正则表达式。
SELECT * FROM java.lang.String s WHERE toString(s) LIKE “. * java. *”
下例返回所有value域不为null的字符串,使用“=”操作符。
SELECT * FROM java.lang.String s where s. value != null
Where子句支持多个条件的AND、OR运算。下例返回数组长度大于15,并且深堆大于1000字节的所有Vector对象。
SELECT * FROM java.util.Vector v WHERE v.elementData.@length>15 AND v.@retainedHeapsize>1000
OQL中可以访问堆内对象的属性,也可以访问堆内代理对象的属性。访问堆内对象的属性时,格式如下:
[< alias >. ] < field >.< field >.< field >
其中alias为对象名称。
访问java.io.File对象的path属性,并进一步访问path的value属性:
SELECT toString(f.path. value ) FROM java.io.File f
下例显示了String对象的内容、objectid和objectAddress。
SELECT s. toString(), s.@objectId, s.@objectAddress FROM java.lang.String s
下例显示java.util.Vector内部数组的长度。
SELECT v.elementData.@length FROM java.util.Vector v
下例显示了所有的java.util.Vector对象及其子类型
SELECT * from INSTANCEOF java.util.Vector
在运行java的时候有时候想测试运行时占用内存情况,这时候就需要使用测试工具查看了。在eclipse里面有 Eclipse Memory Analyzer tool (MAT)插件可以测试。而在IDEA中也有这么一个插件,就是JProfiler。
Jprofiler 是由 ej-technologies 公司开发的一款Java 应用性能诊断工具。功能强大,但是收费。
下载地址:https://www.ej-technologies.com/download/jprofiler/version_100
方法调用,对方法调用的分析可以帮助您了解应用程序正在做什么,并找到提高其性能的方法
内存分配,通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄漏问题,优化内存使用
线程和锁,JProfiler提供多种针对线程和锁的分析视图助您发现多线程问题
高级子系统,许多性能问题都发生在更高的语义级别上。例如,对于JDBC调用,您可能希望找出执行最慢的SQL语句。JProfiler支持对这些子系统进行集成分析
安装与配置略,请参照网上教程(不同操作系统、不同安装版本不同)。
这是 JProfiler 全功能模式。在 class 加载之前,JProfier 把相关功能代码写入到需要分析的 class 的 bytecode 中,对正在运行的 jvm 有一定影响。
优点:功能强大。在此设置中,调用堆栈信息是准确的。
缺点:若要分析的 class 较多,则对应用的性能影响较大,CPU 开销可能很高(取决于 Filter 的控制)。因此使用此模式一般配合 Filter 使用,只对特定的类或包进行分析
类似于样本统计,每隔一定时间(5ms)将每个线程栈中方法栈中的信息统计出来。
优点:对 CPU 的开销非常低,对应用影响小(即使你不配置任何 Filter)
缺点:一些数据/特性不能提供(例如:方法的调用次数、执行时间)
注:JProfiler 本身没有指出数据的采集类型,这里的采集类型是针对方法调用的采集类型。因为JProfiler 的绝大多数核心功能都依赖方法调用采集的数据,所以可以直接认为是 JProfiler 的数据采集类型。
遥感监测 Telemetries
内存视图 Live Memory
4. 堆遍历 heap walker
官方地址:https://arthas.aliyun.com/doc/quick-start.html
Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪 Java 代码;实时监控 JVM 状态。Arthas 支持 JDK 6 +,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。当你遇到以下类似问题而束手无策时,Arthas 可以帮助你解决:
quit/exit 退出当前 Arthas客户端,其他 Arthas喜户端不受影响
stop/shutdown 关闭 Arthas服务端,所有 Arthas客户端全部退出
help 查看命令帮助信息
cat 打印文件内容,和linux里的cat命令类似
echo 打印参数,和linux里的echo命令类似
grep 匹配查找,和linux里的gep命令类似
tee 复制标隹输入到标准输出和指定的文件,和linux里的tee命令类似
pwd 返回当前的工作目录,和linux命令类似
cs 清空当前屏幕区域
session 查看当前会话的信息
reset 重置增强类,将被 Arthas增强过的类全部还原, Arthas服务端关闭时会重置所有增强过的类
version 输出当前目标Java进程所加载的 Arthas版本号
history 打印命令历史
keymap Arthas快捷键列表及自定义快捷键
4. monitor/watch/trace相关
5. 其他
在Oracle收购Sun之前,Oracle的JRockit虚拟机提供了一款叫做 JRockit Mission Control 的虚拟机诊断工具。
在Oracle收购sun之后,Oracle公司同时拥有了Hotspot和 JRockit 两款虚拟机。根据Oracle对于Java的战略,在今后的发展中,会将JRokit的优秀特性移植到Hotspot上。其中一个重要的改进就是在Sun的JDK中加入了JRockit的支持。
在Oracle JDK 7u40之后,Mission Control这款工具己经绑定在Oracle JDK中发布。
自Java11开始,本节介绍的JFR己经开源。但在之前的Java版本,JFR属于Commercial Feature通过Java虚拟机参数-XX:+UnlockCommercialFeatures 开启。
Java Mission Control(简称JMC) , Java官方提供的性能强劲的工具,是一个用于对 Java应用程序进行管理、监视、概要分析和故障排除的工具套件。它包含一个GUI客户端以及众多用来收集Java虚拟机性能数据的插件如 JMX Console(能够访问用来存放虚拟机齐个于系统运行数据的MXBeans)以及虚拟机内置的高效 profiling 工具 Java Flight Recorder(JFR)。
JMC的另一个优点就是:采用取样,而不是传统的代码植入技术,对应用性能的影响非常非常小,完全可以开着JMC来做压测(唯一影响可能是 full gc 多了)。
官方地址:https://github.com/JDKMissionControl/jmc
Java Flight Recorder
Java Flight Recorder是JMC的其中一个组件,能够以极低的性能开销收集Java虚拟机的性能数据。与其他工具相比,JFR的性能开销很小,在默认配置下平均低于1%。JFR能够直接访问虚拟机内的敌据并且不会影响虚拟机的优化。因此它非常适用于生产环境下满负荷运行的Java程序。
Java Flight Recorder 和 JDK Mission Control共同创建了一个完整的工具链。JDK Mission Control 可对 Java Flight Recorder 连续收集低水平和详细的运行时信息进行高效、详细的分析。
当启用时 JFR将记录运行过程中发生的一系列事件。其中包括Java层面的事件如线程事件、锁事件,以及Java虚拟机内部的事件,如新建对象,垃圾回收和即时编译事件。按照发生时机以及持续时间来划分,JFR的事件共有四种类型,它们分别为以下四种:
取样事件的其中一个常见例子便是方法抽样(Method Sampling),即每隔一段时问统计各个线程的栈轨迹。如果在这些抽样取得的栈轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法
在追求极致性能的场景下,了解你的程序运行过程中cpu在干什么很重要,火焰图就是一种非常直观的展示CPU在程序整个生命周期过程中时间分配的工具。火焰图对于现代的程序员不应该陌生,这个工具可以非常直观的显示出调用找中的CPU消耗瓶颈。
火焰图,简单通过x轴横条宽度来度量时间指标,y轴代表线程栈的层次。
案例: 使用JDK自身提供的工具进行JVM调优可以将下 TPS 由2.5提升到20(提升了7倍),并准确 定位系统瓶颈。
系统瓶颈有:应用里释态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。
那么,如何在海量业务代码里边准确定位这些性能代码?这里使用阿里开源工具 Tprofiler 来定位 这些性能代码,成功解决掉了GC 过于频繁的性能瓶预,并最终在上次优化的基础上将 TPS 再提升了4倍,即提升到100。
Tprofiler配置部署、远程操作、 日志阅谈都不太复杂,操作还是很简单的。但是其却是能够 起到一针见血、立竿见影的效果,帮我们解决了GC过于频繁的性能瓶预。
Tprofiler最重要的特性就是能够统汁出你指定时间段内 JVM 的 top method 这些 top method 极有可能就是造成你 JVM 性能瓶颈的元凶。这是其他大多数 JVM 调优工具所不具备的,包括 JRockit Mission Control。JRokit 首席开发者 Marcus Hirt 在其私人博客《 Lom Overhead Method Profiling cith Java Mission Control》下的评论中曾明确指出 JRMC 井不支持 TOP 方法的 统计。
官方地址:http://github.com/alibaba/Tprofiler
常见的动态追踪工具有BTrace、HouseHD(该项目己经停止开发)、Greys-Anatomy(国人开发 个人开发者)、Byteman(JBoss出品),注意Java运行时追踪工具井不限干这几种,但是这几个是相对比较常用的。
BTrace是SUN Kenai 云计算开发平台下的一个开源项目,旨在为java提供安全可靠的动态跟踪分析工具。先看一卜日Trace的官方定义:
大概意思是一个 Java 平台的安全的动态追踪工具,可以用来动态地追踪一个运行的 Java 程序。BTrace动态调整目标应用程序的类以注入跟踪代码(“字节码跟踪“)。