笔记参考于尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
1.工具概述
使用上一章命令行工具或组合能帮您获取目标Java应用性能相关的基础信息,但它们存在下列局限:
- 无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。
- 要求用户登录到目标Java应用所在的宿主机上,使用起来不是很方便。
- 分析数据通过终端输出,结果展示不够直观。
为此, JDK提供了一些内存泄漏的分析工具,如jconsole, jvisualvm等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求,所以这里我们介绍的工具相对多一些、丰富一些。
图形化综合诊断工具:
JDK自带的工具:
- jconsole:JDK自带的可视化监控工具。查看Java应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等。>位置: jdk\bin\jconsole.exe
- Visual VM:Visual VM是一个工具,它提供了一个可视界面,用于查看Java虚拟机上运行的基于Java技术的应用程序的详细信息。>位置: jdk\bin\jvisualvm.exe
- JMC:Java Mission Control,内置Java Flight Recorder.能够以极低的性能开销收集Java虚拟机的性能数据。>位置: jdk\bin\jmc.exe
第三方工具:
- MAT: MAT(Memory Analyzer Tool)是基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗(Eclipse的插件形式)
- JProfiler:商业软件,需要付费。功能强大。(与 VisualVM类似)
- Arthas:Alibaba开源的Java诊断工具。深受开发者喜爱。
- Btrace: Java运行时追踪工具,可以在不停机的情况下,跟踪指定的方法调用、构造函数调用和系统内存等信息。
2.jConsole
1.基本概述
jconsole:
- 从Java5开始,在JDK中自带的java监控和管理控制台。
- 用于对JVM中内存、线程和类等的监控,是一个基于JMX(java management extensions)的GUI性能监控工具
官方教程:Using JConsole – Java SE Monitoring and Management Guide (oracle.com)
2.启动
-Xms600m -Xmx600m -XX:SurvivorRatio=8
代码
package com.yutian.jconsole;
import java.util.ArrayList;
import java.util.Random;
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 100)];
public static void main(String[] args) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
while (true) {
list.add(new HeapInstanceTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
概览

内存



可手动GC


使用之前会造成死锁的代码,再次连接后:
可以直接检测死锁


3.三种连接方式
Local:
使用JConsole连接一个正在本地系统运行的JVM,并且执行程序的和运行JConsole的需要是同一个用户。JConsole使用文件系统的授权通过RMI连接器连接到平台的MBean服务器上。这种从本地连接的监控能力只有Sun的JDK具有。
Remote:
使用下面的URL通过RMI连接器连接到一个JMX代理,service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi。JConsole为建立连接,需要在环境变量中设置mx.remote.credentials来指定用户名和密码,从而进行授权。
Advanced:
使用一个特殊的URL连接JMX代理。一般情况使用自己定制的连接器而不是RMI提供的连接器来连接JMX代理,或者是一个使用JDK1.4的实现了JMX和JMX Rmote的应用。
3.Visual VM
基本概述
- Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具。
- 它集成了多个JDK命令行工具,使用Visual VM可用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替JConsole.
- 在JDK 6 Update 7以后, Visual VM便作为JDK的一部分发布(VisualVM在JDK/bin目录下),即:它完全免费。
Linux:在Tomcat中开启允许远程rmi远程连接,在windows上就可进行远程连接了
此外,Visual VM也可以作为独立的软件安装:首页: https://visualvm.github.io/index.html
插件的安装
Visual VM的一大特点是支持插件扩展,并且插件安装非常方便。我们既可以通过离线下载插件文件*.nbm,然后在Plugin对话框的己下载页面下,添加己下载的插件。也可以在可用插件页面下,在线安装插件。(这里建议安装上:VisualGC)
插件地址: https://visualvm.github.io/pluginscenters.html
也可以直接在软件下VisualGC

再来在idea也装插件Visual VM

会多出几个图标:

同时我们需要给Visual VM配置:
在Settings中


连接方式
本地连接:
监控本地Java进程的CPU、类、线程等
远程连接:
- 确定远程服务器的ip地址
- 添加JMX (通过JMX技术具体监控远端服务器哪个Java进程)
- 修改bin/catalina.sh文件,连接远程的tomcat
- 在…/conf中添加jmxremote.access和jmxremote.password文件
- 将服务器地址改为公网ip地址
- 设置阿里云安全策略和防火墙策略
- 启动tomcat,查看tomcat启动日志和端口监听
- JMX中输入端口号、用户名、密码登录
主要功能
使用下述代码讲解
-Xms600m -Xmx600m -XX:SurvivorRatio=8
package com.yutian.jvisualvm;
import java.util.ArrayList;
import java.util.Random;
/**
* -Xms600m -Xmx600m -XX:SurvivorRatio=8
*/
public class OOMTest {
public static void main(String[] args) {
ArrayList<Picture> 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];
}
}
1.生成/读取堆内存快照

或














2.查看JVM参数和系统属性
看到设置的JVM参数

3.查看运行中的虚拟机进程
线程:

4.生成/读取线程快照
使用会产生死锁的代码:
-Xms600m -Xmx600m -XX:SurvivorRatio=8
package com.yutian.jconsole;
import java.util.ArrayList;
import java.util.Random;
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 100)];
public static void main(String[] args) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ArrayList<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
while (true) {
list.add(new HeapInstanceTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}可以看到显示死锁


或


5.程序资源的实时监控
监视:右上角也可以手动GC和dump

抽样器:





6.其他功能
- JMX代理连接
- 远程环境监控
- CPU分析和内存分析
4.eclipse MAT
1.基本概述
MAT(Memory Analyzer Tool)工具是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。
MAT是基于Eclipse开发的,不仅可以单独使用,还可以作为插件的形式嵌入在Eclipse中使用。是一款免费的性能分析工具,使用起来非常方便。
下载网址:
Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation
2.获取堆dump文件
1.dump文件内容
MAT可以分析heap dump文件。在进行内存分析时,只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前的内存信息。
一般说来,这些内存信息包含:
- 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
- 所有的类信息,包括classloader、类名称、父类、静态变量等
- GCRoot到所有的这些对象的引用路径
- 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)
2.两点说明
说明1:
缺点:MAT不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如Sun, HP, SAP所采用的HPROF二进制堆存储文件,以及IBM的PHD堆存储文件等都能被很好的解析。
说明2:
最吸引人的还是能够快速为开发人员生成内存泄漏报表,方便定位问题和分析问题。虽然MAT有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从MAT展现给我们的信息当中通过经验和直觉来判断才能发现。
3.获取dump文件(对前面的总结)
方法一:通过前一章介绍的jmap工具生成,可以生成任意一个java进程的dump文件
方法二:通过配置JVM参数生成。
- 选项”-XX:+HeapDumpOnOutOfMemoryError”或 “-XX:+HeapDumpBeforeFullGC”
- 选项”-XX:HeapDumpPath”所代表的含义就是当程序出现OutofMemory时,将会在相应的目录下生成一份dump文件。如果不指定选项”-XX:HeapDumpPath”则在当前目录下生成dump文件。
对比:考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用jmap+MAT工具是最常见的组合。
方法三:使用VisualVM可以导出堆dump文件
方法四:使用MAT既可以打开一个已有的堆快照,也可以通过MAT直接从活动Java程序中导出堆快照。该功能将借助jps列出当前正在运行的Java进程,以供选择并获取快照。
方法四如图:






