二十一、JVM可视化监控工具

一、概述

1、命令行工具的缺陷

1、使用命令行工具或组合能获取目标Java应用性能相关的基础信息,但存在以下缺陷:
  • 无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等。
  • 需要登录到目标Java应用所在的服务器上,使用不方便。
  • 分析数据通过终端输出,结果展示不直观。

2、图形化诊断工具

1、JDK自带的工具
  • jconsole:JDK自带的可视化监视工具,查看Java应用程序的运行概况、监控堆信息、永久区(元空间)使用情况、类加载情况等。在JDK的bin目录下的jconsole.exe。
  • VisualVM:提供了一个可视化界面,用于查看Java虚拟机上运行的基于Java技术的应用程序的详细信息,在JDK的bin目录下的jvisualvm.exe。
  • JMC:Java Mission Control,内置Java Flight Recorder,能够以极低的性能开销收集Java虚拟机性能数据
2、第三方工具
  • MAT:Memory Analyzer Tool是基于Eclipse的内存分析工具,是一个快速、功能丰富的Java Heap分析工具,可以帮助查找内存泄漏和减少内存消耗,Eclipse的插件形式。
  • JProfiler:商业软件,需要付费,功能强大,与VisualVM类似。
  • Arthas:阿里巴巴开源的Java诊断工具。
  • Btrace:Java运行时追踪工具,可以在不停机的情况下,跟踪指定的方法调用、构造函数调用和系统内存等情况。

二、JConsole:Java监视与管理控制台

1、概述

1、JConsole(Java Monitoring and Management Console)是一款基于JMX(Java Manage-ment Extensions)的可视化监视、管理工具。它的主要功能是通过JMX的MBean(Managed Bean)对系统进行信息收集和参数动态调整。JMX是一种开放性的技术,不仅可以用在虚拟机本身的管理上,还可以 运行于虚拟机之上的软件中,典型的如中间件大多也基于JMX来实现管理与监控。虚拟机对JMX MBean的访问也是完全开放的,可以使用代码调用API、支持JMX协议的管理控制台,或者其他符合JMX规范的软件进行访问。
2、从Java5开始,在JDK中自带的Java监控与管理控制台,用于对JVM中内存、线程和类等的监控。

2、启动JConsole方式

1、命令行方式启动:直接输入jconsole
2、在JDK安装bin目录找到jconsole.exe双击

在这里插入图片描述

3、连接方式

1、连接本地进程:使用JConsole连接一个正在本地系统运行的JVM,并且执行程序的和运行JConsole的需要是同一个用户。

在这里插入图片描述

在这里插入图片描述

2、远程进程连接:使用下面的URL通过RMI连接器连接到一个JMX代理,service:jmx:rmi:///jndi/rmi://hostName:port/jmxrmi。JConsole为建立连接,需要在环境变量中设置mx.remote.credentials来指定用户名和密码,从而进行授权

4、内存监控

1、内存Tab页面:作用相当于可视化的jstat命令,用于监视被收集器管理的虚拟机内存(被收集器直接管理的Java堆和被间接管理的方法区)的变化趋势。
/**
 * @Date: 2022/1/23
 * JVM启动参数:-Xms600m -Xmx600m -XX:SurvivorRatio=8
 * 代码作用:以100kb/10ms的速度向Java堆中填充数据
 */
public class HeapInstanceTest {
    //内存占位符对象,一个HeapInstanceTest大约占100KB
    byte[] bytes = new byte[1024 * 100];

