其他分享
首页 > 其他分享> > JVM 学习笔记二十六、JVM监控及诊断工具-GUI篇

JVM 学习笔记二十六、JVM监控及诊断工具-GUI篇

作者:互联网

首图

二十六、JVM监控及诊断工具-GUI篇

在这里插入图片描述

1、工具概述

使用上一张命令行工具或组合能帮您获取目标Java应用性能相关的基础信息,但他们存在下列局限:

为此,JDK提供了一些内存泄露的分析工具,如 jconsole,jvisualvm等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求。所以这里我们介绍的工具相对多一些、丰富一些。

图形化综合诊断工具

2、jConsole

2.1、基本概述

2.2、启动

两种方式:

2.3、三种连接方式

2.4、主要作用

演示:

image-20210511154849436

image-20210511154911371

image-20210511154936119

image-20210511155030583

image-20210511155046463

3、Visual VM

3.1、基本概述

3.2、插件的安装

3.3、连接方式

在这里插入图片描述

3.4、主要功能

(1)生成/读取堆内存快照

(2)查看JVM参数和系统属性

(3)查看运行中的虚拟机进程

(4)生成/读取线程快照

(5)程序资源的实时监控

(6)其他功能:JMX代理连接、远程环境监控、CPU分析和内存分析

概览

image-20210511163352540

image-20210511163359911

image-20210511163411267

image-20210511163424295

生成和查看堆dump文件

在这里插入图片描述

然后在快照上右键即可将快照(.hprof文件)保存到磁盘:

在这里插入图片描述

通过选择:文件---->装入,可以导入刚才保存的 .hprof文件:

在这里插入图片描述

生成和查看线程dump文件

类似于堆dump文件,通过VisualVM可以检测到程序是否死锁,有如下测试程序:

在这里插入图片描述

CPU抽样和内存抽样

在这里插入图片描述

在这里插入图片描述

4、eclipse MAT

4.1、基本概述

4.2、获取堆dump文件

dump文件内容

MAT可以分析heap dump文件。进行内存分析时,只要获得了反应当前设备内存映像的 hprof文件,通过MAT打开就可以直观的看到当前的内存信息。

一般来说,这些内存信息包含:

两点说明

如何获取dump文件

4.3、分析堆dump文件

在这里插入图片描述

显示如下:

在这里插入图片描述

分析dump文件:

在这里插入图片描述

在这里插入图片描述

查看对象被谁引用,可以进行如下操作:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

浅堆:

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

深堆:

注意:浅堆是指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接或者间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。

补充:对象的实际大小

在这里插入图片描述

在这里插入图片描述

下面以Lily为例分析深堆的大小是如何计算出来的

在这里插入图片描述

在这里插入图片描述

支配树: 支配树的概念来自于图论。

MAT提供了一个称为支配树(Dominator Tree)的对象图。支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。**如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。**支配树是基于对象间的引用图所建立的,它有以下基本性质:

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

在这里插入图片描述

下图显示了对象支配树的部分视图。该截图显示部分学生Lily的history队列的直接支配对象。即当Lily对象被回收,也会一并回收的所有对象。显然能被3或者5整除的网页不会出现在该列表中,因为他们同时被另外两名学生对象所引用。

在这里插入图片描述

补充1:再谈内存泄露

内存泄露的理解与分类

在这里插入图片描述

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

严格来说,只有对象不会再被程序用到了,但是GC用不能回收它们的情况,才叫内存泄露。

但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄露”。

在这里插入图片描述

对象X引用对象Y,X的生命周期比Y的生命周期长;

那么当Y生命周期结束的时候,X依然引用着Y,这时候,垃圾回收是不会回收对象Y的;

如果对象X还引用着生命周期比较短的A、B、C,对象A又引用着对象a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄露,直至内存溢出。

内存泄露与内存溢出的关系:

可见,内存泄露和内存溢出的关系:内存泄露的增多,最终导致内存溢出。

泄露的分类:

5、Java中内存泄露的8种情况

  1. 静态集合类

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

    public class MemeoryLeak {
        static List list = new ArrayList<>();
        
        public void oomTest() {
            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", "", "");
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("...");
        } catch (Exception e) {  // 异常日志
            
        } finally {
            // 1.关闭结果集
            // 2.关闭声明的对象
            // 3.关闭连接
        }
    }
    
  5. 变量不合理的作用域

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

    public class UsingRandom {
        private String msg;
        public void receiveMsg() {
            readFromNet();  // 从网络上接收数据保存到msg中
            saveDB();  // 把msg保存到数据库中
        }
    }
    

    如上面这个伪代码,通过readReomNet方法把接收的消息保存在变量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不可变。

  7. 缓存泄露

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

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

    在这里插入图片描述

    上面图示主要演示了WeakHashMap如何自动释放缓存对象,当init函数执行完成后,局部变量字符串引用obejct1,obejct2,obejct3,obejct4都会消失,此时只有静态map中保存了对字符串对象的引用,可以看到,调用gc之后,HashMap没有被回收,而WeakHashMap里面的缓存被回收了。

  8. 监听器和回调

    内存泄露的另一个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显式的取消,那么就会聚集。需要确保回调立即被当做垃圾回收的最佳方法是只保存它的弱引用,例如将它们保存成为WeakHashMap中的键。

内存泄露案例分析

案例代码:

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);
    }
}

分析:

假设这个栈一直增长,增长后如下图所示:

在这里插入图片描述

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

image-20210512102946166

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

解决办法:

将pop()这个函数该如如下函数即可:

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

6、JProfiler

6.1、基本概述

介绍

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

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

特点

数据采集方式

在这里插入图片描述

我们选择第二项可以打开当前正在运行的Java程序,之后会弹出一个窗口,让选择数据采集方式,选择默认后点击OK即可。

在这里插入图片描述

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

Instrumentation:这是JProfiler全功能模式。在class加载之前,JProfiler把相关功能代码写入到需要分析的class的bytecode中,对正在运行的jvm有一定的影响。

Sampling:类似于样本统计,每隔一定时间(5ms)将每个线程栈中方法的信息统计出来。

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

遥感检测Telemetries

在这里插入图片描述

内存视图Live Memory

class/class instance的相关信息。例如对象的个数,大小,对象创建的方法执行栈,对象创建的热点。

在这里插入图片描述

显示所有加载的类的列表和在堆上分配的实例数。只有Java 1.5(JVMTI)才会显示此图。

查看特定时间段对象的分配,并记录分配的调用堆栈。

显示一颗请求树或者方法、类、包或对已选择类有待注释的分配信息的J2EE组件。

显示一个列表,包括方法、类、包或已分配已选类的J2EE组件。你可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。

类跟踪视图可以包含任意数量的图标,显示选定的类和包的实例和时间。

堆遍历heap walker

cpu视图 cpu views

线程视图threads

监视器&锁 Monitors&locks

7、Arthas

7.1、基本概述

前面两款工具也有缺点,都必须在服务端项目进程中配置相关监控参数。然后工具通过远程连接到项目进程,获取相关数据。这样就带来一些不便,比如线上环境的网络是隔离的,本地的监控工具根本连不上线上环境。并且类似于Jprofiler这样的商业工具,是需要付费的。

那么有没有一款工具不需要远程连接,也不需要配置监控参数,同时提供了丰富的性能监控数据呢?

这就是下面要介绍的一款阿里巴巴开源的性能分析神器Arthas(阿尔萨斯)

官方使用文档:网址

7.2、相关诊断命令

命令帮助网址

基础指令

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

jvm相关

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

class/classloader相关

sc——查看JVM已加载的类信息
sm——查看已加载类的方法信息
jad——反编译指定已加载类的源码
mc——内存编译器,内存编译.java文件为.class文件
retransform——加载外部的.class文件,retransform到JVM里
redefine——加载外部的.class文件,redefine到JVM里
dump——dump 已加载类的 byte code 到特定目录
classloader——查看classloader的继承树,urls,类加载信息,使用classloader去getResource

monitor/watch/trace相关

请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 stop 或将增强过的类执行 reset 命令。

monitor——方法执行监控
watch——方法执行数据观测
trace——方法内部调用路径,并输出方法路径上的每个节点上耗时
stack——输出当前方法被调用的调用路径
tt——方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测

8、Java Mission Control

8.1、概述

8.2、功能:实时监控JVM运行时的状态

在这里插入图片描述

9、其他工具

在这里插入图片描述

标签:二十六,Java,对象,GUI,内存,JVM,泄露,引用
来源: https://blog.csdn.net/qq_42372017/article/details/116834405