3.分析堆dump文件
查看泄露疑点


histogram
展示了各个类的实例数目以及这些实例的Shallowheap 或Retainedheap的总和
以下两个图标点击都能打开histogram



如果,类太多,难以找到,可以分组:

还可以排序:右键

还可以使用正则表达式:


比如,怀疑哪个类,查看他关联的GC Root,再排除弱应用之类的

还可以与另一个dump文件比较:
先打开histogram



右击也可以排序
thread overview
- 查看系统中的Java线程
- 查看局部变量的信息

用法:
找到出问题的

再打开thread overview

右击,查看引用和被引用,看看有没有因为,错误的引用导致内存泄漏

获得对象相互引用的关系
with outgoing references
with incoming references
上述,即为用法
浅堆与深堆
shallow heap:
浅堆(Shallow Heap)是指一个对象所消耗的内存。在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节, long型变量会占据8个字节,每个对象头需要占用8个字节。根据堆快照格式不同,对象的大小可能会向8字节进行对齐。
以String为例:以下为其实例数据(2个int值和一个对象引用), 2个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占24字节。(jdk7中例子,jdk8会有变化)

这24字节为String对象的浅堆大小。它与String的value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。
retained heap:
保留集(Retained Set):
对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或间接访问到的所有对象的集合。通俗地说,就是指仅被对象A所持有的对象的集合。(比如只被对象A引用的值)
深堆(Retained Heap):
深堆是指对象的保留集中所有的对象的浅堆大小之和。
注意:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。
补充:对象实际大小
另外一个常用的概念是对象的实际大小。这里,对象的实际大小定义为一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小。与深堆相比,似乎这个在日常开发中更为直观和被人接受,但实际上,这个概念和垃圾回收无关。
下图显示了一个简单的对象引用关系图,对象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
这里不包括D对象,因为D对象被GC Roots直接引用。如果GC Roots不引用D对象呢?