    public static void main(String[] args) {
        ArrayList<HeapInstanceTest> list = new ArrayList<>();
        while (true) {
            list.add(new HeapInstanceTest());
            try {
                //稍作延时,令监视曲线的变化更加明显
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
1、运行程序之后,在内存Tab页面可以看到Eden区的运行趋势呈折线状。堆空间内存设置600m,而默认的-XX:NewRatio=2,所以新生代占200m内存空间

在这里插入图片描述

5、线程监控

1、线程Tab页面:作用相当于可视化的jstack命令,遇到线程停顿的时候可以使用此页面的功能进行分析。
2、线程长时间停顿的原因:
  • 等待外部资源(数据库连接、网络资源、设备资源等)
  • 死循环、锁等待等
/**
 * @Date: 2022/1/23
 * 线程死循环示例
 */
public class ThreadLoopTest {
    public static void main(String[] args) {
        new Thread(() -> {
            while (true);
        }, "ThreadLoop").start();
    }
}
1、程序运行后,查看监控的ThreadLoop线程,ThreadLoop线程一直在执行空循环,从堆栈追踪中看到一直在ThreadLoopTest.java代码的11行停留,11行的代码为while(true)。

在这里插入图片描述

/**
 * @Date: 2022/1/23
 * 线程锁等待示例
 */
public class ThreadLockWaitTest {
    public static void main(String[] args) {
        Object obj = new Object();
        new Thread(() -> {
            synchronized (obj) {
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "ThreadLockWait").start();
    }
}
1、运行程序之后,查看监控的ThreadLockWait线程,显示ThreadLockWait线程在等待obj对象的notify()或notifyAll()方法的出现,线程这时候处于WAITING状态,在重新唤醒前不会被分配执行时间。线程处于正常的活锁等待中

在这里插入图片描述

/**
 * @Date: 2022/1/23
 * 线程死锁示例
 * 死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的
 * 一种阻塞的现象,若无外力作用,它们都将无法推进下去。(相互不释放资源从而相互等待)
 */
public class ThreadDeadLock {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        new Thread(()->{
            synchronized (A) {
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock A");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (B) {
                    System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock B");
                    System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 操作...");
                }
            }
        },"ThreadDeadLock1").start();

        new Thread(()->{
            synchronized (B) {
                System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock B");
                try {
                    sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (A) {
                    System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - Lock A");
                    System.out.println(new Date() + "【"+ Thread.currentThread().getName() + "】 - 操作...");
                }
            }
        },"ThreadDeadLock2").start();
    }
}
1、运行程序之后,点击检测死锁,会出现一个新的死锁Tab页面

在这里插入图片描述

2、分别查看死锁页的线程情况,线程ThreadDeadLock2在等待线程ThreadDeadLock1持有的Object对象,线程ThreadDeadLock1在等待线程ThreadDeadLock2持有的Object对象,这样两个线程就互相卡住,除非牺牲其中一个,否则死锁无法释放。

在这里插入图片描述

三、VisualVM:多合-故障处理工具

1、概述

1、VisualVM是功能最强大的运行监视和故障处理工具之一。
3、位于JDK安装目录的bin下的jvisualvm.exe

2、插件安装及作用

1、VisualVM基于NetBeans平台开发工具,因此它具备了通过插件扩展功能的能力,有了插件的扩展支持,它的作用更广泛
  • 显示虚拟机进程及进程的配置、环境信息(类似jps、jinfo命令)。
  • 监视应用程序的CPU、GC、堆、方法区及线程的信息(类似jstat、jstack命令)。
  • dump以及分析堆转储快照(类似jmap、jhat命令)。
  • 方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法。
  • 离线程序快照:收集程序的运行时配置、线程dump、内存dump等信息建立一个快照。
2、插件安装方式
  • 手工安装:插件官网下载nbm包后,点击“工具 -> 已下载 -> 添加插件”,然后选择下载好的nbm包即可完成安装。独立安装的插件存储在VisualVM的根目录。

在这里插入图片描述

  • 连网安装:点击“工具 -> 可用插件”可以看到插件列表,勾选所需的插件,点击安装即可。

在这里插入图片描述

3、IDEA安装VisualVM,在Plugins中搜索VisualVM,点击Install即可完成安装,再完成配置即可使用

在这里插入图片描述

在这里插入图片描述

3、连接方式

1、本地连接:监控本地Java进程的CPU、类、线程等。当启动一个程序后,进入VisualVM即可连接

在这里插入图片描述

2、远程连接步骤如下:
  • 确定远程服务器的IP地址
  • 添加JMX(通过JMX技术具体监控远程服务器哪个Java进程)
  • 修改bin/catalina.sh文件,连接远程Tomcat
  • 在conf中添加jmxremote.access和jmxremote.password文件
  • 将服务器地址改为公网IP地址
  • 设置阿里云安全策略和防火墙策略
  • 启动Tomcat,查看Tomcat启动日志和端口监听
  • JMX中输入端口号、用户名、密码登录

4、生成与查看堆转储快照(Dump文件)

1、在应用程序窗口中右键单击应用程序节点,然后选择堆Dump

在这里插入图片描述

2、在应用程序窗口中双击应用程序节点,然后在监控Tab页面单击堆Dump

在这里插入图片描述

3、生成堆转储快照文件之后,应用程序页签会在该堆的应用程序下增加一个以[heap-dump]开头的子节点,双击即可打开并且在主页签中显示详细信息,如果需要把堆转储快照保存或发送出去,就应在heapdump节点上右键选择“另存为”菜单,否则当VisualVM关闭时,生成的堆转储快照文件会被当作临时文件自动清理掉。
  • 堆页签中的“概要”面板可以看到应用程序dump时的运行时参数、System.getProperties()的内容、 线程堆栈等信息;“类”面板则是以类为统计口径统计类的实例数量、容量信息;“实例”面板不能直接使用,因为VisualVM在此时还无法确定用户想查看哪个类的实例,所以需要通过“类”面板进入, 在“类”中选择一个需要查看的类,然后双击即可在“实例”里面看到此类的其中500个实例的具体属性信息;“OQL控制台”面板则是运行OQL查询语句的,同jhat中的OQL功能一样。

在这里插入图片描述

4、要打开一个由已经存在的堆转储快照文件,通过文件菜单中的“装入”功能, 选择硬盘上的文件即可。

5、生成与查看线程快照

1、在应用程序窗口中右键单击应用程序节点,然后选择线程Dump

在这里插入图片描述

2、在应用程序窗口中双击应用程序节点,然后在线程Tab页面单击线程Dump

在这里插入图片描述

3、说明:生成与查看线程快照的方式与堆转储快照的操作一样

6、分析程序性能

1、在Profiler或抽样器页签中,VisualVM提供了程序运行期间方法级的处理器执行时间分析以及内存分析。
2、先选择“CPU”和“内存”按钮中的一个,然后切换到应用程序中对程序进行操作,VisualVM会记录这段时间中应用程序执行过的所有方法。
  • 如果是进行处理器执行时间分析,将会统计每个方法的执行次数、执行耗时;
  • 如果是内存分析,则会统计每个方法关联的对象数以及这些对 象所占的空间。
  • 等要分析的操作执行结束后,点击“停止”按钮结束监控过程

在这里插入图片描述

在这里插入图片描述

7、BTrace动态日志跟踪

1、BTrace是一个VisualVM插件,它本身也是一个可运行的独立程序。作用是在不中断目标程序运行的前提下,通过HotSpot虚拟机的Instrument功能(是JVMTI中的主要组成部分,HotSpot虚拟机允许在不停止运行的情况下,更新已经加载的类的代码)动态加入原本并不存在的调试代码。
2、对实际生产中的程序很有意义:如当程序出现问题时,排查错误的一些必要信息时 (如方法参数、返回值等),在开发时并没有打印到日志之中以至于不得不停掉服务时,都可以通过调试增量来加入日志代码以解决问题。
3、BTrace的用途很广泛,打印调用堆栈、参数、返回值只是它最基础的使用形式,在它的网站上有使用BTrace进行性能监视、定位连接泄漏、内存泄漏、解决多线程竞争问题等的使用案例,可以去网上了解相关信息。官方网站
4、进入VisualVM界面后选择工具->插件->搜索BTrace进行安装即可

在这里插入图片描述

/**
 * @Date: 2022/2/8
 * 测试BTrace的功能
 */
public class BtraceTest {
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        for (int i = 0; i < 10; i++) {
            reader.readLine();
            int a = (int) Math.round(Math.random() * 1000);
            int b = (int) Math.round(Math.random() * 1000);
            System.out.println(add(a, b));
        }
    }

    /**
     * 计算两数之和
     * @param a
     * @param b
     * @return
     */
    public static int add(int a, int b) {
        return a + b;
    }
}
1、运行程序之后,打开VisualVM进行监控,在应用程序中右击要调试的程序,点击“Trace Application…”进入到Btrace面板。看起来很像一个Java程序开发环境,里面有一小段Java代码。

在这里插入图片描述

2、假设想要知道每次的随机数是多少,此时就可以填充TracingScript的内容了,就可以在不中断程序运行的情况下看到参数的值。

在这里插入图片描述

3、由于reader.readLine()方法阻塞住,因此在应用程序中输入任意数字并按回车键,即可看到调试信息。

在这里插入图片描述

四、Eclipse MAT

1、概述

1、MAT是Memory Analyzer Tool的简称,它是一款功能强大的Java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况
2、MAT是基于Eclipse开发的,不仅可以单独使用(解压使用即可),还可以作为插件的形式嵌入在Eclipse中使用,是一款免费的性能分析工具。
3、MAT可以分析heap dump文件,在进行内存分析时,只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前内存信息。
4、只需要配置好JDK相关的环境变量,MAT可正常启动,下载并使用MAT

在这里插入图片描述

2、获取堆Dump文件

1、通过jmap命令行工具可以生成任意Java进程的dump文件(手动方式)
2、通过设置JVM启动参数生成(自动方式)
  • -XX:+HeapDumpOnOutOfMemoryError:在程序发生OOM时,导出应用程序的当前堆快照
  • -XX:+HeapDumpBeforeFullGC:在程序发生FullGC之前,导出应用程序的当前堆快照
  • -XX:+HeapDumpAfterFullGC:在程序发生FullGC之后,导出应用程序的当前堆快照
  • -XX:HeapDumpPath=<filename.hprof>:指定dump文件的生成的位置(这个目录路径必须是已经创建好的,它不会自动创建目录路径),如果不指定该参数,则在当前目录下生成dump文件。
3、使用VisualVM导出堆dump文件
4、使用MAT既可以打开一个已有的dump文件,也可以通过MAT直接从活动Java程序中导出dump文件
  • 该功能将借助jps列出当前正在运行的Java进程,然后选择并保存dump文件。

在这里插入图片描述

5、使用下面一段代码,生成一个dump文件,方便后续操作
  • 设置JVM启动参数
  • 通过jps -l命令获取到进程id
  • 再通过jmap -dump:format=b,file=d:\mat_dump\a.hprof jps查出的pid生成dump文件
/**
 * @Date: 2022/2/9
 * JVM启动参数:-Xms600m -Xmx600m -XX:SurvivorRatio=8
 * 代码作用:以100kb/10ms的速度向Java堆中填充数据
 */
public class OOMTest {

    public static void main(String[] args) {
        ArrayList<Picture> list = new ArrayList<>();
        while (true) {
            try {
                //稍作延时,令监视曲线的变化更加明显
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(new Picture(100 * 1024));
        }
    }
}

class Picture {
    //内存占位符对象
    private byte[] pixels;

    public Picture(int length) {
        //创建length大小的字节数组
        this.pixels = new byte[length];
    }
}

3、使用MAT及一些说明

1、打开MAT点击File,再点击Open即可打开一个dump文件
  • Leak Suspects Report:自动检查堆转储中是否有泄漏嫌疑。报告中有哪些对象还始终存活,以及它们为什么没有被垃圾收收集器所收集。
  • Component Report:分析一组对象是否存在可疑的内存问题:重复字符串、空集合、终结器、弱引用等。
  • Re-open previously run report:从新打开一个之前已经运行过的报告

在这里插入图片描述

2、进入MAT分析界面

在这里插入图片描述

3、点击Reports->Leak Suspects链接来生成报告,查看导致内存泄漏的罪魁祸首

在这里插入图片描述

4、直方图(histogram)

1、展示了各个类的实例数目以及这些实例的浅堆(Shallow heap)或者深堆(Retained heap)的总和

在这里插入图片描述

在这里插入图片描述

2、假设已经找到要分析的类,对类进行分析时常常分析GCRoots,进行定位内存泄漏问题

在这里插入图片描述

在这里插入图片描述

5、线程分析(thread overview)

1、查看系统中的Java线程以及局部变量的信息

在这里插入图片描述

2、查看局部变量引用了那些以及查看局部变量被那些引用了(通过此操作可以找到引用链,获取到对象的互相引用关系),选中局部变量右键->List objects

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

6、深堆与浅堆

1、浅堆(Shallow Heap)是指一个Java对象结构所占用内存的大小,这个Java对象内部包含了三部分数据:对象头、实例数据和对齐填充。对于这些理解可以参考探秘HotSpot虚拟机对象与直接内存篇中【对象的内存布局】
2、深堆(Retained Heap)是指对象的保留集中所有的对象的浅堆大小之和。
3、保留集(Retained Set)是指当某个对象(A)被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或者间接访问到的所有对象的集合。
4、特别说明:浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接或者间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。当前深堆大小 = 当前对象的浅堆大小 + 对象中所包含对象的深堆大小
5、对象实际大小:定义为一个对象所能触及的所有对象的浅堆大小之和。和垃圾回收无关。如下:对象A引用了C和D,对象B引用了C和E,那么对象A的浅堆大小只是A本身,不含C和D,而A的实际大小为A、C、D三者之和。而A的深堆大小为A、D之和,由于对象C还可以通过对象B访问到,因此不在对象A的深堆范围内。

在这里插入图片描述

7、支配树

1、MAT提供了一个称为支配树(Diminator Tree)的对象图。支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。支配树是基于对象间的引用图所建立的,有以下性质:
  • 对象A的子树(所有被对象A支配的对象集合)表示对象A的保留集(Retained Set),即深堆。
  • 如果对象A支配对象B,那么对象A的直接支配者也支配对象B。
  • 支配树的边与对象引用图的边不直接对应。
2、如下图所示:左图表示对象引用图,右图表示左图所对应的支配树。对象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的直接支配者。

在这里插入图片描述

3、MAT工具中查看支配树:点击工具栏中的对象支配树按钮,即可打开对象支配树视图。

在这里插入图片描述

五、内存泄漏与内存溢出补充

1、概述

1、在Java堆篇垃圾回收的一些概念篇中有对内存泄漏与内存溢出有一些说明,在此再作点补充。
2、内存泄漏(Memory Leak):

在这里插入图片描述

  • 可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用,那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(JVM会误以为此对象还在引用中,无法回收,造成内存泄漏)。
  • 左边的图最上面的对象不可达,就是需要被回收的对象,右边的图有一些对象不用了,按道理应该断开引用链,但是没有断开引用,从而JVM无法回收对象,就造成内存泄漏。
  • 通俗来说,程序在申请内存后,无法释放已申请的内存空间。假设一共有1024M的内存,分配了512M的内存一直没有被回收,那么可以用的内存就只有512M了,仿佛泄漏掉了一部分内存。
3、内存溢出(OutOfMemory):
  • 程序在申请内存时,没有足够的内存空间供其使用,并且垃圾收集器也无法提供更多内存。

2、内存泄漏分类

1、常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
2、偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
3、一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
4、隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,称这类内存泄漏为隐式内存泄漏。

3、内存泄漏的情况

1、静态集合类,如HashMap、LinkedList等,如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束之前不会被释放,从而造成内存泄漏。简单说,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期的对象持有它,从而导致不能被回收。
public class Memoryleak {
    static List list = new ArrayList();
    
    public void addList() {
        Object obj = new Object();
        list.add(obj);
    }
}
2、单例模式,和静态集合导致内存泄漏的原因类似,因为单例的静态特性,它的生命周期和JVM的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。
3、内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,那么这个外部类对象将不会被垃圾收集器回收,也会造成内存泄漏。
4、各种连接,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接关闭后,垃圾收集器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显式地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
public static void main(String []args) {
    try {
        Connection conn = null;
        Class.forName("com.mysql.jdbc.Driver");
        conn = DriverManager.getConnection("url", "root", "123456");
        Statement st = conn.createStatement();
        ResultSet res = st.executeQuery("select * from users");
    } catch (Exception e) {
        
    } finally {
        //1、关闭结果集Statement
        //2、关闭声明的对象ResultSet
        //3、关闭连接Connection
    }
}
5、变量不合理的作用域,一般来说,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏,另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
示例:通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveMsg方法把msg保存到数据库中,此时msg已经没有用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。实际上msg变量可以放到receiveMsg方法内部,当方法调用完,那么msg的生命周期也就结束,此时可以回收了。还可以在msg使用完后,把msg设置为null,这样垃圾收集器也能回收msg占用的内存了。
public class UsingRandom {
    private String mag;
    
    public void receiveMsg() {
        readFromNet();//从网络中接收数据并保存到msg中
        saveMsg();//保存msg到数据库中
    }
}
6、改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独移除当前对象,造成内存泄漏。
public class ChangeHashCode1 {
    public static void main(String[] args) {
        HashSet<Point> sets = new HashSet<>();
        Point p = new Point();
        p.setX(10);//对应hashCode = 41
        sets.add(p);
        p.setX(20);//对应hashCode = 51 此行为导致了内存的泄漏
        System.out.println("sets.remove = " + sets.remove(p));//false
        sets.add(p);
        System.out.println("sets size = " + sets.size());//size = 2
        System.out.println(sets);
    }
}

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 p = (Point) obj;
        if (x != p.x) return false;
        return true;
    }

    @Override
    public String toString() {
        return "Point{" +
                "x=" + x +
                '}';
    }
}
7、缓存泄漏,当一个对象引用放入到缓存中,就很容易遗忘。比如:在测试环境中会加载一个表的数据到缓存(内存)中,测试的数据非常小,但是到生产环境中之后,数据量很大,这时占用的内存也就很多,可能导致应用很慢很卡。对于这个问题,可以使用WeakHashMap代表缓存,它的特点是,当除了自身有对key的引用外,此key没有其他引用,那么此map会自动丢弃此值。
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);
        }
    }
}
说明:上面代码和图示主要演示WeakHashMap如何自动释放缓存对象,当init函数执行完后,局部变量字符串引用ref1、ref2、ref3、ref4都会消失,此时只有静态map中保存对字符串对象的引用,可以看到,调用GC之后,HashMap的没有被回收,WeakHashMap里面的缓存被回收了。

