20220605 JVM下篇:性能监控与调优篇 3. JVM 监控及诊断工具-GUI 篇
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 虚拟机的性能数据。
- jconsole : JDK 自带的可视化监控工具。查看 Java 应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等
- 第三方工具
- MAT : MAT ( Memory Analyzer Tool )是基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
- Eclipse 插件形式
- JProfiler :商业软件,需要付费。功能强大
- 与 VisualVM 类似
- Arthas :Alibaba 开源的 Java 诊断工具
- Btrace :Java 运行时追踪工具。可以在不停机的情况下,跟踪指定的方法调用、构造函数调用和系统内存等信息
- MAT : MAT ( Memory Analyzer Tool )是基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
2. JConsole
基本概述
jconsole :
- 从 Java 5 开始,在 JDK 中自带的 Java 监控和管理控制台
- 用于对 JVM 中内存、线程和类等的监控,是一个基于 JMX ( java management extensions )的 GUI 性能监控工具
启动
jdk/bin
目录下,启动 jconsole.exe 即可
不需要使用 jps 命令查询 pid
三种连接方式
- 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 代理,或者是一个使用 JDK 1.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 目录下)即:它完全免费。
JAVA_HOME/bin/jvisualvm.exe
插件安装
Visual VM 的一大特点是支持插件扩展,并且插件安装非常方便。我们既可以通过离线下载插件文件 *.nbm ,然后在 Plugin 对话框的已下载页面下,添加己下载的插件。也可以在可用插件页面下在线安装插件。(这里建议安装上: VisualGC )
在 IDEA 上安装插件:
- 安装 VisualVM Launcher 插件
- 在 Settings 中设置 VisualVM 路径
- 运行程序时,选择使用 VisualVM 插件
连接方式
- 本地连接
- 监控本地 Java 进程的 CPU 、类、线程等
- 远程连接
- 确定远程服务器的 ip 地址
- 添加 JMX (通过 JMX 技术具体监控远端服务器哪个 Java 进程)
- 修改 bin/catalina.sh 文件,连接远程的 tomcat
- 在 ../conf 中添加 jmxremote.access 和 jmxremote.password 文件
- 将服务器地址改为公网 ip 地址
- 设置阿里云安全策略和防火墙策略
- 启动 Tomcat ,查看 Tomcat 启动日志和端口监听
- JMX 中输入端口号、用户名、密码登录
主要功能
- 生成/读取堆内存快照
- 查看 JVM 参数和系统属性
- 查看运行中的虚拟机进程
- 程序资源的实时监控
- 程序资源的实时监控
- 其他功能
- JMX 代理连接
- 远程环境监控
- CPU 分析和内存分析
4. Eclipse MAT
基本概述
MAT ( Memory Analyzer Tool )工具是一款功能强大的 Java 堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。
MAT 是基于 Eclipse 开发的,不仅可以单独使用,还可以作为插件的形式嵌入在 Eclipse 中使用。是一款免费的性能分析工具,使用起来非常方便。
获取堆 dump 文件
dump 文件内容
MAT 可以分析 heap dump 文件。在进行内存分析时,只要获得了反映当前设备内存映像的 hprof 文件,通过 MAT 打开就可以直观地看到当前的内存信息。一般说来,这些内存信息包含:
-
所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
-
所有的类信息,包括 classloader 、类名称、父类、静态变量等
-
GCRoot 到所有的这些对象的引用路径
-
线程信息,包括线程的调用栈及此线程的线程局部变量( TLS )
两点说明
说明1:缺点
MAT 不是一个万能工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如 Sun , HP , SAP 所采用的 hprof 二进制堆存储文件,以及 IBM 的 PHD 堆存储文件等都能被很好的解析。
最吸引人的还是能够快速为开发人员生成内存泄漏报表,方便定位问题和分析问题。虽然 MAT 有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从 MAT 展现给我们的信息当中通过经验和直觉来判断才能发现。
获取 dump 文件
- 方法一:通过前一章介绍的
jmap
工具生成,可以生成任意一个 Java 进程的 dump 文件 - 方法二:通过配置 JVM 参数生成。
-
- 选项
-xx:+HeapDumpOnOutOfMemoryError
或-xx:+HeapDumpBeforeFullGC
- 选项
-xx:+HeapDumpPath
所代表的含义就是当程序出现 OutofMemory 时,将会在相应的目录下生成一份 dump 文件。如果不指定选项-xx:+HeapDumpPath
则在当前目录下生成 dump 文件。 - 对比:考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用 jmap + MAT 工具是最常见的组合。
- 选项
- 方法三:使用 Visual VM 可以导出堆 dump 文件
- 方法四:使用 MAT 既可以打开一个已有的堆快照,也可以通过 MAT 直接从活动 Java 程序中导出堆快照。
- 该功能将借助
jps
列出当前正在运行的 Java 进程,以供选择并获取快照。
- 该功能将借助
分析 dump 文件
- histogram:展示了各个类的实例数目以及这些实例的 Shallow heap 或者 Retained heap 的总和
- thread overview
- 查看系统中的 Java 进程
- 查看局部变量的信息
- 获取对象互相引用的关系
- with outcoming references :我引用了谁
- with incoming references :谁引用了我
- 浅堆和深堆
- shallow heap :指一个对象所消耗的内存
- retained heap :所有保留集(指唯一被当前对象引用的对象)的浅堆大小
- 保留集:对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括A本身)
- 支配树
- 源自图论,体现对象实例间的支配关系
- 所有指向对象B的路径都经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的 直接支配者
内存泄漏
内存泄漏( memory leak )的理解,严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 OOM ,也可以叫做 宽泛意义上的“内存泄漏”
内存泄漏与内存溢出的关系:
- 内存泄漏( memory leak )申请了内存用完了不释放,比如一共有 1024M 的内存,分配了 512M 的内存一直不回收,那么可以用的内存只有 512M 了,仿佛泄露掉了一部分;
- 内存溢出( out of memory )申请内存时,没有足够的内存可以使用;
可见,内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出
内存泄漏的分类:
- 经常发生:发生内存泄露的代码会被多次执行,每次执行,泄露一块内存;
- 偶然发生:在某些特定情况下才会发生;
- 一次性:发生内存泄露的方法只会执行一次;
- 隐式泄漏:一直占着内存不释放,直到执行结束;严格的说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽。
Java 中内存泄漏的 8 种情况
1. 静态集合类
静态集合类,如 HashMap
、 LinkedList
等等。如果这些容器为静态的,那么它们的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
public class MemoryLeak {
static List list = new ArrayList();
public void oomTests() {
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("sql");
} catch (Exception e) {
// 异常处理
} finally {
// 1. 关闭结果集 ResultSet
// 2. 关闭声明 Statement
// 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
变量可以放在 readFromNet
方法内部,当方法使用完,那么 msg
的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完 msg
后,把 msg
设置为 null
,这样垃圾回收器也会回收 msg
的内存空间。
6. 改变哈希值
改变哈希值,当一个对象被存储进 HashSet
集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。
否则,对象修改后的哈希值与最初存储进 HashSet
集合中时的哈希值就不同了,在这种情况下,即使在 contains
方法使用该对象的当前引用作为的参数去 HashSet
集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet
集合中单独删除当前对象,造成内存泄漏
这也是 String
为什么被设置成了不可变类型,我们可以放心地把 String 存入 HashSet
,或者把 String
当做 HashMap
的 key 值
当我们想把自己定义的类保存到散列表的时候,需要保证对象的 hashcode
不可变。
7. 缓存泄漏
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题,可以使用 WeakHashMap
代表缓存,此种 Map 的特点是,当除了自身有对 key 的引用外,此 key 没有其他引用那么此 map 会自动丢弃此值。
8. 监听器和回调
内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的 API 中注册回调,却没有显式的取消,那么就会积聚。
需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用,例如将他们保存成为 WeakHashMap
中的键。
5. JProfiler
基本概述
介绍
在运行 Java 的时候有时候想测试运行时占用内存情况,这时候就需要使用测试工具查看了。在 eclipse 里面有 Eclipse Memory Analyzer tool ( MAT )插件可以测试,而在 IDEA 中也有这么一个插件,就是 JProfiler 。
JProfiler 是由 ej-technologies 公司开发的一款 Java 应用性能诊断工具。功能强大,但是收费。
特点
-
使用方便、界面操作友好(简单且强大)
-
对被分析的应用影响小(提供模板)
-
CPU,Thread,Memory 分析功能尤其强大
-
支持对 jdbc,noSql,jsp,servlet,socket 等进行分析
-
支持多种模式(离线,在线)的分析
-
支持监控本地、远程的 JVM
-
跨平台,拥有多种操作系统的安装版本
主要功能
-
方法调用:对方法调用的分析可以帮助您了解应用程序正在做什么,并找到提高其性能的方法
-
内存分配:通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄露问题,优化内存使用
-
线程和锁:JProfiler提供多种针对线程和锁的分析视图助您发现多线程问题
-
高级子系统:许多性能问题都发生在更高的语义级别上。例如,对于 JDBC 调用,您可能希望找出执行最慢的 SQL 语句。JProfiler 支持对这些子系统进行集成分析
6. Arthas
上述工具都必须在服务端项目进程中配置相关的监控参数,然后工具通过远程连接到项目进程,获取相关的数据。这样就会带来一些不便,比如线上环境的网络是隔离的,本地的监控工具根本连不上线上环境。并且类似于 JProfiler 这样的商业工具,是需要付费的。
那么有没有一款工具不需要远程连接,也不需要配置监控参数,同时也提供了丰富的性能监控数据呢?
阿里巴巴开源的性能分析神器 Arthas 应运而生。
Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪 Java 代码;实时监控 JVM 状态。 Arthas 支持 JDK 6 +,支持 Linux / Mac / Windows ,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。当你遇到以下类似问题而束手无策时, Arthas 可以帮助你解决:
-
这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
-
我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
-
遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
-
线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
-
是否有一个全局视角来查看系统的运行状况?
-
有什么办法可以监控到JVM的实时运行状态?
-
怎么快速定位应用的热点,生成火焰图?
7. Java Misssion Control
在 Oracle 收购 Sun 之前, Oracle 的 JRockit 虚拟机提供了一款叫做 JRockit Mission Control 的虚拟机诊断工具。
在 Oracle 收购 Sun 之后, Oracle 公司同时拥有了 Hotspot 和 JRockit 两款虚拟机。根据 Oracle 对于 Java 的战略,在今后的发展中,会将 JRokit 的优秀特性移植到 Hotspot 上。其中一个重要的改进就是在 Sun 的 JDK 中加入了 JRockit 的支持。
在 Oracle JDK 7u40 之后, Mission Control 这款工具己经绑定在 Oracle JDK 中发布。
自 Java 11 开始,本节介绍的 JFR 己经开源。但在之前的 Java 版本, JFR 属于 Commercial Feature 通过 Java 虚拟机参数 -XX:+UnlockCommercialFeatures
开启。
Java Mission Control (简称 JMC ) , Java 官方提供的性能强劲的工具,是一个用于对 Java 应用程序进行管理、监视、概要分析和故障排除的工具套件。
它包含一个 GUI 客户端以及众多用来收集 Java 虚拟机性能数据的插件如 JMX Console (能够访问用来存放虚拟机齐个于系统运行数据的 MXBeans )以及虚拟机内置的高效 profiling 工具 Java Flight Recorder ( JFR )。
JMC 的另一个优点就是:采用取样,而不是传统的代码植入技术,对应用性能的影响非常非常小,完全可以开着 JMC 来做压测(唯一影响可能是 full gc 多了)。
Java Flight Recorder ( JFR )
Java Flight Recorder 是 JMC 的其中一个组件,能够以极低的性能开销收集 Java 虚拟机的性能数据。与其他工具相比, JFR 的性能开销很小,在默认配置下平均低于 1% 。 JFR 能够直接访问虚拟机内的敌据并且不会影响虚拟机的优化。因此它非常适用于生产环境下满负荷运行的 Java 程序。
Java Flight Recorder 和 JDK Mission Control 共同创建了一个完整的工具链。 JDK Mission Control 可对 Java Flight Recorder 连续收集低水平和详细的运行时信息进行高效、详细的分析。
当启用时 JFR 将记录运行过程中发生的一系列事件。其中包括 Java 层面的事件如线程事件、锁事件,以及 Java 虚拟机内部的事件,如新建对象,垃圾回收和即时编译事件。按照发生时机以及持续时间来划分, JFR 的事件共有四种类型,它们分别为以下四种:
-
瞬时事件( Instant Event ) ,用户关心的是它们发生与否,例如异常、线程启动事件。
-
持续事件( Duration Event ) ,用户关心的是它们的持续时间,例如垃圾回收事件。
-
计时事件( Timed Event ) ,是时长超出指定阈值的持续事件。
-
取样事件( Sample Event ),是周期性取样的事件。
取样事件的其中一个常见例子便是方法抽样( Method Sampling ),即每隔一段时问统计各个线程的栈轨迹。如果在这些抽样取得的栈轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法
8. 其他工具
Flame Graphs(火焰图)
在追求极致性能的场景下,了解你的程序运行过程中 cpu 在干什么很重要,火焰图就是一种非常直观的展示 CPU 在程序整个生命周期过程中时间分配的工具。火焰图对于现代的程序员不应该陌生,这个工具可以非常直观的显示出调用找中的 CPU 消耗瓶颈。
火焰图,简单通过 x 轴横条宽度来度量时间指标,y 轴代表线程栈的层次。
Tprofiler
TProfiler 是一个可以在生产环境长期使用的性能分析工具
Btrace
BTrace 是 SUN Kenai 云计算开发平台下的一个开源项目,旨在为 Java 提供安全可靠的动态跟踪分析工具。
其他
- YourKit
- JProbe
- Spring Insight