案例分析:StudentTrace
package com.yutian.mat;
import java.util.ArrayList;
import java.util.List;
/**
* 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。
* 它由三个部分组成:Student、WebPage和StudentTrace三个类
*/
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;
}
}
获取dump文件
-XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=d:\JPO\student.hprof
使用MAT打开:
打开线程:

在main线程中的浅堆一样:


扩展:
在上面我们举例过浅堆大小的计算,当时我们以32位系统举例,而在64位系统则更复杂
一个对象占用的内存大小可以分为下面三部分:
- 对象头
- 对象成员占用内存
- 内存对齐
首先看来对象头:对象头主要分为三部分:
- 运行时元数据:这个主要保存了对象的哈希值、GC分代年龄、锁信息等等占用8个字节。
- 类型指针:指向方法区该类的Klass(对,你没看错就是klass,具体是什么大家有兴趣下去自行了解)
- 数组长度:如果当前对象是数组类型的,那么还会拥有4字节的数组长度(只有数组对象保存了这部分数据。)
那么对象头的大小=8 + 指针大小 + [4:如果是数组的话]
而指针大小不是通常意义的4个字节
Java Tuning White Paper (oracle.com)

在堆大小小于32G的情况下,我们默认采用了压缩指针,对象引用会变成32位偏移量,也就是4个字节。
所以该指针在32位JVM中的长度是32bit(4个字节),在64位JVM中长度是64bit。
结论: 普通对象的对象头大小为12字节,数组为16字节。
考虑Lily同学:
默认采用了指针压缩则为12字节,没有采用则为16字节(数组还需要加上数组长度)。
1.为什么有152字节和144字节:
因为我们的URL和content存在两种情况:
- URL:”http://www.7.com”、content:”7″
- URL:”http://www.14.com”、content:”14″
第一种URL长度为16,底层的char数组的占用空间为:
- 数组对象头(16字节) + 16个字符(32字节) = 48字节,符合8字节对齐。
- 同理content 占用 数组对象头(16字节) + 一个字符(2字节) = 18字节,八字节对齐后等于24字节。
第二种URL长度为17,底层的插入数组的占用空间为:
- 数组对象头(16字节) + 17个字符(34) = 50字节,不符合8字节对齐,对齐为56
- 同理content 占用 数组对象头(16字节) + 两个字符(4) = 20字节,八字节对齐=24字节
所以第一种总字节为48 + 24 = 72,
第二种总字节为56 + 24 = 80
因此第二种比第一种多了8字节,所以是152和144。
2.为什么最终结果是1288:
首先ElementData数组本身的浅堆大小为:
- 数组对象头(16字节) + 数组内容【15个Obejct引用=16*4】 = 76,八字节对齐=80字节
- 15个Object分为13个152字节+2个144字节,总大小为=2264字节
- 7号和其他student重复的有0、21、42、63、84、35、70总计6个152和1一个144
所以2264 – 6 * 152 – 144 = 1208字节
所以ElementData本身的浅堆80 + 仅能通过它到达的浅堆1208 = 1288