在这里插入图片描述

8、监听器和回调,如果客户端在实现的API中注册回调,却没有显式的取消,那么就会积聚。需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将它保存在WeakHashMap中的键。

4、内存泄漏示例

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];
    // }

    //不存在泄漏的出栈方法
    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);
    }
}
分析:
1、上述程序并没有明显的错误,但是这段程序有一个内存泄漏问题,随着GC活动的增加,或者内存占用不断增加,程序性能的降低就会表现出来,严重时可能导致内存泄漏,但是这种失败情况相对较少。代码问题主要出在pop函数中。
2、假设这个栈一直在增长,如图所示

在这里插入图片描述

3、当进行大量的pop操作时,由于只改变长度大小而引用未进行置空,垃圾回收器并不会回收的。

在这里插入图片描述

4、如果栈先增长,再收缩,那么从栈中弹出的对象将不会被当作垃圾回收掉,即使程序不再使用栈中的这些对象,也不会进行回收,因为栈中仍然保存这些对象的引用,很是隐秘的内存泄漏问题。
5、使用代码中不存在内存泄漏的出栈方法,出栈并将引用置为null,垃圾回收器就会自动进行回收没有引用的对象。

在这里插入图片描述

六、JProfiler

1、概述

1、在Eclipse中有MAT插件可以查看程序运行占用内存情况,而在IDEA中有JProfiler插件可以查看,官网地址
2、JProfiler是由ej-technologies公司开发的一款Java应用性能诊断工具,但是收费
3、特点:
  • 使用方便,对被分析的应用影响小(提供模板)
  • CPU、Thread、Memory分析功能强大
  • 支持对JDBC、NoSQL、Jsp、Servlet、Socket等进行分析
  • 支持离线、在线分析;支持监控本地或远程的JVM;跨平台拥有多种操作系统的版本
