深入理解JVM(二十)一一JVM监控及诊断工具(GUI)

JVM监控及诊断工具(GUI)

jconsole

  • 从Java5开始,在JDK中自带的java监控和管理控制台。
  • 用于对JVM中内存、线程和类等的监控,是一个基于JMX(java management extensions )的GUI性能监控工具。
  • 官方教程
  1. 启动
  • jdk/bin目录下,启动jconsole.exe命令即可
  • cmd中输入jconsole即可
  1. 三种连接方式
  • Local
    • 使用JConsole连接一个正在本地系统运行的JVM,并且执行程序的和运行JConsole的需要是同一个用户。JConsole使用文件系统的授权通过RMl连接器连接到平台的MBean服务器上。这种从本地连接的监控能力只有Sun的JDK具有。
  • Remote
    • 使用下面的URL通过RMI连接器连接到一个JMX代理,service:jmx.rmi:///jndi/rmi://hostNane:portNum/jmxrmi。JConsole为建立连接,需要在环境变量中设置mx.remote.credentials来指定用户名和密码从而进行授权。
  • Advanced
    • 使用一个特殊的URL连接JMX代理。一般情况使用自己定制的连接器而不是RMI提供的连接器来连接JMX代理,或者是一个使用JDK1.4的实现了JMX和JMX Rmote的应用。

image.png

Visual VM

  • Visual VM是一个功能强大的多合一故障诊断和性能监控的可视化工具。
  • 它集成了多个DK命令行工具,使用visual VM可用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替JConsole。
  • 在DK 6 Update 7以后,Visual VM便作为JDK的一部分发布 (VisualVM 在JDK/bin目录下),即:它完全免费。
  • 此外,Visual VM也可以作为独立的软件安装

插件的安装
Visual VM的一大特点是支持插件扩展,并且插件安装非常方便。我们既可以通过离线下载插件文件*.nbm,然后在Plugin对话框的已下载页面下,添加已下载的插件。也可以在可用插件页面下,在线安装插件。(这里建议安装上:VisualGC)
插件地址

  • 本地连接
    • 监控本地Java进程的CPU、类、线程等
  • 远程连接
  1. 确定远程服务器的ip地址
  2. 添加JMX(通过JMX技术具体监控远端服务器哪个Java进程
  3. 修改bin/catalina.sh文件,连接远程的tomcat
  4. 在.../conf中添加jmxremote.access和jmxremote.password文件
  5. 将服务器地址改为公网ip地址
  6. 设置阿里云安全策略和防火墙策略
  7. 启动tomcat,查看tomcat启动日志和端口监听8-JMX中输入端口号、用户名、密码登录
  • 主要功能
  1. 生成/读取堆内存快照

image.png

image.png

image.png

  1. 查看JVM参数和系统属性

image.png

  1. 查看运行中的虚拟机进程
  2. 生成/读取线程快照

image.png

image.png

  1. 程序资源的实时监控

image.png

image.png

  1. 其他功能
    • JMX代理连接
    • 远程环境监控
    • CPU分析和内存分析

MAT

MAT(Memory Analyzer Tool)工具是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。

MAT是基于Eclipse开发的,不仅可以单独使用,还可以作为插件的形式嵌入在Eclipse中使用。是一款免费的性能分析工具,使用起来非常方便。下载MAT

hprof文件信息:

  • 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
  • 所有的类信息,包括classloader、类名称、父类、静态变量等. GCRoot到所有的这些对象的引用路径
  • 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)

缺点:

MAT不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如Sun, HP,SAP所采用的 HPROF 二进制堆存储文件,以及IBM 的 PHD堆存储文件等都能被很好的解析。

说明:

最吸引人的还是能够快速为开发人员生成内存泄漏报表,方便定位问题和分析问题。虽然MAT有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从MAT展现给我们的信息当中通过经验和直觉来判断才能发现。

主要常用功能:

  1. histogram

展示了各个类的实例数目以及这些实例heap或Retainedheap的总和

image.png

image.png

image.png