如下图,刚好数组长度15,存放也15个,因为数组扩容,10个变15个,如果是16个,还需扩容,变20个·

支配树
支配树(Dominator Tree)
支配树的概念源自图论。
从根结点出发
MAT提供了一个称为支配树(Dominator Tree)的对象图。支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象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中,单击工具栏上的对象支配树按钮,可以打开对象支配树视图。


搜索一下Thread,还是找到Lily
和上面线程不一样,lily虽然数组扩容为10个了,但只有8个,因为这是支配树,不能被其他访问到。
案例:Tomcat堆溢出分析
说明
Tomcat是最常用的Java Servlet容器之一,同时也可以当做单独的Web服务器使用。Tomcat本身使用Java实现,并运行于Java虚拟机之上。在大规模请求时,Tomcat有可能会因为无法承受压力而发生内存溢出错误。这里根据一个被压垮的Tomcat的堆快照文件,来分析Tomcat在崩溃时的内部情况。
分析过程
第一个黑框大小为堆大小,第二个黑框大小为最大对象大小

对最大对象点右键,查看他引用了什么对象

sessions对象,它占用了整个约17MB空间

可以看到sessions对象为ConcurrentHashMap,其内部分为16个Segment。从深堆大小看,每个Segment都比较平均,大约为1MB,合计17MB。

Segment

搜索一下session


当前堆中含有9941个session,并且每一个session的深堆为1592字节,合计约15MB,达到当前堆大小的50%。
再点击一下,在左边有其详细信息:分别为创建时间和结束时间


根据当前的session总数,可以计算每秒的平均压力为: 9941/(1403324677648-1403324645728)*1000=311次/秒。
由此推断,在发生Tomcat堆溢出时, Tomcat在连续30秒的时间内,平均每秒接收了约311次不同客户端的请求,创建了合计9941个session。
补充1:再谈内存泄漏
1.内存泄漏的理解与分类
何为内存泄漏(memory leak)

可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)。
内存泄漏(memory leak)的理解
严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。

对象x引用对象Y, x的生命周期比Y的生命周期长;
那么当Y生命周期结束的时候,x依然引用着Y,这时候,垃圾回收期是不会回收对象Y的;
如果对象X还引用着生命周期比较短的A、B、C,对象A又引用着对象a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。
内存泄漏与内存溢出的关系:
1.内存泄漏(memory leak )申请了内存用完了不释放,比如一共有1024M的内存,分配了521M的内存一直不回收,那么可以用的内存只有521M了,仿佛泄露掉了一部分;
2. 内存溢出(out of memory)申请内存时,没有足够的内存可以使用。
可见,内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出,
泄漏的分类:
经常发生:发生内存泄露的代码会被多次执行,每次执行,泄露一块内存
偶然发生:在某些特定情况下才会发生
一次性:发生内存泄露的方法只会执行一次
隐式泄漏:一直占着内存不释放,直到执行结束:严格的说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽。
2.Java中内存泄漏的8种情况
1.静态集合类
静态集合类,如HashMap, LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
package com.yutian.mat;
import java.util.ArrayList;
import java.util.List;
public class MemoryLeak {
static List list = new ArrayList<>();
public static void oomTests() {
Object obj = new Object();
list.add(obj);
}
}
2.单例模式
单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和JVM的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。
3.内部类持有外部类
内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
4.各种连接,如数据库连接、网络连接和10连接等
各种连接,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、 Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏,
package com.yutian.mat;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class NetMemoryLeak {
public static void main(String[] args) {
Connection conn = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("url","","");
Statement statement = conn.createStatement();
ResultSet rs = statement.executeQuery("......");
} catch (Exception e) {//异常日志
}finally {
//1. 关闭结果集 Statement
// 2.关闭声明的对象 ResultSet
// 3. 关闭连接 Connection
}
}
}5.变量不合理的作用域
变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
public class UsingRandom{
private String msg;
public void receiveMsg(){
readFromNet();//从网络中接受数据保存到msg中
saveDB();//ו把msg保存到数据库
}
}如上面这个伪代码,通过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不可变。
package com.yutian.memoryleak;
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 +
'}';
}
}
package com.yutian.memoryleak;
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 + '\'' +
'}';
}
}
7.缓存泄漏
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。
package com.yutian.memoryleak;
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
* Disconnected from the target VM, address: '127.0.0.1:51628', transport: 'socket'
* HashMap GC之后
* obejct4=cacheObject4
* obejct3=cacheObject3
**/