4、主要功能:
  • 方法调用:对方法调用的分析可以帮助了解应用程序正在做什么,并找到提高其性能的方法
  • 内存分配:通过分析堆上对象、引用链和垃圾收集能帮助修复内存泄漏问题,优化内存使用
  • 线程和锁:JProfiler提供多种针对线程和锁的分析视图帮助发现多线程问题
  • 高级子系统:许多性能问题都发生在更高的语义级别上。例如,对于JDBC调用,希望能够找出执行最慢的SQL语句。JProfiler支持对这些子系统进行集成分析
5、官方使用文档:JProfiler11的英文版说明JProfiler12的中文版说明

2、下载与安装

1、阿里网盘分享地址附带破解
2、IDEA集成JProfiler
  • 在线安装:在File中找到Settings,在Plugins中搜索JProfiler插件并安装,重启IDEA
  • 离线安装:官方插件下载地址再进行手动安装

在这里插入图片描述

在这里插入图片描述

3、将JProfiler配置到IDEA中

在这里插入图片描述

4、JProfiler基本使用之Starter Center说明

在这里插入图片描述

3、JProfiler数据采集方式

1、Instrumentation(重构模式):这是JProfiler全功能模式。在class加载之前,JProfiler把相关功能代码写入到需要分析的class的bytecode中,对正在运行的JVM有一定影响。
  • 优点:功能强大,在此设置中调用堆栈信息是准确的。
  • 缺点:若要分析class较多,则对应用的性能影响较大,CPU开销可能很高(取决于Filter的控制),因此该模式一般配合Filter使用,只对特定的类或包进行分析。