image.png

  1. thread overview

查看系统中的Java线程,查看局部变量的信息

image.png

  1. 获得对象相互引用的关系

image.png

  • with outgoing references:当前对象作为根的引用其他对象的树图
  • with incoming references:当前对象被哪些对象引用的树图
  1. 浅堆与深堆

浅堆(Shallow Heap)

是指一个对象所消耗的内存。在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。根据堆快照格式不同,对象的大小可能会向8字节进行对齐。

以String为例: 2个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占24字节。(jdk7中)

image.png

这24字节为String对象的浅堆大小。它与String的value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。

计算对象大小

保留集(Retained Set):

对象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的深堆范围内。

image.png
案例分析: StudentTrace

  1. 支配树

支配树(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。

image.png

同理,对象E支配对象G。到达对象H的可以通过对象D,也可以通过对象E,因此对象D和E都不能支配对象H,而经过对象C既可以到达D也可以到达E,因此对象C为对象H的直接支配者。

在MAT中,单击工具栏上的对象支配树按钮,可以打开对象支配树视图。

image.png

JProfiler

在运行Java的时候有时候想测试运行时占用内存情况,这时候就需要使用测试工具查看了。在
eclipse里面有Eclipse Memory Analyzer too1(MAT)插件可以测试,而在IDEA中也有这么一个插件,就是JProfiler。

JProfiler是由 ej-technologies公司开发的一款 Java 应用性能诊断工具。功能强大,但是收费。

官网下载地址

idea 安装VisualVM,Jprofiler,jclasslib

JProfier数据采集方式分为两种:Sampling(样本采集)和Instrumentation(重构模式)

  • Instrumentation:这是JProfiler全功能模式。在class加载之前,JProfier把相关功能代码写入到需要分析的class的bytecode中,对正在运行的jvm有一定影响。
    • 优点:功能强大。在此设置中,调用堆栈信息是准确的。
    • 缺点:若要分析的class较多,则对应用的性能影响较大,CPU开销可能很高(取决于Filter的制)。因此使用此模式一般配合Filter使用,只对特定的类或包进行分析。
  • Sampling:类似于样本统计,每隔一定时间(5ms)将每个线程栈中方法栈中的信息统计出来。
    • 优点:对CPU的开销非常低,对应用影响小(即使你不配置任何Filter)
    • 缺点:一些数据/特性不能提供(例如:方法的调用次数、执行时间)

注: JProfiler本身没有指出数据的采集类型,这里的采集类型是针对方法调用的采集类型。因为JProfiler的绝大多数核心功能都依赖方法调用采集的数据,所以可以直接认为是JProfiler的数据采集类型。

/**
 * @Description: 内存泄漏
 * @Author: jianweil
 * @date: 2021/9/9 9:04
 */
public class MemoryLeak {
    public static void main(String[] args) {

        while (true) {
            ArrayList beanList = new ArrayList();//每次循环会被回收
            for (int i = 0; i < 500; i++) {
                Bean data = new Bean();
                data.list.add(new byte[1024 * 10]);//10kb,不会回收
                beanList.add(data);
            }
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }
}

class Bean {
    int size = 10;
    String info = "hello";
     //ArrayList list = new ArrayList();
    static ArrayList list = new ArrayList();
}
复制代码
  1. OOM例子查看

image.png

image.png

image.png

image.png

image.png

image.png

image.png

  1. 死锁例子查看
/**
 * @Description: 死锁 jstack
 * @Author: jianweil
 * @date: 2021/9/6 16:07
 */
public class DealLockTest {

    public static void main(String[] args) {

            Object o1 = new Object();
            Object o2 = new Object();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (o1) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("线程1开始运行");
                        synchronized (o2) {
                            System.out.println("线程2获取到o1");
                        }

                    }
                }
            }, "线程1").start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (o2) {
                        System.out.println("线程2开始运行");
                        synchronized (o1) {
                            System.out.println("线程2获取到o1");
                        }

                    }
                }
            }, "线程2").start();

        System.out.println("main线程结束");
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                Map<Thread, StackTraceElement[]> all = Thread.getAllStackTraces();//追踪当前进程中的所有的线程
                Set<Map.Entry<Thread, StackTraceElement[]>> entries = all.entrySet();
                for (Map.Entry<Thread, StackTraceElement[]> en : entries) {
                    Thread t = en.getKey();
                    StackTraceElement[] v = en.getValue();
                    System.out.println("【Thread name is :" + t.getName() + "】");
                    for (StackTraceElement s : v) {
                        System.out.println("\t" + s.toString());
                    }
                }
            }
        }, "线程3").start();


        }
    }