上面代码和图示主演演示WeakHashMap如何自动释放缓存对象,当init函数执行完成后,局部交量字符串引用weakd1,weakd2,d1,d2都会消失,此时只有静态map中保存中对字符串对象的引用,可以看到,调用gc之后, HashMap的没有被回收,而WeakHashMap里面的缓存被回收了。
8.监听器和回调
内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为WeakHashMap中的键。
3.内存泄漏案例分析
案例1:
package com.yutian.memoryleak;
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];
}
public void push(Object e) { //入栈
ensureCapacity();
elements[size++] = e;
}
//存在内存泄漏
public Object pop() { //出栈
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}上述程序并没有明显的错误,但是这段程序有一个内存泄漏,随着GC活动的增加,或者内存占用的不断增加,程序性能的降低就会表现出来,严重时可导致内存泄漏,但是这种失败情况相对较少。代码的主要问题在pop函数,下面通过这张图示展现假设这个栈一直增长,增长后如下图所示
return elements[--size];

当进行大量的pop操作时,由于引用未进行置空,gc是不会释放的,如下图所示

从上图中看以看出,如果栈先增长,在收缩,那么从栈中弹出的对象将不会被当作垃圾回收,即使程序不再使用栈中的这些队象,他们也不会回收,因为栈中仍然保存这对象的引用,俗称过期引用,这个内存泄露很隐蔽。
正确使用:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}补充2:支持使用OQL语言查询对象信息
MAT支持一种类似于SQL的查询语言OQL (Object Query Language) . OQL使用类SQL语法,可以在堆中进行对象的查找和筛选。


按F1可以进入help
按F5可以执行
1.SELECT子句:
Select子句:在MAT中,Select子句的格式与SQL基本一致,用于指定要显示的列。Select子句中可以使用“*”,查看结果对象的引用实例(相当于outgoing references) 。
SELECT * FROM java.util.Vector v
使用“OBJECTS”关键字,可以将返回结果集中的项以对象的形式显示。
SELECT objects v.elementData FROM java.util.Vector v
SELECT OBJECTS s.value FROM java.lang.String s
在Select子句中,使用“AS RETAINED SET”关键字可以得到所得对象的保留集。
SELECT AS RETAINED SET * FROM com.yutian.mat.Student

“DISTINCT”关键字用于在结果集中去除重复对象。
SELECT DISTINCT OBJECTS classof(s) FROM java.lang.String s
2.FROM子句:
From子句用于指定查询范围,它可以指定类名、正则表达式或者对象地址。
SELECT * FROM java.lang.String s
下例使用正则表达式,限定搜索范围,输出所有com.yutian包下所有类的实例
SELECT * FROM "com\.yutian\..*"
也可以直接使用类的地址进行搜索。使用类的地址的好处是可以区分被不同ClassLoader加载的同一种类型。
select * from 0x37a0b4dI
输入语句后,按F5

3.WHERE子句
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
参考资料:
尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ
周志明:深入理解java虚拟机