2、Sampling(样本采集):类似样本统计,每隔一定时间将每个线程栈中方法栈中的信息统计出来。
  • 优点:CPU开销低,对应用影响小(即使不配置任何Filter)
  • 缺点:一些数据/特性不能提供(如:方法的调用次数、执行时间)
3、注意:JProfiler本身没有指出数据的采集类型,这里的采集类型是针对方法调用的采集类型,因为JProfiler的绝大多数核心功能都依赖方法调用采集的数据,因此可以直接认为是JProfiler的数据采集类型,推荐使用Sampling方式

在这里插入图片描述

4、遥感监测(Telemetries)

在这里插入图片描述

5、内存视图(Live Memory)

1、内存剖析:class/class instance的相关信息。如:对象的个数、大小、对象创建的方法执行栈,对象创建的热点。
  • 所有对象(All Objects):显示所有加载的类的列表和堆上分配的实例数
  • 记录对象(Recorded Objects):查看特定时间段对象的分配,并记录分配的调用堆栈,判断内存泄漏的时候才用到此功能。
  • 分配访问树(Allocation Call Tree):显示一颗请求树或者方法、类、包或对已选择类有带注释的分配信息的J2EE组件。
  • 分配热点(Allocation Hot Spots):显示一个列表包括方法、类、包或分配已选类的J2EE组件,可以标注当前值并显示差异值,对于每个热点都可以显示它的跟踪记录树。
  • 类追踪器(Class Tracker):类跟踪视图可以包含任意数量的图表,显示选定的类和包的实例与时间。

在这里插入图片描述

2、分析:内存中的对象情况
  • 频繁创建的Java对象
  • 存在大的对象
  • 存在内存泄漏
3、注意:
  • All Objects后面的Size是浅堆大小
  • Record Objects在判断内存泄漏的时候使用,可以通过观察Telemetries中的Memory,如果里面出现垃圾回收之后的内存占用逐步提高,这就有可能出现内存泄漏问题,所以可以使用Record Objects查看,但是该分析默认不开启,毕竟占用CPU性能太多。

6、堆遍历(Heap Walker)

1、如果通过内存视图Live Memory已经分析出哪个类的对象不能进行垃圾回收,并且有可能导致内存溢出,若想进一步分析,可以在该对象上点击右键,选择Show Selection In Heap Walker

在这里插入图片描述

2、进入Heap Walker页面之后再进行溯源,查看对象引用

在这里插入图片描述

3、查看结果,并根据结果查看对应的图表

在这里插入图片描述

在这里插入图片描述

7、CPU视图(CPU views)

1、当JProfiler测量方法调用的执行时间和它们的调用堆栈时,称之为"CPU分析"。 这些数据以多种方式呈现。 默认是不记录CPU数据,必须打开CPU记录才能采集到。
  • Call Tree(调用树):跟踪所有的方法调用及其调用栈会消耗相当大的内存,短时间内就会耗尽所有内存。 另外,在一个繁忙的JVM中,很难直观获得方法调用的数量。通常情况下,这个数字是如此之大,以至于定位和跟随跟踪是不可能的。另一个方面,只有将收集到的数据进行汇总,许多性能问题才会变得清晰。 这样,就可以知道在某个时间段内,方法调用相对于整个活动的重要性。 如果是单一的跟踪,你对你所看的数据的相对重要性没有概念。
  • Hot Spots(热点):如果应用程序运行太慢,要想找到那些占用大部分时间的方法,通过调用树有时可以直接找到方法,但通常是行不通的,因为调用树可能很大而且有大量的叶节点。这种情况下,需要反转调用树:一个所有方法的列表,按其总的自身时间排序,从所有不同的调用堆栈中累计出来, 并通过回溯跟踪显示这些方法是如何被调用的。 在热点树中,叶节点是入口点, 就像应用程序的main 方法或线程的run 方法。 从热点树中最深的节点开始,调用向上传递到顶层节点。
  • Call Graph(调用图):显示一个从已选方法、类、包开始访问队列的图
  • Outlier Detection(异常值检测):显示了每个方法的调用持续时间和调用次数的信息,以及单次调用的最长时间。 最大调用时间与平均时间的偏差显示是否所有调用持续时间保持在一个小范围内,还是有显著的异常值。 计算异常值系数。
2、异常值检测说明:

在这里插入图片描述

3、调用树说明:
  • Thread selection(线程选择):默认情况下,会累积所有线程。 JProfiler以每个线程为基础维护CPU数据,可以显示单个线程或线程组。
  • Thread status(线程状态):每个线程都有一个相应的线程状态。如果线程已经准备好处理字节码指令,或者当前正在CPU处理器上执行这些指令, 则线程状态称为"就绪(Runnable)“。 在寻找性能瓶颈时,该线程状态是值得关注的,所以默认选择它。一个线程可能正在等待一个Monitor,例如通过调用Object.wait()Thread.sleep() , 在这种情况下,该线程状态称为"等待(Waiting)”。一个线程在试图获取Monitor时被阻塞, 例如在synchronized 代码块的边界处,则处于"阻塞(Blocking)"状态。最后,JProfiler增加了一个合成的"Net I/O"状态,用于跟踪线程等待网络数据的时间。 这对于分析服务器和数据库驱动程序很重要,因为该时间可能与性能分析有关,例如调查缓慢的SQL查询。
  • Aggregation level(聚合级别):虽然所有的测量都是针对方法进行的,但JProfiler允许你通过在类或包级别上聚合调用树来获得更广阔的视角。 聚合级别选择器还包含一个"Java EE组件"模式。如果你的应用程序使用Java EE,你可以使用该模式只查看类级别上的JEE组件。 像URL这样的拆分节点会在所有聚合级别中保留。
  • View mode(视图模式):显示视图的方式,默认是树的形式。