复制代码

image.png

image.png

image.png

Arthas

Arthas(阿尔萨斯)是Alibaba开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪Java代码;实时监控VM状态。

官方文档

Arthas支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的Tab自动补全功能,进一步方便进行问题的定位和诊断。

当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

  • 这个类从哪个jar包加载的?为什么会报各种类相关的 Exception?
  • 我改的代码为什么没有执行到?难道是我没commit?分支搞错了?
  • 遇到问题无法在线上debug,难道只能通过加日志再重新发布吗?
  • 线上遇到某个用户的数据处理有问题,但线上同样无法debug,线下无法重现!
  • 是否有一个全局视角来查看系统的运行状况?
  • 有什么办法可以监控到JVM的实时运行状态?

jmc

在Oracle 收购Sun之前,Oracle 的JRockit虚拟机提供了一款叫做JRockitMission Control 的虚拟机诊断工具。

在Oracle收购Sun之后,Oracle公司同时拥有了Sun Hotspot和JRockit两款虚拟机。根据Oracle对于Java的战略,在今后的发展中,会将JRockit的优秀特性移植到Hotspot上。其中,一个重要的改进就是在Sun的JDK中加入了JRockit的支持。

在oracle JDK 7u40之后,Mission Control这款工具已经绑定在Oracle JDK中发布。自Java 11 开始,本节介绍的JFR已经开源。但在之前的Java版本,JFR属于Commercial Feature,需要通过Java 虚拟机参数-XX:+UnlockCommercialFeatures开启

如果你有兴趣请可以查看openJDK的Mission Control项目

Java Mission Control(简称JMC) ,Java官方提供的性能强劲的工具。是一个用于对Java 应用程序进行管理、监视、概要分析和故障排除的工具套件。

它包含一个GUI客户端,以及众多用来收集 Java 虚拟机性能数据的插件,如JMX
Console(能够访问用来存放虚拟机各个子系统运行数据的MXBeans),以及虚拟机内置的高效profiling 工具 Java Flight Recorder (JFR)。

JMC 的另一个优点就是:采用取样,而不是传统的代码植入技术,对应用性能的影响非常非常小,完全可以开着JMC来做压测(唯一影响可能是 full gc多)。

image.png

Java Flight Recorder 取样分析,要采用取样,必须先添加参数:

  • -XX:+UnlockCommercialFeatures
  • -XX:+FlightRecorder

否则

image.png

启动方式

  1. 方式一:-XX:StartFlightRecording=参数

第一种是在运行目标Java程序时添加-XX:StartFlightRecording=参数。

比如:下面命令中,JFR将会在Java 虚拟机启动5s后(对应delay=5s)收集数据,持续20s(对应duration=20s)。当收集完毕后,JFR 会将收集得到的数据保存至指定的文件中(对应filename=myrecording.jfr)

java - XX:StartFlightRecording=delay=5s,duration=20s,filename=myrecording.jfr,settings=profile MyApp

由于JFR将持续收集数据,如果不加以限制,那么JFR可能会填满硬盘的所有空间。因此,我们有必要对这种模式下所收集的数据进行限制。

比如:

java -XX:StartFlightRecording=maxage=10m, maxsize=100m, name=SomeLabel MyApp

  1. 方式2:使用jcmd的JFR.*子命令

通过jcmd来让JFR开始收集数据、停止收集数据,或者保存所收集的数据,对应的子命令分别为FR.start,JFR.stop,以及JFR.dump。

