JVM - 监控及诊断工具GUI
# 工具概述
使用上一章命令行工具或组合能帮您获取目标 Java 应用性能相关的基础信息,但它们存在下列局限:
无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)
要求用户登录到目标 Java 应用所在的宿主机上,使用起来不是很方便
分析数据通过终端输出,结果展示不够直观
为此,JDK 提供了一些内存泄漏的分析工具,如 jconsole,jvisualvm 等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求。所以这里我们介绍的工具相对多一些、丰富一些。
JDK 自带的工具
自带的工具在 JDK 的 Bin 目录下。
- jconsole:JDK 自带的可视化监控工具。查看 Java 应用程序的运行概况、监控堆信息、永久区(或元空间)使用情况、类加载情况等
- Visual VM:Visual VM 是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机上运行的基于 Java 技术的应用程序的详细信息
- JMC:Java Mission Control,内置 Java Flight Recorder。能够以极低的性能开销收集 Java 虚拟机的性能数据
第三方工具
MAT:MAT(Memory Analyzer Tool)是基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
JProfiler:商业软件,需要付费,功能强大
# JConsole
官方地址:
https://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html
jconsole:从 Java5 开始,在 JDK 中自带的 java 监控和管理控制台。用于对 JVM 中内存、线程和类等的监控,是一个基于 JMX(java management extensions)的 GUI 性能监控工具。
直接在 JDK 的 bin 目录下启动 jconsole.exe 命令即可。
# 三种连接方式
Local:使用 JConsole 连接一个正在本地系统运行的JVM,并且执行程序的和运行 JConsole 的需要是同一个用户。JConsole 使用文件系统的授权通过 RMIl 连接器连接到平台的 MBean 服务器上。这种从本地连接的监控能力只有 Sun 的 JDK 具有。
Remote:使用下面的 URL 通过 RMI 连接器连接到一个 JMX 代理,servicejmx.rmi:///jndi/rmi://hostName:portNum/jmxrmi
。JConsole 为建立连接,需要在环境变量中设置 mx.remote.credentials
来指定用户名和密码,从而进行授权。
Advanced:使用一个特殊的 URL 连接 JMX 代理。一般情况使用自己定制的连接器而不是 RMI 提供的连接器来连接 JMX 代理,或者是一个使用 JDK1.4 的实现了 JMX 和 JMX Rmote 的应用。
# 工具图
概述、内存、线程、类、VM 概要、MBean。
# Visual VM
官方地址:
https://visualvm.github.io/index.html
Visual VM 是一个功能强大的多合一故障诊断和性能监控的可视化工具。它集成了多个 JDK 命令行工具,使用 Visual VM 可用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的 CPU、GC、堆、方法区及线程的信息(jstat、jstack)等,甚至代替 JConsole。在 JDK 6 Update 7 以后,Visual VM 便作为 JDK 的一部分发布(VisualVM 在 JDK/bin 目录下)即:它完全免费。
# 插件安装
Visual VM 的一大特点是支持插件扩展,并且插件安装非常方便。我们既可以通过离线下载插件文件 *.nbm
,然后在 Plugin 对话框的已下载页面下,添加已下载的插件。也可以在可用插件页面下,在线安装插件。(这里建议安装上 Visual GC)
插件地址:
https://visualvm.github.io/pluginscenters.html
可以在 Idea 按照 Visual VM:
按照完插件之后,重启 Idea ,然后配置 Visual VM 的启动路径:
# 连接方式
本地连接
监控本地 Java 进程的 CPU、类、线程等。
远程连接
- 确定远程服务器的 IP 地址
- 添加 JMX,通过 JXM 技术具体监控远端服务器的哪个 Java 进程)
- 修改 JDK 的
bin/catalina.sh
文件,连接远程的 Tomcat - 在
../conf
中添加jmxremote.access
和jmxremote.password
文件 - 将服务器地址改为公网 IP 地址
- 设置阿里云安全策略和防火墙策略
- 启动 Tomcat,查看 Tomcat 启动日志和端口监听
- JMX 中输入端口号、用户名、密码登录
# 主要功能
- 生成/读取堆内存/线程快照
- 查看 JVM 参数和系统属性
- 查看运行中的虚拟机进程
- 程序资源的实时监控
- JMX 代理连接、远程环境监控、CPU 分析和内存分析
# Eclipse MAT
官方地址:
https://www.eclipse.org/mat/downloads.php
MAT(Memory Analyzer Tool)工具是一款功能强大的 Java 堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。MAT 是基于 Eclipse 开发的,不仅可以单独使用,还可以作为插件的形式嵌入在 Eclipse 中使用。是一款免费的性能分析工具,使用起来非常方便。
MAT 可以分析 heap dump 文件。在进行内存分析时,只要获得了反映当前设备内存映像的 hprof 文件,通过 MAT 打开就可以直观地看到当前的内存信息。一般说来,这些内存信息包含:
- 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值
- 所有的类信息,包括 classloader、类名称、父类、静态变量等
- GCRoot 到所有的这些对象的引用路径
- 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)
# 优缺点
缺点
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文件
打开一个 dump 文件。
打开后的界面:
概述,查看堆大小,类的数量,类加载器等信息。
Histogram 直方图
thread overview:查看系统中的 Java 线程,查看局部变量表的信息。
查看对象互相引用的关系
在 MAT 工具的 thread overview 中,右键目标类找到 with outgoing references
或 with incoming references
。即可查看谁引用了该类,该类引用了谁。
# 浅堆和深堆
浅堆(Shallow Heap)
指一个对象所消耗的内存。在 32 位系统中,一个对象引用会占据 4 个字节,一个 int 类型会占据 4 个字节,long 型变量会占据 8 个字节,每个对象头需要占用 8 个字节。根据堆快照格式不同,对象的大小可能会向8字节进行对齐。
以 String 为例:2 个 int 值共占 8 字节,对象引用占用 4 字节,对象头 8 字节,合计 20 字节,向 8 字节对齐,故占 24 字节。(JDK7 中)
数据类型 | 类型 | 目的 |
---|---|---|
int | hash32 | 0 |
int | hash | 0 |
ref | value | C:\User\Administration |
这 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 的深堆范围内。
练习:
上图中,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 对象呢?则 D 就只属于 B 调用。如下图:
# 深堆与浅堆案例分析
案例视频:https://www.bilibili.com/video/BV1PJ411n7xZ?p=333
代码:
/**
* 有一个学生浏览网页的记录程序,它将记录 每个学生访问过的网站地址。
* 它由三个部分组成:Student、WebPage和StudentTrace三个类
*
* -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=d:\student.hprof
*/
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
JVM 参数:
-XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=d:\student.hprof
运行后,生成 dump 文件,利用 MAT 打开 dump 文件,找到 thread overview,如图得到其中一个学生的访问数据大小 1288:
计算 1288 怎么得到的。
首先有 15 个 WebPage,每个对应 152 个字节:15 * 152 = 2280
字节,这是 elementData 的实际大小。
能被 7 整除,且能被 3 整除,以及能被 7 整除,且能被 5 整除的数值有:0、21、42、63、84、35、70 共 7 个数。
7 * 152 = 1064
字节。
那么我们得到 2280 - 1064 - 1288 = 72
字节,这个 72 字节是什么?
15 个 elementData 的元素 * 4 字节 = 60 字节。
60 + 8 个对象头的字节 + 数组本身 4 个字节 = 72 字节。
# 支配树
支配树的概念源自图论。
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 中,单击工具栏上的对象支配树按钮,可以打开对象支配树视图。
可以看到,有 8 个 WebPage 属于该学生的,上面案例得到的 7 个 WebPage 属于「共有」,15 - 7 = 8
个 WebPage 属于该学生的「私有」。
# Tomcat堆溢出分析案例
案例视频:
https://www.bilibili.com/video/BV1PJ411n7xZ?p=334
Tomcat 是最常用的 Java Servlet 容器之一,同时也可以当做单独的 Web 服务器使用。Tomcat 本身使用 Java 实现,并运行于 Java 虚拟机之上。在大规模请求时,Tomcat 有可能会因为无法承受压力而发生内存溢出错误。这里根据一个被压垮的 Tomcat 的堆快照文件,来分析 Tomcat 在崩溃时的内部情况。
图 1:
图 2:
图 3:sessions 对象,它占用了约 17MB 空间。
图 4:可以看到 sessions 对象为 ConcurrentHashMap,其内部分为 16 个 Segment。从深堆大小看,每个 Segment 都比较平均,大约为 1MB,合计 17MB。
图 5:
图 6:当前堆中含有 9941 个 session,并且每一个 session 的深堆为 1592 字节,合计约 15NB,达到当前堆大小的 50%。
图 7:
图 8:
根据当前的 session 总数,可以计算每秒的平均压力为:9941 / (1403324677648 - 1403324645728) * 1000 = 311
次 / 秒。
由此推断,在发生 Tomcat 堆溢出时,Tomcat 在连续 30 秒的时间内,平均每秒接收了约 311 次不同客户端的请求,创建了合计 9941 个 session。
# 再谈内存问题
# 内存泄漏(memory leak)
可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让 JVM 误以为此对象还在引用中,无法回收,造成内存泄漏)。
是否还被使用?是
是否还被需要?否
严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致 00M,也可以叫做宽泛意义上的「内存泄漏」。
如下图,当 Y 生命周期结束的时候,X 依然引用着 Y,这时候,垃圾回收期是不会回收对象 Y 的;如果对象 X 还引用着生命周期比较短的 A、B、C,对象 A 又引用着对象 a、b、c,这样就可能造成大量无用的对象不能被回收,进而占据了内存资源,造成内存泄漏,直到内存溢出。
申请了内存用完了不释放,比如一共有 1024M 的内存,分配了 512M 的内存一直不回收,那么可以用的内存只有 512M 了,仿佛泄露掉了一部分;通俗一点讲的话,内存泄漏就是「占着茅坑不拉 shi」
# 内存溢出(out of memory)
申请内存时,没有足够的内存可以使用;通俗一点儿讲,一个厕所就三个坑,有两个站着茅坑不走的(内存泄漏),剩下最后一个坑,厕所表示接待压力很大,这时候一下子来了两个人,坑位(内存)就不够了,内存泄漏变成内存溢出了。可见,内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出。
泄漏的分类:
- 经常发生:发生内存泄露的代码会被多次执行,每次执行,泄露一块内存
- 偶然发生:在某些特定情况下才会发生
- 一次性:发生内存泄露的方法只会执行一次
- 隐式泄漏:一直占着内存不释放,直到执行结束;严格的说这个不算内存泄漏,因为最终释放掉了,但是如果执行时间特别长,也可能会导致内存耗尽
# 内存泄露的8种情况
静态集合类
静态集合类,如 HashMap、LinkedList 等等。如果这些容器为静态的,那么它们的生命周期与 JVM 程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
public class MemoryLeak {
static List list = new ArrayList(); // 静态和类的生命周期一致
public void oomTests(){
Object obj = new Object(); // 局部变量
list.add(obj);
}
}
2
3
4
5
6
7
单例模式
单例模式,和静态集合导致内存泄露的原因类似,因为单例的静态特性,它的生命周期和 JVM 的生命周期一样长,所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。
内部类持有外部类
内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。
各种连接,如数据库连接、网络连接和 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.关闭结果集 Statement
// 2.关闭声明的对象 ResultSet
// 3.关闭连接 Connection
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
变量不合理的作用域
变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为 null,很有可能导致内存泄漏的发生。
public class UsingRandom {
private String msg;
public void receiveMsg(){
readFromNet();//从网络中接受数据保存到msg中
saveDB();//把msg保存到数据库中
}
}
2
3
4
5
6
7
如上面这个伪代码,通过 readFromNet 方法把接受的消息保存在变量 msg 中,然后调用 saveDB 方法把 msg 的内容保存到数据库中,此时 msg 已经就没用了,由于 msg 的生命周期与对象的生命周期相同,此时 msg 还不能回收,因此造成了内存泄漏。
实际上这个 msg 变量可以放在 receiveMsg 方法内部,当方法使用完,那么 msg 的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完 msg 后,把 msg 设置为 null,这样垃圾回收器也会回收 msg 的内存空间。
改变哈希值
改变哈希值,当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。
否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单独删除当前对象,造成内存泄漏。
这也是 String 为什么被设置成了不可变类型,我们可以放心地把 String 存入 HashSet,或者把 String 当做 HashMap 的 key 值。
当我们想把自己定义的类保存到散列表的时候,需要保证对象的 hashCode 不可变。
代码示例 1:
/**
* 例 1
*/
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 + '\'' +
'}';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
输出结果:
[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]
[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}]
[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]
2
3
代码示例 2:
/**
* 例 2
*/
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 +
'}';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
输出结果:
hs.remove = false
hs.size = 2
[Point{x=20}, Point{x=20}]
2
3
缓存泄露
内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题,可以使用 WeakHashMap 代表缓存,此种 Map 的特点是,当除了自身有对 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);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
输出结果:
String引用ref1,ref2,ref3,ref4 消失
WeakHashMap GC之前
obejct2=cacheObject2
obejct1=cacheObject1
WeakHashMap GC之后
HashMap GC之前
obejct4=cacheObject4
obejct3=cacheObject3
HashMap GC之后
obejct4=cacheObject4
obejct3=cacheObject3
2
3
4
5
6
7
8
9
10
11
上面代码和图示主演演示 WeakHashMap 如何自动释放缓存对象,当 init 函数执行完成后,局部变量字符串引用 weakd1,weakd2,d1,d2 都会消失,此时只有静态 map 中保存中对字符串对象的引用,可以看到,调用 gc 之后,HashMap 的没有被回收,而 WeakHashMap 里面的缓存被回收了。
监听器和其他回调
内存泄漏第三个常见来源是监听器和其他回调,如果客户端在你实现的 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];
// }
// 正确的做法
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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
上述程序并没有明显的错误,但是这段程序有一个内存泄漏,随着 GC 活动的增加,或者内存占用的不断增加,程序性能的降低就会表现出来,严重时可导致内存泄漏,但是这种失败情况相对较少。
代码的主要问题在 pop 函数,下面通过这张图示展现。假设这个栈一直增长,增长后如下图所示
当进行大量的 pop 操作时,由于引用未进行置空,GC 是不会释放的,如下图所示
从上图中看以看出,如果栈先增长,再收缩,那么从栈中弹出的对象将不会被当作垃圾回收,即使程序不再使用栈中的这些队象,他们也不会回收,因为栈中仍然保存这对象的引用,俗称过期引用,这个内存泄露很隐蔽。
将代码中的 pop()方法变成如下方法:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
2
3
4
5
6
7
一旦引用过期,清空这些引用,将引用置空。
# 使用OQL语言查询对象信息
MAT 支持一种类似于 SQL 的查询语言 OQL(Object Query Language)。OQL 使用类 SQL 语法,可以在堆中进行对象的查找和筛选。
# 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
2
3
在 Select 子句中,使用 AS RETAINED SET
关键字可以得到所得对象的保留集。
SELECT AS RETAINED SET * FROM cn.kbt.mat.Student
DISTINCT
关键字用于在结果集中去除重复对象。
SELECT DISTINCT OBJECTS classof(s) FROM java.lang.String s
# FROM子句
From 子句用于指定查询范围,它可以指定类名、正则表达式或者对象地址。
SELECT * FROM java.lang.String s
使用正则表达式,限定搜索范围,输出所有 cn.kbt 包下所有类的实例。
SELECT * FROM "cn\.kbt\..*"
使用类的地址进行搜索。使用类的地址的好处是可以区分被不同 ClassLoader 加载的同一种类型。
select * from 0x37a0b4d
# WHERE子句
Where 子句用于指定 OQL 的查询条件。OQL 查询将只返回满足 Where 子句指定条件的对象。Where 子句的格式与传统 SQL 极为相似。
返回长度大于 10 的 char 数组。
SELECT * FROM Ichar[] 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
返回数组长度大于 15,并且深堆大于 1000 字节的所有 Vector 对象。
SELECT * FROM java.util.Vector v WHERE v.elementData.@length > 15 AND v.@retainedHeapSize > 1000
# 内置对象与方法
OQL 中可以访问堆内对象的属性,也可以访问堆内代理对象的属性。访问堆内对象的属性时,格式如下,其中 alias 为对象名称:
[ <alias>. ] <field> . <field>. <field>
访问 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
# JProfiler
官网地址:
https://www.ej-technologies.com/products/jprofiler/overview.html
在运行 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 支持对这些子系统进行集成分析
Idea 集成安装:
视频讲解:https://www.bilibili.com/video/BV1PJ411n7xZ?p=341
数据采集方式:
JProfier 数据采集方式分为两种:Sampling(样本采集)和 Instrumentation(重构模式)。
Instrumentation:这是 JProfiler 全功能模式。在 class 加载之前,JProfier 把相关功能代码写入到需要分析的 class 的 bytecode 中,对正在运行的 jvm 有一定影响。
- 优点:功能强大。在此设置中,调用堆栈信息是准确的
- 缺点:若要分析的 Class 较多,则对应用的性能影响较大,CPU 开销可能很高(取决于 Filter 的控制)。因此使用此模式一般配合 Filter 使用,只对特定的类或包进行分析
Sampling:类似于样本统计,每隔一定时间(5ms)将每个线程栈中方法栈中的信息统计出来。
- 优点:对 CPU 的开销非常低,对应用影响小(即使你不配置任何 Filter)
- 缺点:一些数据/特性不能提供(例如:方法的调用次数、执行时间)
注:JProfiler 本身没有指出数据的采集类型,这里的采集类型是针对方法调用的采集类型。因为 JProfiler 的绝大多数核心功能都依赖方法调用采集的数据,所以可以直接认为是 JProfiler 的数据采集类型。
# 遥感监测 Telemetries
# 内存视图 Live Memory
Live memory 内存剖析:class/class instance 的相关信息。例如对象的个数,大小,对象创建的方法执行栈,对象创建的热点。
- 所有对象 All Objects:显示所有加载的类的列表和在堆上分配的实例数。只有 Java 1.5(JVMTI)才会显示此视图。
- 记录对象 Record Objects:查看特定时间段对象的分配,并记录分配的调用堆栈。
- 分配访问树 Allocation Call Tree:显示一棵请求树或者方法、类、包或对已选择类有带注释的分配信息的 J2EE 组件。
- 分配热点 Allocation Hot Spots:显示一个列表,包括方法、类、包或分配已选类的 J2EE 组件。你可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。
- 类追踪器 Class Tracker:类跟踪视图可以包含任意数量的图表,显示选定的类和包的实例与时间。
# 堆遍历 heap walker
# cpu 视图(cpu views)
JProfiler 提供不同的方法来记录访问树以优化性能和细节。线程或者线程组以及线程状况可以被所有的视图选择。所有的视图都可以聚集到方法、类、包或 J2EE 组件等不同层上。
- 访问树 Call Tree:显示一个积累的自顶向下的树,树中包含所有在 JVM 中已记录的访问队列。JDBC,JMS 和 JNDI 服务请求都被注释在请求树中。请求树可以根据 Servlet 和 JSP 对 URL 的不同需要进行拆分。
- 热点 Hot Spots:显示消耗时间最多的方法的列表。对每个热点都能够显示回溯树。该热点可以按照方法请求,JDBC,JMS 和 JNDI 服务请求以及按照 URL 请求来进行计算。
- 访问图 Call Graph:显示一个从已选方法、类、包或 J2EE 组件开始的访问队列的图。
- 方法统计 Method Statistis:显示一段时间内记录的方法的调用时间细节。
# 线程视图 threads
JProfiler 通过对线程历史的监控判断其运行状态,并监控是否有线程阻塞产生,还能将一个线程所管理的方法以树状形式呈现。对线程剖析。
- 线程历史 Thread History:显示一个与线程活动和线程状态在一起的活动时间表
- 线程监控 Thread Monitor:显示一个列表,包括所有的活动线程以及它们目前的活动状况
- 线程转储 Thread Dumps:显示所有线程的堆栈跟踪
线程分析主要关心三个方面:
- Web 容器的线程最大数。比如:Tomcat 的线程容量应该略大于最大并发数
- 线程阻塞
- 线程死锁
# 监控和锁 Monitors&Locks
所有线程持有锁的情况以及锁的信息。观察 JVM 的内部线程并查看状态:
- 死锁探测图表 Current Locking Graph:显示 JVM 中的当前死锁图表
- 目前使用的监测器 Current Monitors:显示目前使用的监测器并且包括它们的关联线程
- 锁定历史图表 Locking History Graph:显示记录在 JVM 中的锁定历史
- 历史检测记录 Monitor History:显示重大的等待事件和阻塞事件的历史记录
- 监控器使用统计 Monitor Usage Statistics:显示分组监测,线程和监测类的统计监测数据
# 简单案例
案例 1
public class JProfilerTest {
public static void main(String[] args) {
while (true){
ArrayList list = new ArrayList();
for (int i = 0; i < 500; i++) {
Data data = new Data();
list.add(data);
}
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Data{
private int size = 10;
private byte[] buffer = new byte[1024 * 1024];//1mb
private String info = "hello,kele";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
利用 Jprofiler 监控代码的执行,查看内存和 GC 的活动。
案例 2
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,kele";
static ArrayList list = new ArrayList(); // 错误的写法:静态与类的声明周期一致,所以上面的变量不会被回收
// ArrayList list = new ArrayList(); // 正确的写法
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
利用 Jprofiler 监控代码的执行,查看内存和 GC 的活动。
# Arthas
官方地址:
https://arthas.aliyun.com/doc/quick-start.html
上述工具都必须在服务端项目进程中配置相关的监控参数,然后工具通过远程连接到项目进程,获取相关的数据。这样就会带来一些不便,比如线上环境的网络是隔离的,本地的监控工具根本连不上线上环境。并且类似于 Jprofiler 这样的商业工具,是需要付费的。
那么有没有一款工具不需要远程连接,也不需要配置监控参数,同时也提供了丰富的性能监控数据呢?
阿里巴巴开源的性能分析神器 Arthas 应运而生。
Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪 Java 代码;实时监控 JVM 状态。Arthas 支持 JDK 6 +,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。当你遇到以下类似问题而束手无策时,Arthas 可以帮助你解决:
- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception
- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗
- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现
- 是否有一个全局视角来查看系统的运行状况
- 有什么办法可以监控到 JVM 的实时运行状态
- 怎么快速定位应用的热点,生成火焰图
# 安装与使用
在 Linux 的安装方式:如果速度较慢,可以尝试国内的码云 Gitee 下载。
wget https://io/arthas/arthas-boot.jar
wget https://arthas/gitee/io/arthas-boot.jar
2
Arthas 只是一个 java 程序,所以可以直接用 java -jar
运行。
执行成功后,arthas 提供了一种命令行方式的交互方式,arthas 会检测当前服务器上的 Java 进程,并将进程列表展示出来,用户输入对应的编号(1、2、3、4 ...)进行选择,然后回车。
比如:
- 方式一:
java -jar arthas-boot.jar
- 方式二:运行时选择 Java 进程 PID(先使用
jps
指令),再java -jar arthas-boot.jar [PID]
最后一行 [arthas@5826]$,说明打开进入了监控客户端,在这里就可以执行相关命令进行查看了。
除了在命令行查看外,Arthas 目前还支持 Web Console。在成功启动连接进程之后就已经自动启动,可以直接访问 http://127.0.0.1:8563/
访问,页面上的操作模式和控制台完全一样。
两个实用指令:
查看日志:
cat -/logs/arthas/arthas.log
查看帮助:
java -jar arthas-boot.jar -h
工程目录
目录名 | 作用 |
---|---|
arthas-agent | 基于 JavaAgent 技术的代理 |
bin | 一些启动脚本 |
arthas-boot | Java 版本的一键安装启动脚本 |
arthas-client | telnet client 代码 |
arthas-common | 一些共用的工具类和枚举类 |
arthas-core | 核心库,各种 arthas 命令的交互和实现 |
arthas-demo | 示例代码 |
arthas-memorycompiler | 内存编译器代码 |
arthas-packaging | Maven 打包相关 |
arthas-site | arthas 站点 |
arthas-spy | 编织到目标类的各个切面 |
static | 静态资源 |
arthas-testcase | 测试 |
# 相关指令
指令源于官网,建议看官网,有更详细的介绍和 Demo。
基础指令
指令 | 作用 |
---|---|
quit/exit | 退出当前 Arthas客户端,其他 Arthas 客户端不受影响 |
stop/shutdown | 关闭 Arthas 服务端,所有 Arthas 客户端全部退出 |
help | 查看命令帮助信息 |
cat | 打印文件内容,和 linux 里的 cat 命令类似 |
echo | 打印参数,和 linux 里的 echo 命令类似 |
grep | 匹配查找,和 linux 里的 gep 命令类似 |
tee | 复制标隹输入到标准输出和指定的文件,和 linux 里的 tee 命令类似 |
pwd | 复制标隹输入到标准输出和指定的文件,和 linux 里的 tee 命令类似 |
cls | 清空当前屏幕区域 |
session | 查看当前会话的信息 |
reset | 重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类 |
version | 输出当前目标 Java 进程所加载的 Arthas 版本号 |
history | 打印命令历史 |
keymap | Arthas 快捷键列表及自定义快捷键 |
jvm 相关
指令 | 选项 | 作用 |
---|---|---|
dashboard | 当前系统的实时数据面板 | |
-i | 每间隔多少毫秒输出一次最新的数据面板 | |
-n | 一共打印多少次数据面板 | |
thread | 查看当前 JVM 的线程堆栈信息 | |
-b | 查看出现死锁的线程堆栈信息 | |
jvm | 查看当前 JVM 的信息 | |
sysprop | 查看和修改 JVM 的系统属性 | |
sysem | 查看 JVM 的环境变量 | |
vmoption | 查看和修改 JVM 里诊断相关的 option | |
perfcounter | 查看当前 JVM 的 Perf Counter 信息 | |
logger | 查看和修改 logger | |
getstatic | 查看类的静态属性 | |
ognl | 执行 ognl 表达式 | |
mbean | 查看 Mbean 的信息 | |
heapdump | 类似 jmap 命令的 heap dump 功能 | |
--live | 生成存活的堆 dump 文件 |
class/classloader 相关
指令 | 选项 | 作用 |
---|---|---|
sc | 查看 JVM 已加载的类信息 | |
-d | 输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的 Classloader 等详细信息。如果一个类被多个 Classloader 所加载,则会出现多次 | |
-E | 开启正则表达式匹配,默认为通配符匹配 | |
-f | 输出当前类的成员变量信息(需要配合参数 -d 一起使用) | |
-X | 指定输出静态变量时属性的遍历深度,默认为 0,即直接使用 toString 输出 | |
sm | 查看已加载类的方法信息 | |
-d | 展示每个方法的详细信息 | |
-E | 开启正则表达式匹配,默认为通配符匹配 | |
jad | 反编译指定已加载类的源码 | |
mc | 内存编译器,内存编译 .java 文件为 .class 文件 | |
retransform | 加载外部的 .class 文件,retransform 到 JVM 里 | |
redefine | 加载外部的 .class 文件,redefine 到 JVM 里 | |
dump | dump 已加载类的 byte code 到特定目录 | |
classloader | 查看 classloader 的继承树、urts、类加载信息,使用 classloader 去调 getResource | |
-t | 查看 classloader 的继承树 | |
-l | 按类加载实例查看统计信息 | |
-c | 用 classloader 对应的 hashcode 来查看对应的 Jar urls |
如:
sc -d cn.kele.jprofiler # 查看 jprofiler 的类信息
sm -d java.lang.String # 查看 String 的方法信息
jad java.lang.String # 查看 String 的源码(反编译)
mc /opt/HelloWorld.java # 编译成 HelloWorld.class 文件
retransform /opt/HelloWorld.class # 将 Class 文件放到 JVM 里
classloader # 查看 classloader 的继承树、urts、类加载信息
2
3
4
5
6
monitor/watch/trace 相关
指令 | 选项 | 作用 |
---|---|---|
monitor | 方法执行监控,调用次数、执行时间、失败率 | |
-c | 统计周期,默认值为 120 秒 | |
watch | 方法执行观测,能观察到的范围为:返回值、抛出异常、入参,通过编写 groovy 表达式进行对应变量的查看 | |
-b | 在方法调用之前观察(默认关闭) | |
-e | 在方法异常之后观察(默认关闭) | |
-s | 在方法返回之后观察(默认关闭) | |
-f | 在方法结束之后(正常返回和异常返回)观察(默认开启) | |
-x | 指定输岀结果的属性遍历深度,默认为 0 | |
trace | 方法内部调用路径,并输出方法路径上的每个节点上耗时 | |
执行次数限制 | ||
stack | 输出当前方法被调用的调用路径 | |
tt | 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测 |
如:
monitor -c 5 cn.kbt.java.Picture <init> # 监控 init 构造器,每隔 5 秒输出一次信息
watch cn.kbt.java.Picture <init>
2
其他
指令 | 作用 |
---|---|
jobs | 列出所有 job |
kill | 强制终止任务 |
fg | 将暂停的任务拉到前台执行 |
bg | 将暂停的任务放到后台执行 |
grep | 搜索满足条件的结果 |
plaintext | 将命令的结果去除 ANSI 颜色 |
wc | 按行统计输出结果 |
options | 查看或设置 Arthas 全局开关 |
profiler | 使用 async -profiler 对应用采样,生成火焰图 |
# Java Misssion Control
官方地址:
https://github.com/JDKMissionControl/jmc
在 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 中发布。
自 Java11 开始,本节介绍的 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
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),即每隔一段时问统计各个线程的栈轨迹。如果在这些抽样取得的栈轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法
# 其他工具
# Flame Graphs(火焰图)
在追求极致性能的场景下,了解你的程序运行过程中 CPU 在干什么很重要,火焰图就是一种非常直观的展示 CPU 在程序整个生命周期过程中时间分配的工具。火焰图对于现代的程序员不应该陌生,这个工具可以非常直观的显示出调用找中的 CPU 消耗瓶颈。
网上的关于 Java 火焰图的讲解大部分来自于 Brenden Gregg 的博客:http://new.brendangregg.com/flamegraphs.html
。
火焰图,简单通过 x 轴横条宽度来度量时间指标,y 轴代表线程栈的层次。
# Tprofiler
官方地址:
http://github.com/alibaba/Tprofiler
案例: 使用 JDK 自身提供的工具进行 JVM 调优可以将下 TPS 由 2.5 提升到 20(提升了 7 倍),并准确 定位系统瓶颈。
系统瓶颈有:应用里释态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。
那么,如何在海量业务代码里边准确定位这些性能代码?这里使用阿里开源工具 Tprofiler 来定位 这些性能代码,成功解决掉了 GC 过于频繁的性能瓶预,并最终在上次优化的基础上将 TPS 再提升了 4 倍,即提升到 100。
- Tprofiler 配置部署、远程操作、 日志阅谈都不太复杂,操作还是很简单的。但是其却是能够 起到一针见血、立竿见影的效果,帮我们解决了 GC 过于频繁的性能瓶预。
- Tprofiler 最重要的特性就是能够统汁出你指定时间段内 JVM 的 top method 这些 top method 极有可能就是造成你 JVM 性能瓶颈的元凶。这是其他大多数 JVM 调优工具所不具备的,包括 JRockit Mission Control。JRokit 首席开发者 Marcus Hirt 在其私人博客《 Lom Overhead Method Profiling cith Java Mission Control》下的评论中曾明确指出 JRMC 井不支持 TOP 方法的统计。
# Btrace
常见的动态追踪工具有 BTrace、HouseHD(该项目己经停止开发)、Greys-Anatomy(国人开发 个人开发者)、Byteman(JBoss 出品),注意 Java 运行时追踪工具井不限干这几种,但是这几个是相对比较常用的。
BTrace 是 SUN Kenai 云计算开发平台下的一个开源项目,旨在为 java 提供安全可靠的动态跟踪分析工具。先看一卜日 Trace 的官方定义:
大概意思是一个 Java 平台的安全的动态追踪工具,可以用来动态地追踪一个运行的 Java 程序。BTrace 动态调整目标应用程序的类以注入跟踪代码(「字节码跟踪「)。
其他工具
YourKit
JProbe
Spring Insight