在这里插入图片描述

8、线程视图(Threads)

1、JProfiler通过对线程历史的监控判断其运行状态,并监控是否有线程阻塞产生,还能将一个线程所管理方法以树状形式呈现,对线程剖析。
  • Thread History(线程历史):显示一个与线程活动和线程状态在一起的时间表。
  • Thread Monitor(线程Monitor):显示一个列表包括所有的活动线程以及它们目前的活动状态。
  • Thread Dumps(线程转储):显示所有线程的堆栈追踪。
2、查看线程状态,启动线程死锁的测试类,打开JProfiler观察

在这里插入图片描述

3、查看线程Monitor

在这里插入图片描述

4、线程转储

在这里插入图片描述

9、监视器&锁(Monitors&locks)

1、分析所有线程持有锁的情况及锁的信息
  • Current Locking Graph(当前锁状态图):显示JVM中的当前死锁图表
  • Current Monitors(当前Monitor):显示当前使用的监测器并且包括它们的关联线程
  • Locking History Graph(锁状态历史图):显示记录在JVM中的锁定历史
  • Monitor History(Monitor历史):显示重大的等待事件和阻塞事件的历史记录
  • Monitor Usage Statistics(Monitor使用情况统计):显示分组监测、线程和监测类的统计监测数据
2、死锁检测:在Quick Attach模式下无法监测到的,可以new session一个,找到程序所在类或者jar包

在这里插入图片描述

在这里插入图片描述

3、锁状态历史图(分析锁状态情况):
  • 每个Java对象都有一个相关联的Monitor,它可以用于两种同步操作: 一个线程可以在Monitor上等待,直到另一个线程对其发出通知,或者它可以在Monitor上获得一个锁,可能会阻塞, 直到另一个线程放弃锁的所有权。此外,Java在java.util.concurrent.locks 包中提供了实现更高级锁策略的类。该包中的锁不使用对象的Monitor,而是使用不同的本地实现。
  • 对于上述机制的两种锁状态情况,JProfiler都可以记录。在锁状态情况下,会有一个或多个线程、 一个Monitor或一个java.util.concurrent.locks.Lock 实例,以及一个需要一定时间的等待或阻塞操作。 这些锁状态情况在Monitor历史视图中以表格的方式呈现,在锁状态历史图中以可视化的方式呈现。
  • 锁状态历史图关注的是所有涉及的Monitor和线程的整体关系,而不是孤立的Monitor事件过程。 参与锁状态情况的线程和Monitor被涂成蓝色和灰色的矩形,如果它们是死锁的一部分,则被涂成红色。 黑色箭头表示Monitor的所有权,黄色箭头从等待的线程延伸到相关的Monitor, 而虚线的红色箭头则表示某个线程想要获取Monitor,并且当前正处于阻塞状态。 如果已经记录了CPU数据,则将鼠标悬停在阻塞或等待箭头上时,可以获得堆栈跟踪。 这些工具提示包含的超链接,可将你带到Monitor历史视图中的相应行

在这里插入图片描述

4、Current Monitors(当前Monitor):

在这里插入图片描述

10、示例分析

/**
 * @Date: 2022/2/24
 * JVM启动参数:-Xms600m -Xmx600m -XX:SurvivorRatio=8
 */