jcmd <PID> JFR.start settings=profile maxage=10m maxsize=150m name=SomeLabel

上述命令运行过后,目标进程中的JFR已经开始收集数据。此时,我们可以通过下述命令来导出已经收集到的数据:

jcmd <PID> JFR.dump name=SomeLabel filename=myrecording.jfr

最后,我们可以通过下述命令关闭目标进程中的JFR:

jcmd <PID> JFR.stop name=SomeLabel

  1. 图形化界面直接打开

image.png

image.png

  • 加上对象数量的统计: Java Virtual Machine -> GC -> Detailed ->Object Count/Object Count after GC
  • 方法调用采样的间隔从10ms改为1ms(但不能低于1ms,否则会影响性能了): JavaVirtual Machine -> Profiling -> Method Profiling Sample/Method Sampling Information
  • Socket 与 File采样,10ms 太久,但即使改为1ms也未必能抓住什么,可以干脆取消掉: Java Application->File Read/Filewrite/Socket Read/Socket write

image.png

OQL语言

image.png

MAT支持一种类似于SQL的查询语言0QL(object Query Language)。OQL使用类SQL语法,可以在堆中进行对象的查找和筛选。

  1. 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.atguigu.mat.Student
  • “DISTINCT”关键字用于在结果集中去除重复对象。
    • SELECT DISTINCT OBJECTS classof(s) FROM java.lang.String s
  1. FROM子句
  • From子句用于指定查询范围,它可以指定类名、正则表达式或者对象地址。
    • SELECT * FROM java.lang.String s
  • 下例使用正则表达式,限定搜索范围,输出所有com.xxx包下所有类的实例
    • SELECT * FROM "com.xxx..*”
  • 也可以直接使用类的地址进行搜索。使用类的地址的好处是可以区分被不同ClassLoader加载的同一种类型。
    • select * from 0x37a0b4d
  1. 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 AN D v.@retainedHeapSize>1000
  1. 内置对象与方法

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

内存泄漏

image.png
可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题〈让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)。

  • 严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
  • 但实际情况,很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。

image.png

对象×引用对象Y,X的生命周期比Y的生命周期长;
那么当Y生命周期结束的时候,x依然引用着Y,这时候,垃圾回收期是不会回收对象Y的;
如果对象X还引用着生命周期比较短的A、B、C,对象A又引用着对象a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。

内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出

Java中内存泄漏的8种情况

  1. 静态集合类

静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

/**
 * @Description: 静态集合类
 * @Author: jianweil
 * @date: 2021/9/7 20:18
 */
public class LeakDemo {

    static List list = new ArrayList();

    public void oomTests() {
        Object obj = new Object();//局部变量
        list.add(obj);
    }

}
复制代码
  1. 单例模式

单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

  1. 内部类持有外部类

内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。
这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

  1. 各种连接,如数据库连接、网络连接和IO连接等

在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。
否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会成大量的对象无法被回收,从而引起内存泄漏。

public static void main( String[] args) {
    try {
        Connection conn = null;
        class.forName("com.mysql.jdbc.Driver") ;
        conn = DriverManager.getconnection("uri", "","");
        statement stmt = conn.createstatement();
        Resultset rs = stmt.executeQuery("....");
      } catch (Exception e) {//异常日志
 
     }finally {
        //1.关闭结果集Statement
        //2.关闭声明的对象Resultset1
        //3.关闭连接Connection
     }
}
复制代码
  1. 变量不合理的作用域

变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。

/**
 * @Description: 静态集合类
 * @Author: jianweil
 * @date: 2021/9/7 20:18
 */
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的内存空间。

  1. 改变哈希值

改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。

否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。

这也是String为什么被设置成了不可变类型,我们可以放心地把String存入 HashSet,或者把String当做HashMap的key值;

当我们想把自己定义的类保存到散列表的时候,需要保证对象的hashCode不可变。

/**
 * @Description:改变哈希值
 * @Author: jianweil
 * @date: 2021/9/7 20:18
 */
