JVM监控及诊断的GUI工具
VisualVM:多合一故障处理工具
VisualVM(All-in-One Java Troubleshooting Tool)是功能最强大的运行监视和故障处理程序之一,曾经在很长一段时间内是Oracle官方主力发展的虚拟机故障处理工具。Oracle曾在VisualVM的软件说明中写上了“All-in-One”的字样,预示着它除了常规的运行监视、故障处理外,还将提供其他方面的能力,譬如性能分析(Profiling)。VisualVM的性能分析功能比起JProfiler、YourKit等专业且收费的Profiling工具都不遑多让。而且相比这些第三方工具,VisualVM还有一个很大的优点:不需要被监视的程序基于特殊Agent去运行,因此它的通用性很强,对应用程序实际性能的影响也较小,使得它可以直接应用在生产环境中。这个优点是JProfiler、YourKit等工具无法与之媲美的。
插件Profiler:
注意 在JDK 5之后,在客户端模式下的虚拟机加入并且自动开启了类共享——这是一个在多虚拟机进程共享 rt.jar
中类数据以提高加载速度和节省内存的优化,而根据相关Bug报告的反映,VisualVM的 Profiler 功能会因为类共享而导致被监视的应用程序崩溃,所以进行Profiling前,最好在被监视程序中使用 -Xshare:off
参数来关闭类共享优化。
eclipse-MAT
MAT(Memory Analyzer Tool)工具是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。
MAT是基于Eclipse开发的,不仅可以单独使用,还可以作为插件形式嵌入Eclipse中使用。是一款免费的性能分析工具,使用起来非常方便。
官网下载地址:
https://www.eclipse.org/mat/downloads.php
获取堆dump文件
dump文件内容
MAT可以分析heap dump文件。在进行内存分析时,只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前的内存信息。
一般来说,这些内存信息包含:
- 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
- 所有的类信息,包括classloader、类名称、父类、静态变量等。
- GCRoot到所有的这些对象的引用路径。
- 线程信息,包括线程的调用栈以及此线程的线程局部变量(TLS)
MAT优缺点
缺点:MAT不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如:Sun,HP,SAP所采用的HPROF二进制堆存储文件,以及IBM的PHD堆存储文件等都能被很好的解析。
优点:最吸引人的还是能够快速为开发人员生成内存泄漏表,方便定位问题和解析问题。虽然MAT有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从MAT展现给我们的信息当中通过经验和直觉来判断才能发现。
获取dump文件
方法一:通过 jmap 工具生成,可以生成任意一个java进程的dump文件;
方法二:通过配置JVM参数生成。
- 选项
"-XX:+HeapDumpOrOutOfMemoryError"
或"-XX:+HeapDumpBeforeFullGC"
- 选项
-XX:HeapDumpPath
所代表的含义就是当程序出现OutofMemory时,将会在相应的目录下生成一份dump文件。如果不指定选项"XX:HeapDumpPath"
则在当前目录下生成dump文件。
对比:考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用 jmap + MAT 工具是最常见的组合。
方法三:使用VisualVM 可以导出堆dump文件。
方法四:使用MAT既可以打开一个已有的堆快照,也可以通过MAT直接从活动Java程序中导出堆快照。使用该功能将借助 jps 列出当前正在运行的Java进程,以供选择并获取快照。
分析堆dump文件
- Leak Suspects Report(泄漏疑点报告)
自动检测堆dump文件查看哪些是可疑内存泄漏的疑点,报告里说明哪些对象还存活以及为什么这些对象没有被垃圾回收收集回收。
- Component Report(组件报告)
分析一系列对象的集合,找到可疑的内存空间,例如重复字符串,空集合,finalizer,弱引用。
- Re-open previously run reports(重新打开之前运行过的报告)
之前的运行过的报告在与dump同一个目录的zip文件里。
Overview概述
histogram直方图
展示了各个类的实例数目以及这些实例的Shallowheap 或 Retainedheap的总和。
thread overview线程概述
-
查看系统中的Java线程
-
查看局部变量的信息
-
获得对象相互引用的关系
with outgoing references
with incoming references
浅堆与深堆
浅堆shallow heap
浅堆是指一个对象所消耗的内存。在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。根据堆快照格式不同,对象的大小可能会向8字节进行对齐。
以String为例:2个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占24字节。(JDK7中)
int | hash32 | 0 |
int | hash | 0 |
ref | value | Class xxx |
这24字节为String对象的浅堆大小。它与String的value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。
深堆retained heap
保留集(Retained Set)
对象A的保留集指当对象A被垃圾回收后,可以被释放的所有对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或间接访问到的所有对象的集合。通俗地说,就是指仅被对象A所持有的对象的集合。
对象S1、S2的value属性值都指向字符串常量池的"Hello",如果S2对象value属性没有指向常量池的"Hello",在回收对象S1时,因"Hello"只能由S1直接或间接访问到,所以回收了S1同时也应该计算常量池中"Hello",它属于S1的保留集。如果S2对象value属性指向常量池常量,回收S1时,常量池中"Hello"不应该算在S1的保留集内。
深堆是指对象的保留集中所有的对象的浅堆大小之和。
⚠注意:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。
补充:对象实际大小
另外一个常用的概念是对象的实际大小,这里,对象的实际大小定义为一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小。与深堆相比,似乎这个在日常开发中更为直观和被人接受,但实际上,这个概念和垃圾回收无关。
下图显示了一个简单的对象引用关系图,对象A引用了C和D,对象B引用了C和E。那么对象A的浅堆大小只是A本身,不含C和D,而A的实际大小为A、C、D三者之和。而A的深堆大小为A与D之和,由于对象C还可以通过对象B访问到,因此不在对象A的深堆范围内。
理解Retained Size练习
上图中,GC Roots直接引用了A和B两个对象。
A对象的深堆大小Retained Size=A对象的Shallow Size。
B对象的深堆大小Retained Size=B对象的Shallow Size + C对象的Shallow Size。
如果GC Roots不引用D对象呢?
A对象的深堆大小Retained Size=A对象的Shallow Size。
B对象的深堆大小Retained Size=B对象的Shallow Size + C对象的Shallow Size + D对象的Shallow Size。
案例分析:StudentTrace
示例代码:
import java.util.ArrayList; import java.util.List; /** * 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。 * 它由三个部分组成:Student、WebPage和StudentTrace三个类 * * VM options配置: -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=d:\student.hprof */ public class StudentTrace { static List<WebPage> webpages = new ArrayList<WebPage>(); 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(); System.gc(); } } class Student { private int id; private String name; private List<WebPage> 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<WebPage> getHistory() { return history; } public void setHistory(List<WebPage> 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; } }
Full GC前导出dump文件配置
线程概述浅堆大小分析:
也可以使用with outgoing references
、with incoming references
查看WebPage类被那些类所引用。被多个Student类所引用的WebPage类,在当前Student类被回收时不能被回收,所以每个Student类深堆大小是不同的。
支配树
支配树(Dominator Tree),概念源自图论。MAT提供了一个称为支配树的对象图。支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。支配树是基于对象间的引用图所建立的,它有以下基本性质:
- 对象A的子树(所有被对象A支配的对象集合)表示对象A的保留集(retained set),即深堆。
- 如果对象A支配对象B,那么对象A的直接支配者也支配对象B。
- 支配树的边与对象引用图的边不直接对应。
如下图所示:左图表示对象引用图,右图表示左图所对应的支配树。对象A和B由根对象直接支配,由于在到对象C的路径中,可以经过A,也可以经过B,因此对象C的直接支配者也是根对象。对象F与对象D相互引用,因为到对象F的所有路径必然经过对象D,因此,对象D是对象F的直接支配者。而到对象D的所有路径中,必然经过对象C,即使是从对象F到对象D的引用,从根节点出发,也是经过对象C的,所以,对象D的直接支配者为对象C。
同理,对象E支配对象G。到达对象H的可以通过对象D,也可以通过对象E,因此对象D和E都不能支配对象H,而经过对象C既可以到达D也可以到达E,因此对象C为对象H的直接支配者。
MAT工具栏打开对象支配树:
内存泄漏
什么是内存泄漏?
内存泄漏(memory leak):可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)。
内存泄漏的理解
严格来说,只有对象不会再被程序用到了,但是GC又不能回收它们的情况,才叫内存泄漏。但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的"内存泄漏"。
对象X引用对象Y,X的生命周期比Y的生命周期长;那么当Y生命周期结束的时候,X依然引用着Y,这时候,垃圾回收器不会回收对象Y;如果对象X还引用着生命周期比较短的A、B、C,对象A又引用着对象a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。
内存泄漏与内存溢出的关系
1、内存泄漏(memory leak)
申请了内存用完不释放,比如一共有1024M的内存,分配了512M的内存一直不回收,那么可以用的内存只有512M了,仿佛泄漏掉了一部分;
2、内存溢出(out of memory)
申请内存时,没有足够的内存可以使用;
可见内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出。
内存泄漏的分类
经常发生:发生内存泄漏的代码会被多次执行,每次执行,泄漏一块内存;
偶然发生:在某些特定情况下才会发生;
一次性:发生内存泄漏的方法只会执行一次;
隐式泄漏:一直占着内存不释放,直到执行结束;严格的说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽。
Java中内存泄漏的8中情况
1.静态集合类
静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
public class MemorLeak { static List list = new ArrayList(); public void oomTests() { Object obj = new Object(); list.add(obj); } }
2.单例模式
单例模式,和静态集合导致内存泄漏的原因类似,因为单例的静态特性,它的生命周期和JVM的生命周期一样长,所以单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成 内存泄漏。
3.内部类持有外部类
内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内存类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
4.各种连接,如数据库连接、网络连接和IO连接等
各种连接,如数据库连接、网络连接和IO连接等,在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性的关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
public static void main(String[] args) { try { Connection conn = null; Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("..."); } catch (Exception e) { } finally { // 关闭结果集 Statement // 关闭声明的对象 ResultSet // 关闭连接 Connection } }
5.变量不合理的作用域
变量不合理的作用域,一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
public class UsingRandom { private String msg; public void receiveMsg() { readFromNet(); // 从网络中接收数据保存到msg中 saveDB(); // 把msg保存到数据库中 // msg = null; } }
如上面伪代码,通过readFromNet方法把接收的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。
实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间。
6.改变哈希值
改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这中情况下,即使在contains方法使用该对象当前引用作为的参数区HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。
这也是String 为什么被设置成了不可变类型,我们可以放心地把String存入HashSet,或者把String当做HashMap的key值。当我们想把自己定义的类保存到散列表的时候,需要保证对象的hashCode不可变。
举个🌰1:
import java.util.HashSet; /** * 演示内存泄漏 */ public class ChangeHashCode { public static void main(String[] args) { HashSet set = new HashSet(); Person p1 = new Person(1001, "AA"); Person p2 = new Person(1002, "BB"); set.add(p1); set.add(p2); p1.name = "CC";//导致了内存的泄漏 set.remove(p1); //删除失败 System.out.println(set); set.add(new Person(1001, "CC")); System.out.println(set); set.add(new Person(1001, "AA")); System.out.println(set); } } class Person { int id; String name; public Person(int id, String name) { this.id = id; this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Person)) return false; Person person = (Person) o; if (id != person.id) return false; return name != null ? name.equals(person.name) : person.name == null; } @Override public int hashCode() { int result = id; result = 31 * result + (name != null ? name.hashCode() : 0); return result; } @Override public String toString() { return "Person{" + "id=" + id + ", name='" + name + '\'' + '}'; } }
打印结果:
[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}] [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}] [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}] Process finished with exit code 0
举个🌰2:
import java.util.HashSet; /** * 演示内存泄漏 */ public class ChangeHashCode1 { public static void main(String[] args) { HashSet<Point> hs = new HashSet<Point>(); Point cc = new Point(); cc.setX(10);//hashCode = 41 hs.add(cc); cc.setX(20);//hashCode = 51 此行为导致了内存的泄漏 System.out.println("hs.remove = " + hs.remove(cc));//false hs.add(cc); System.out.println("hs.size = " + hs.size());//size = 2 System.out.println(hs); } } class Point { int x; public int getX() { return x; } public void setX(int x) { this.x = x; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + x; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Point other = (Point) obj; if (x != other.x) return false; return true; } @Override public String toString() { return "Point{" + "x=" + x + '}'; } }
打印结果:
hs.remove = false hs.size = 2 [Point{x=20}, Point{x=20}] Process finished with exit code 0
7.缓存泄漏
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到卡死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是:当除了自身有对key的引用外,此key没有其他引用,那么此map会自动丢弃此值。
举个🌰:
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(); } public static void init() { String ref1 = new String("obejct1"); String ref2 = new String("obejct2"); String ref3 = new String("obejct3"); String ref4 = new String("obejct4"); wMap.put(ref1, "cacheObject1"); wMap.put(ref2, "cacheObject2"); map.put(ref3, "cacheObject3"); map.put(ref4, "cacheObject4"); System.out.println("String引用ref1,ref2,ref3,ref4 消失"); } public 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) { e.printStackTrace(); } System.out.println("WeakHashMap GC之后"); for (Object o : wMap.entrySet()) { System.out.println(o); } } public 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) { e.printStackTrace(); } System.out.println("HashMap GC之后"); for (Object o : map.entrySet()) { System.out.println(o); } } }
输出结果:
String引用ref1,ref2,ref3,ref4 消失 WeakHashMap GC之前 obejct2=cacheObject2 obejct1=cacheObject1 WeakHashMap GC之后 // 这里可以看出被回收了,没有打印 HashMap GC之前 obejct4=cacheObject4 obejct3=cacheObject3 HashMap GC之后 obejct4=cacheObject4 // 这里没有被回收 obejct3=cacheObject3 Process finished with exit code 0
8.监听器和回调
内存泄漏另一个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显式的取消,那么就会积聚。需要确保回调立即被当做垃圾回收的最佳方法是只保存它的弱引用,例如将它们保存成为WeakHashMap中的键。
Arthas
TProfiler
下载地址:https://githup.com.alibaba/TProfiler
Spring Insight
本文来自博客园,作者:Lz_蚂蚱,转载请注明原文链接:https://www.cnblogs.com/leizia/p/16990510.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2020-12-18 06-数据库事务