public class JProfilerTest {
    public static void main(String[] args) {
        while (true) {
            ArrayList beanList = new ArrayList();
            for (int i = 0; i < 500; i++) {
                Info info = new Info();
                info.list.add(new byte[1024 * 10]);//10kb
                beanList.add(info);
            }
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Info {
    int size = 10;
    String desc = "Info";
    //这里会出现问题
    static ArrayList list = new ArrayList();
    
    //出现OOM之后改成非静态的即可
    //ArrayList list = new ArrayList();
}
1、设置JVM参数,运行程序,打开JProfiler进行分析
2、通过Memory视图,可以看到内存一个劲的往上涨,但是就是没有下降的趋势,说明这肯定有问题,过不了多久就会出现OOM

在这里插入图片描述

3、再到Live memory中,点击Mark Current先标记,等一会之后发现有些对象在内存持续增长,发现有些对象能自动进行垃圾回收,点击Run GC手动触发垃圾回收,发现byte[]没有被回收,说明它存在问题。

在这里插入图片描述

4、选中有问题的对象右键点击Show Selection In Heap Walker,然后查看该对象被哪些对象引用了

在这里插入图片描述

5、可以看出byte[]来自于Info类是的list中,并且这个list是ArrayList类型的静态集合,所以找到了:static ArrayList list = new ArrayList();发现list是静态的,这不妥,因为我们的目的是while结束之后Info对象被回收,并且Info对象中的所有字段都被回收,但是list是静态的,那就是类的,众所周知,类变量随类而生,随类而灭,因此每次我们往list中添加值,都是往同一个list中添加值,这会造成list不断增大,并且不能回收,所以最终会导致OOM。

七、Arthas

1、概述

1、Arthas(阿尔萨斯)是阿里巴巴开源的Java诊断工具,在线排查问题无需重启,动态跟踪Java代码,实时监控JVM状态。
2、支持JDK6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的Tab自动补全功能,进一步方便进行问题的定位和诊断。官方文档GitHub地址
3、当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:
  • 这个类从哪个jar包加载的?为什么会报各种类相关的Exception?
  • 我改的代码为什么没有执行到?难道是我没commit?分支搞错了?
  • 遇到问题无法在线上debug,难道只能通过加日志再重新发布吗?
  • 线上遇到某个用户的数据处理有问题,但线上同样无法debug,线下无法重现!
  • 是否有一个全局视角来查看系统的运行状况?
  • 有什么办法可以监控到JVM的实时运行状态?
  • 怎么快速定位应用的热点,生成火焰图?
  • 怎样直接从JVM内查找某个类的实例?

2、Arthas安装与卸载及基本命令

1、在Windows安装先在D盘下新建一个安装目录,下载arthas-boot.jar,然后再用java -jar方式启动
  • 下载命令:curl -O https://arthas.aliyun.com/arthas-boot.jar
  • 使用java -jar启动arthas-boot.jar,第一次执行这个jar,会自动从服务上下载Arthas并安装,运行此命令会先检查是否有启动的Java进程,如果没有则无法运行这个jar

在这里插入图片描述

2、Linux下安装与Windows类似,先下载arthas-boot.jar,再java -jar启动

在这里插入图片描述

3、linux下安装完成之后,.开头的文件夹是隐藏的,可以通过ls -a命令来查看

在这里插入图片描述

4、卸载Arthas
  • Windows:在用户目录下删除.arthas与logs目录即可
  • Linux:执行rm -rf ~/.arthas/rm -rf ~/logs/arthas
5、除了使用java -jar arthas-boot.jar启动,还可以使用java -jar arthas-boot.jar PID启动,不过需要知道Java的进程ID(可以通过jps -l查看正在运行的Java程序的PID)
6、如果端口被占用,可以自定义端口java -jar arthas-boot.jar --telnet-port 端口号 -http-port -1
7、查看帮助:java -jar arthas-boot.jar -h
8、退出Arthas监控客户端:
  • quit\exit:退出当前客户端
  • stop\shutdown:关闭Arthas服务端,并退出所有客户端

3、基本命令

命令说明
help查看命令帮助信息
cat打印文件内容,与Linux里的cat命令类似
echo打印参数
grep匹配查找
tee复制标准输入到标准输出和指定的文件
pwd返回当前的工作目录
cls清空当前屏幕区域
session查看当前会话的信息
reset重置增强类,将被Arthas增强过的类全部还原,Arthas服务端关闭时会重置所有增强过的类
version输出当前目标Java进程所加载的Arthas版本号
history打印命令历史
quit退出当前Arthas客户端,其他Arthas客户端不受影响
stop关闭Arthas服务端,所有客户端退出
keymapArthas快捷键列表及自定义快捷键

4、JVM相关命令

命令说明
dashboard当前系统的实时数据面板
thread查看当前JVM的线程堆栈信息
jvm查看当前JVM的信息
sysprop查看和修改JVM的系统属性
sysenv查看JVM的环境变量
vmoption查看和修改JVM里诊断相关的option
perfcounter查看当前JVM的Perf Counter信息
logger查看和修改logger
getstatic查看类的静态属性
ognl执行ognl表达式
mbean查看Mbean的信息
heapdumpdump java heap,类似jmap命令的heap dump功能
vmtool从JVM里查询对象,执行forceGc

5、JVM相关命令使用

1、dashboard命令基本格式dashboard -i 时间 -n 打印次数,每隔多少的时间间隔(默认5000ms)刷新实时数据共多少次
  • ID:Java级别的线程ID,注意这个ID不能跟jstack中的nativeID一一对应。
  • NAME:线程名
  • GROUP:线程组名
  • PRIORITY:线程优先级(1~10之间的数字,越大表示优先级越高)
  • STATE:线程的状态
  • CPU%:线程的CPU使用率。比如采样间隔1000ms,某个线程的增量CPU时间为100ms,则CPU使用率=100/1000=10%
  • DELTA_TIME:上次采样之后线程运行增量CPU时间,数据格式为
  • TIME:线程运行总CPU时间,数据格式为分:秒
  • INTERRUPTED:线程当前的中断位状态
  • DAEMON:是否是daemon线程

在这里插入图片描述

JVM内部线程:Java8之后支持获取JVM内部线程CPU时间,这些线程只有名称和CPU时间,没有ID及状态等信息(显示ID为-1)。 通过内部线程可以观测到JVM活动,如GC、JIT编译等占用CPU情况,方便了解JVM整体运行状况。
  • 当JVM 堆(heap)/元数据(metaspace)空间不足或OOM时,可以看到GC线程的CPU占用率明显高于其他的线程。
  • 当执行trace/watch/tt/redefine等命令后,可以看到JIT线程活动变得更频繁。因为JVM热更新class字节码时清除了此class相关的JIT编译结果,需要重新编译。
  • JIT编译线程:如 C1 CompilerThread0, C2 CompilerThread0
  • GC线程:如GC Thread0, G1 Young RemSet Sampling
  • 其它内部线程:如VM Periodic Task Thread, VM Thread, Service Thread
2、thread命令参数说明
  • thread id:显示指定线程的运行堆栈
  • thread -n N:指定最忙的前N个线程并打印堆栈
  • thread -b:找出当前阻塞其他线程的线程,目前只支持找出synchronized关键字阻塞住的线程, 如果是java.util.concurrent.Lock,目前还不支持
  • thread -i M:指定CPU使用率统计的采样间隔,单位为毫秒,默认值为200
  • thread –state:查看指定状态的线程

在这里插入图片描述

在这里插入图片描述

3、heapdump命令
  • dump到指定文件heapdump /tmp/dump.hprof
  • 只dump存活对象heapdump --live /tmp/dump.hprof
  • 注意:如果直接使用heapdump命令,表示dump到临时文件

在这里插入图片描述

6、class/classloader相关指令

命令说明
sc查看JVM已加载的类信息
sm查看已加载类的方法信息,只能看到由当前类所声明 (declaring) 的方法,父类则无法看到
jad反编译指定已加载类的源码
mc内存编译器,内存编译.java文件生成.class文件
retransform加载外部的.class文件,retransform到JVM里
redefine加载外部的.class文件,redefine到JVM里
dumpdump已加载类的byte code到特定目录
classloader查看classloader的继承树,urls,类加载信息,使用classloader去getResource,打印出所有查找到的resources的url。对于ResourceNotFoundException比较有用。

7、class/classloader相关指令使用

1、sc命令参数说明:
  • class-pattern:类名表达式匹配,支持全限定名,如com.itan.test.XXX,也支持com/itan/test/XXX这样的格式,这样,我们从异常堆栈里面把类名拷贝过来的时候,不需要在手动把/替换为.
  • method-pattern:方法名表达式匹配
  • -d:输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的ClassLoader等详细信息。如果一个类被多个ClassLoader所加载,则会出现多次
  • -E:开启正则表达式匹配,默认为通配符匹配
  • -f:输出当前类的成员变量信息(需要配合参数-d一起使用)
  • -x:指定输出静态变量时属性的遍历深度,默认为0,即直接使用 toString 输出
  • -n:具有详细信息的匹配类的最大数量(默认为100)
  • 注意:sc 默认开启了子类匹配功能,也就是说所有当前类的子类也会被搜索出来,想要精确的匹配,请打开options disable-sub-class true开关

在这里插入图片描述

2、sm命令参数说明:
  • class-pattern:类名表达式匹配
  • method-pattern:方法名表达式匹配
  • -d:展示每个方法的详细信息
  • -E:开启正则表达式匹配,默认为通配符匹配
  • -n:具有详细信息的匹配类的最大数量(默认为100)

在这里插入图片描述

3、jad命令参数说明:
  • class-pattern:类名表达式匹配
  • -E:开启正则表达式匹配,默认为通配符匹配

在这里插入图片描述

4、mc命令参数说明:
  • -c:指定class的ClassLoader的hashcode
  • -d:输出到指定目录
  • 注意:编译生成.class文件之后,可以结合retransform、redefine命令实现热更新代码,mc命令有可能失败。如果编译失败可以在本地编译好.class文件,再上传到服务器
5、retransform、redefine命令的缺点:
  • 不允许新增加field/method
  • 正在运行的函数,没有退出不能生效
6、classloader命令参数说明:
  • -l:按类加载实例查看统计信息
  • -t:打印所有ClassLoader的继承树
  • -a:列出所有ClassLoader加载的类,请谨慎使用
  • -c:用ClassLoader的hashcode来查看对应的jar的urls
  • –load:加载类

在这里插入图片描述

8、monitor/watch/trace相关指令

注意:这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行stop或将增强过的类执行reset命令
命令说明
monitor方法执行监控
watch方法执行数据观测
trace方法内部调用路径,并输出方法路径上的每个节点上耗时
stack输出当前方法被调用的调用路径
tt方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测

9、monitor/watch/trace相关指令使用

1、monitor命令参数说明:对匹配class-pattern/method-pattern/condition-express的类、方法的调用进行监控,涉及方法的调用次数、执行时间、失败率等;是一个非实时返回命令
  • class-pattern:类名表达式匹配
  • method-pattern:方法名表达式匹配
  • condition-express:条件表达式
  • -c:统计周期,默认值为120秒
  • -b:在方法调用之前计算condition-express
  • -E:开启正则表达式匹配,默认为通配符匹配
监控项说明
timestamp时间戳
classJava类
method方法(构造方法、普通方法)
total调用次数
success成功次数
fail失败次数
rt平均RT
fail-rate失败率

在这里插入图片描述

2、watch命令说明:观察到指定函数的调用情况。能观察到的范围为:返回值、抛出异常、入参,通过编写OGNL表达式进行对应变量的查看
  • class-pattern:类名表达式匹配
  • method-pattern:函数名表达式匹配
  • express:观察表达式,默认值:{params, target, returnObj},主要由OGNL表达式组成
  • condition-express:条件表达式
  • -b:在方法调用之前观察
  • -e:在方法异常之后观察
  • -s:在方法返回之后观察
  • -f:在方法结束之后(正常返回和异常返回)观察
  • -x:指定输出结果的属性遍历深度,默认为1
  • -n:表示执行的次数
  • #cost:方法执行耗时
特别说明:
  • watch命令定义了4个观察事件点,即-b方法调用前,-e方法异常后,-s方法返回后,-f方法结束后
  • 4个观察事件点-b-e-s默认关闭,-f默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出
  • 这里要注意方法入参方法出参的区别,有可能在中间被修改导致前后不一致,除了-b事件点params代表方法入参外,其余事件都代表方法出参
  • 当使用-b时,由于观察事件点是在方法调用前,此时返回值或异常均不存在
  • 在watch命令的结果里,会打印出location信息。location有三种可能值:AtEnterAtExitAtExceptionExit。对应方法入口,方法正常return,方法抛出异常。

在这里插入图片描述

3、trace命令说明:参数与watch命令类似
  • trace命令能主动搜索class-patternmethod-pattern对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。
  • trace能方便的帮助定位和发现因RT高而导致的性能问题缺陷,但其每次只能跟踪一级方法的调用链路。
  • trace在执行的过程中本身是会有一定的性能开销,在统计的报告中并未像JProfiler一样预先减去其自身的统计开销。所以这统计出来有些许的不准,渲染路径上调用的类、方法越多,性能偏差越大。但还是能让你看清一些事情的。

在这里插入图片描述

4、stack命令说明:参数与watch命令类似

在这里插入图片描述

5、tt命令说明:
  • class-pattern:类名表达式匹配
  • method-pattern:函数名表达式匹配
  • condition-express:条件表达式
  • -t:记录类的某个方法每次调用的情况
  • -n:表示执行的次数
  • -s:筛选指定方法的调用信息
  • -i:参数后面跟对应的index编号查看到它的详细信息
  • -p:重做一次调用,通过--replay-times指定调用次数,通过--replay-interval指定多次调用时间间隔(默认1000ms)
表格字段字段解释
INDEX时间片段记录编号,每一个编号代表着一次调用,后续tt还有很多命令都是基于此编号指定记录操作,非常重要。
TIMESTAMP方法执行的本机时间,记录了这个时间片段所发生的本机时间
COST(ms)方法执行的耗时
IS-RET方法是否以正常返回的形式结束
IS-EXP方法是否以抛异常的形式结束
OBJECT执行对象的hashCode(),注意,曾经有人误认为是对象在JVM中的内存地址,但很遗憾他不是。但他能帮助你简单的标记当前执行方法的类实体
CLASS执行的类名
METHOD执行的方法名

在这里插入图片描述

  • 6
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值