public class LeakDemo {

    public static void main(String[] args) {
        HashSet<Point> hs = new HashSet<>();
        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:由于字段修改后,调用remove方法查找时的hashcode已经变为51.而原来是41,找不到了
        hs.add(cc);
        System.out.println("hs.size= " + hs.size());//size = 2
        System.out.println(hs);//size = 2
    }

    static 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}]

复制代码
  1. 缓存泄漏

内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。

对于这个问题,可以使用WeakHashMap代表缓存,此种Map的特点是,当除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值。

/**
 * @Description: WeakHashMap使用对比
 * @Author: jianweil
 * @date: 2021/9/7 20:50
 */
public class MapTest {


    static Map wMap = new WeakHashMap();
    static Map map = new HashMap();


    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, "cache0bject3");
        map.put(ref4, "cacheobject4");
        System.out.println("string引用ref1 ,ref2, ref3 ,ref4消失");

    }


    private static void testweakHashMap() {
        System.out.println("iHeakHashMap cc之前");
        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 G之后");
        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) {
            e.printStackTrace();
        }
        System.out.println("HashMap Gc之后");
        for (Object o : map.entrySet()) {
            System.out.println(o);
        }
    }


    public static void main(String[] args) {
        init();
        testweakHashMap();
        testHashMap();
    }


}
复制代码
string引用ref1 ,ref2, ref3 ,ref4消失
iHeakHashMap cc之前
obejct2=cacheobject2
obejct1=cacheobject1
weakHashMap G之后
HashMap Gc之前
obejct4=cacheobject4
obejct3=cache0bject3
HashMap Gc之后
obejct4=cacheobject4
obejct3=cache0bject3
复制代码

image.png

  1. 监听器和回调

如果客户端在你实现的API中注册回调,却没有显示的取消,那么就会积聚。

需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为weakHashMap中的键。

案例剖析

/**
 * @Description: todo
 * @Author: jianweil
 * @date: 2021/9/7 21:01
 */
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;
    }

 /*   *//**
     * 出栈:存在泄漏问题
     *
     * @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;
    }

    /**
     * 扩容
     */
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }

}
复制代码

上述程序并没有明显的错误,但是这段程序有一个内存泄漏,随着GC活动的增加,或者内存占用的不断增加,程序性能的降低就会表现出来,严重时可导致内存泄漏,但是这种失败情况相对较少。

代码的主要问题在pop函数,假设这个栈一直增长,增长后如下图所示

  public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }
复制代码

image.png

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

image.png

从上图中看以看出,如果栈先增长,再收缩,那么从栈中弹出的对象将不会被当作垃圾回收,即使程序不再使用栈中的这些队象,他们也不会回收,因为栈中仍然保存这对象的引用,俗称过期引用,这个内存泄露很隐蔽。

解决办法:

一旦引用过期,清空这些引用,将引用置空。

   public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }
复制代码

image.png

获取dump文件方式

  1. 通过前一章介绍的jmap工具生成,可以生成任意一个java进程的dump文件;
    • jmap -dump:live,format=b,file=<filename.hprof> <pid> (生产推荐导出活的对象)
  2. 通过配置JVM参数生成。
  • 选项"-XX:+HeapDumpOnOutOfMemoryError”或"-XX:+HeapDumpBeforeFullGC"
  • 选项"-XX:HeapDumpPath"所代表的含义就是当程序出现OutOfMemory时,将会在相应的目录下生成一份dump文件。如果不指定选项“XX:HeapDumpPath”则在当前目录下生成dump文件。
    • 对比: 考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用jmap+MAT工具是最常见的组合。
    • 如:-Xmx100m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\m.hprof
  1. 使用VisualVM可以导出堆dump文件

image.png

  1. 使用MAT既可以打开一个己有的堆快照,也可以通过MAT直接从活动Java程序中导出堆快照。该功能将借助jps列出当前正在运行的 Java进程,以供选择并获取快照。

image.png
5. 使用JProfiler导出dump文件

image.png

深入理解JVM系列