一种以 非强行/入侵 方式 收集/查看 应用运营性能数据的活动。
监控通常是指一种在生产、质量评估、开发环境下实施的带有 预防 或 主动性 的活动。
当应用相关干系人提出性能问题却 没有提供足够多的线索 时,首先我们需要进行性能监控,随后是性能分析。
常见性能问题:
一种以 侵入方式 收集运行性能数据的活动,它会影响应用的吞吐量或响应性。
性能分析是针对性能问题的答复结果,关注的范围通常比性能监控更加集中。
性能分析很少在生产环境下进行,通常是在质量评估、系统测试/开发环境 下进行,是监控之后的步骤。
常用分析手段:
一种为改善应用响应性或吞吐量而更改参数、源代码、属性配置的活动,性能调优是在性能监控、性能分析之后的活动。
常见调优策略:
提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间常用操作的响应时间列表:
操作 | 响应时间 |
---|---|
打开一个站点 | 几秒 |
数据库查询一条记录(有索引) | 十几毫秒 |
机械磁盘一次寻址定位 | 4毫秒 |
从机械磁盘顺序读取1M数据 | 2毫秒 |
从SSD硬盘顺序读取1M数据 | 0.3毫秒 |
从远程分布式换成Redis读取一个数据 | 0.5毫秒 |
从内存读取1M数据 | 十几微秒 |
Java程序本地方法调用 | 几微秒 |
网络传输2KB数据 | 1微秒 |
在垃圾回收环节中:
-XX:MaxGCPauseMillis
对单位时间内完成的工作量(请求)的量度。
在GC中:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间 + 内存回收的时间)
吞吐量 = 1 - 1 / (1 + n),其中-XX:GCTimeRatio=n
同一时刻,对服务器有实际交互的请求数。
Java堆区所占用的内存大小
以高速公路通行状况为例:
- 吞吐量:每天通过高速公路收费站的车辆数(或收费站收取的高速费用)
- 并发数:高速公路上正在行驶的车辆数
- 响应时间:车速
性能诊断是软件工程师日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来非常大的收益。
Java作为最流行的编程语言之一,其应用性能诊断一直受到业界广泛关注。可能造成Java应用出现性能问题的因素非常多,例如:线程控制、磁盘读写、数据库访问、网络I/O、垃圾收集等。想要定位这些问题,一款优秀的性能诊断工具必不可少。
在我们刚接触Java学习时,最先了解的两个命令是java
、javac
,那么除此之外,还有没有其他的命令可以供我们使用呢?在JDK的bin目录,还有一系列辅助工具。这些辅助工具用来获取目标JVM的不同方面、不同层次的信息,帮助开发人员更好地解决Java应用的一些疑难杂症。
Open JDK 11 源码:
https://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/jdk.jcmd/share/classes/sun/tools
jps (Java Process Status)
显示指定系统内所有的HotSpot虚拟机进程 (查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。
说明:对于本地虚拟机进程而言,进程的本地虚拟机ID与操作系统的进程ID是一致的。
public class ScannerTest {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String info = sc.nextLine(); // 阻塞
}
}
它的基本语法为:
jps [options] [hostid]
我们还可以通过追加参数,来打印额外的信息。
-q
:仅仅显示LVMID(local virtual machine id),即本地虚拟机唯一ID,不显示主类名称等。
-l
:输出程序的主类的全类名 或 如果进程执行的是jar包,则输出jar完整路径。
-m
:输出进程启动时传递给主类main()的参数。
-v
:列出虚拟机进程启动时的JVM参数。比如:-Xms20m -Xmx50m 是启动程序指定的jvm参数。
说明:以上参数可以综合使用。
补充:如果某Java进程关闭了默认开启的UserPerfData参数(即使用参数-XX:-UseParfData
),那么jps命令(以及下面介绍的jstat
)将无法探知该Java进程。
RMI注册表中注册的主机名。
如果想要远程监控主机上的Java程序,需要安装jstatd
。
对于具有更严格的安全实践的网络场所而言,可能使用一个自定义的策略文件来显示对特定的可信主机或网络的访问,尽管 这种技术容易收到IP地址欺诈攻击。
如果安全问题无法使用一个定制的策略文件来处理,那么最安全的操作是不运行jstatd
服务器,而是在本地使用jstat
和jps
工具。
jstat (JVM Statistics Monitoring Tool):用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地/远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题、内存泄漏问题。
官方文档:
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
它的基本语法为:
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
查看命令相关参数:
jstat -h
或jstat -help
选项option可以由以下值构成。
类装载相关:
-class
:显示ClassLoader的相关信息:类的装载/卸载数量、总空间、类装载所消耗的时间等
表头信息 | 解释说明 |
---|---|
Loaded | 装载类的数量 |
Bytes(Loaded) | 装载类所占用的字节数 |
Unloaded | 卸载类的数量 |
Bytes(Unloaded) | 卸载类所占用的字节数 |
Time | 装载和卸载类所花费的时间 |
垃圾回收相关:
-gc
:显示与GC相关的对信息。包括Eden区、两个Survivor区、老年代、方法区等的容量、已用空间、GC时间合计等信息。-gccapacity
:显示内容与-gc
基本相同,单输出主要关注Java堆各个区域使用的最大、最小空间。-gcutil
:显示内容与-gc
基本相同,但输出主要关注已使用空间占总空间的百分比。gccause
:与-gcutil
功能一样,但是会额外输出导致最后一次或当前正在发生的GC产生的原因。-gcnew
:显示新生代GC状况。-gcnewcapacity
:显示内容与-gcnew
基本相同,输出主要关注使用到的最大、最小空间。-gcold
:显示老年代GC状况。-gcoldcapacity
:显示内容与-gcold
基本相同,输出主要关注使用到的最大、最小空间。-gcpermcapacity
:显示永久代使用到的最大、最小空间。表头信息 | 解释说明 |
---|---|
S0C / S1C | 新生代中 第一个 / 第二个 survivor(幸存区)的容量 (字节) |
S0U / S1U | 新生代中 第一个 / 第二个 survivor(幸存区)目前已使用空间 (字节) |
EC | 新生代中Eden(伊甸园)的容量 (字节) |
EU | 新生代中Eden(伊甸园)目前已使用空间 (字节) |
OC | 老年代的容量 (字节) |
OU | 老年代目前已使用空间 (字节) |
PC | 永久代的容量 (字节) |
PU | 永久代目前已使用空间 (字节) |
YGC | 从应用程序启动到采样时年轻代中gc次数 |
YGCT | 从应用程序启动到采样时年轻代中gc所用时间(s) |
FGC | 从应用程序启动到采样时old代(全gc)gc次数 |
FGCT | 从应用程序启动到采样时old代(全gc)gc所用时间(s) |
GCT | 从应用程序启动到采样时gc用的总时间(s) |
TT | 持有次数限制 |
MTT | 最大持有次数限制 |
NGCMN | 新生代(young)中初始化(最小)的大小(字节) |
NGCMX | 新生代(young)的最大容量 (字节) |
NGC | 新生代(young)中当前的容量 (字节) |
OGCMN | 老年代中初始化(最小)的大小 (字节) |
OGCMX | 老年代的最大容量(字节) |
OGC | 老年代当前新生成的容量 (字节) |
PGCMN | 永久代中初始化(最小)的大小 (字节) |
PGCMX | 永久代的最大容量 (字节) |
PGC | 永久代当前新生成的容量 (字节) |
S0 / S1 | 年轻代中 第一个 / 第二个 survivor(幸存区)已使用的占当前容量百分比 |
E | 年轻代中Eden(伊甸园)已使用的占当前容量百分比 |
O | 老年代已使用的占当前容量百分比 |
P | 永久代已使用的占当前容量百分比 |
JIT相关:
-compiler
:输出 编译方法、耗时等信息。
表头信息 | 解释说明 |
---|---|
Compiled | 编译任务执行数量 |
Failed | 编译任务执行失败数量 |
Invalid | 编译任务执行失效数量 |
Time | 编译任务消耗时间 |
FailedType | 最后一个编译失败任务的类型 |
FailedMethod | 最后一个编译失败任务所在的类及方法 |
-printcompilation
:输出 编译任务数目、方法生成的字节码大小、编译类型、已经被编译的方法等。
表头信息 | 解释说明 |
---|---|
Compiled | 编译任务的数目 |
Size | 方法生成的字节码的大小 |
Type | 编译类型 |
Method | 类名和方法名用来标识编译的方法。类名使用/做为一个命名空间分隔符。 |
用于指定输出统计数据的周期,即:查询间隔。单位:毫秒(ms)
用于指定查询的总次数。
可以在输出信息前加上一个Timestamp列,显示程序的运行时间。单位:秒(s)
经验:
我们可以比较Java进程的启动时间以及总GC时间(GCT列),或者两次测量的间隔时间以及总GC时间的增量,来得出GC占运行时间的比例。
如果该比例超过20%,则说明目前堆的压力比较大;如果该比例超过90%,则说明堆里几乎没有可用空间,随时都可能抛出OOM异常。
可以在周期性数据输出时,输出多少行数据后输出一个表头信息。
jstat还可以用来判断是否出现内存泄漏。
jstat
命令连续获取多行性能数据,并取这几行数据中 OU 列(即已占用的老年代内存)的最小值。jinfo(Configuration Info for Java)
查看虚拟机配置参数信息,也可以用于调整虚拟机的配置参数。
在很多情况下,Java程序不会指定所有的Java虚拟机参数。而开发人员可能不知道某一个具体的Java虚拟机参数的默认值。此时,可能需要查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了jinfo
工具,开发人员就可以很方便地找到Java虚拟机参数的当前值。
官方文档:
https://docs.oracle.com/en/java/javase/11/tools/jinfo.html
它的基本语法为:
jinfo [options] pid
说明:Java 进程ID (pid) 必须要加上
[options]:
选项 | 选项说明 |
---|---|
no option | 输出全部的参数和系统属性 |
-flag name | 输出对应名称的参数 |
-flag [+|-]name | 开启/关闭对应名称的参数, 只有被标记为manageable的参数才可以被动态修改 |
-flag name=value | 设定对应名称的参数 |
-flags | 输出全部的参数 |
-sysprops | 输出系统属性 |
jinfo -sysprops PID
jinfo -flags PID
jinfo -flag 具体参数 PID
jinfo
不仅可以查看运行时某一个Java虚拟机参数的实际取值,甚至可以在运行时修改部分参数,并使之立即生效。
但是,并非所有参数都支持动态修改。参数只有被标记为manageable的flag可以被实时修改。这个修改能力极其有限。
查看被标记为manageable的参数:(grep命令仅限Linux)
java -XX:+PrintFlagsFinal -version | grep manageable
针对boolean类型
jinfo -flag [+|-]具体参数 PID
针对非boolean类型
jinfo -flag 具体参数=具体参数值 PID
查看所有JVM启动的初始值
java -XX:+PrintFlagsInitial
查看所有JVM参数的最终值
java -XX:+PrintFlagsFinal
查看那些已经被用户或者JVM设置过的详细的XX参数的名称和值
java -XX:+PrintCommandLineFlags
jmap(JVM Memory Map)
作用:
开发人员可以在控制台输入命令jamp -help
查阅jmap工具的具体使用方式和一些标准选项配置。
官方文档:
https://docs.oracle.com/en/java/javase/11/tools/jmap.html
它的基本语法为:
jmap [option] <pid>
jmap [option] <executable <core>
jmap [option] [server_id@]<remote server IP or hostname>
[option]:
选项 | 选项说明 | 备注 |
---|---|---|
-dump | 生成堆转储快照:dump文件 | -dump:live 只保存堆中的存活对象 |
-heap | 输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等 | |
–histo | 输出整个堆空间的详细信息,包括GC的使用、堆配置信息,以及内存的使用信息等 | -histo:live 只统计堆中的存活对象 |
-permstat | 以ClassLoader为统计口径输出永久代的内存状态信息 | 仅linux/solaris平台有效 |
-finalizerinfo | 显示在F-Queue中等待Finalizer线程执行finalizer方法的对象 | 仅linux/solaris平台有效 |
-F | 当虚拟机进程对-dump选项没有任何响应时,可使用此选项强制执行生成dump文件 | 仅linux/solaris平台有效 |
-help | -h | jmap工具使用的帮助命令 | |
-J |
传递参数给jmap启动的JVM |
说明:这些参数和linux下输出显示的命令多少会有些不同,包括也受JDK版本的影响。
一般来说,使用jmap
指令生成dump文件的操作算得上是最常用的jmap
命令之一,将堆中所有存活对象导出至一个文件之中。
Heap Dump 又叫做堆存储文件,指一个Java进程在某个时间点的内存快照。Heap Dump 在触发内存快照的时候会保存此刻的信息如下:
All Objects
Class,files,primitive values and fields
All Classes
ClassLoader,name,super class,static fields
Garbage Collection Roots
Objects defined to be reachable by the JVM
Thread Stacks and Local Variables
The call-stacks of threads at the moment of the snapshot, and per-frame information about local objects
说明:
jmap -dump:format=b,file=<filename.hprof> <pid>
jmap -dump:live,format=b,file=<filename.hprof> <pid>
当程序发生OOM退出系统时,一些瞬时信息随着程序的终止而消失,而重视OOM问题往往比较困难或耗时。此时若能在OOM时,自动导出dump文件就显得非常迫切。
这里介绍一种比较常见的取得对快照文件的方法:
-XX:+HeapDumpOnOutOfMemoryError
:在程序发生OOM时,导出应用程序的当前堆快照。-XX:HeapDumpPath=
:课可以指定堆快照的保存位置。-XX:+HeapDumpOnOutOfMemeryError
-XX:HeapDumpPath=<filename.hprof>
比如:
-Xmx100m -XX:+HeapDumpOnOutOfMemery -XX:HeapDumpPath=D:\m.hprof
jmap -head
:查看Heap空间比例分配、使用情况jmap -histo
:查看Heap中各类型→实例数目、实例所占空间情况jmap -head <pid>
jmap -histo <pid>
查看系统的ClassLoader信息
jmap -permstat pid
查看堆积在finalizer队列中的对象
jmap -finalizerinfo pid
由于jmap
将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap
需要借助安全点机制,让所有线程停留在不改变堆中数据的状态。也就是说,由jmap
导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。
举例:假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么:live
选项将无法探知到这些对象。
另外,如果某个线程长时间无法跑到安全点,jmap
将一直等下去。与前面讲的jstat
则不同,垃圾回收器会主动将jstat
所需要的摘要数据保存至固定位置之中,而jstat
只需直接读取即可。
jhat(JVM Heap Analysis Tool)
Sun JDK 提供的jhat
命令与jmap
命令搭配使用,用于分析jmap
生成的heap dump文件(堆转储快照)。jhat
内置了一个微型的HTTP/HTML
服务器,生成dump文件的分析结果后,用户可以在浏览器中查看分析结果(分析虚拟机转储快照信息)。
使用了jhat
命令,就启动了一个http服务,端口为7000,即http://localhost:7000/
,就可以在浏览器里分析。
说明:jhat
命令在JDK9、JDK10中已经被删除,官方建议用 JVisualVM 代替。
它的基本语法是:
jhat [option] [dumpfile]
查看localhost:7000
:
OQL语句查询对象:
[option]:
选项 | 选项说明 |
---|---|
-stack false|true | 关闭|打开对象分配调用栈跟踪 |
-refs false|true | 关闭|打开对象引用跟踪 |
-port port-number | 设置jhat HTTP Server的端口号 (默认7000) |
-exclude exclude-file | 执行对象查询时需要排除的数据成员 |
-baseline exclude-file | 指定一个基准堆转储 |
-debug int | 设置debug级别 |
-version | 启动后显示版本信息就退出 |
-J |
传入启动参数, 如 -J -Xmx512m |
jstack(JVM Stack Trace)
用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。线程快照就是当前虚拟机指定进程的每一条线程正在执行的方法堆栈的集合。
生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如:线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack
显示各个线程调用的对栈情况。
官方文档:
https://docs.oracle.com/en/java/javase/11/tools/jstack.html
在 thread dump 中,要留意下面几种状态:
它的基本语法为:
jstack [option] <pid>
[option]:
选项 | 选项说明 |
---|---|
-F | 当正常输出的请求不被响应时,强制输出线程堆栈 |
-l | 除堆栈外, 显示关于锁的附加信息 |
-m | 调用到本地方法时,可以显示C/C++的堆栈 |
-h | 帮助操作 |
jstack管理远程进程时,需要在远程程序的启动参数中添加:
-Djava.rmi.server.hostname=...
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=8888
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
举例:
死锁/阻塞:
public class ThreadDeadLock {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
时段等待(睡眠):
public class ThreadSleepTest {
public static void main(String[] args) {
System.out.println("hello - 1");
try {
Thread.sleep(1000 * 60 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello - 2");
}
}
同步问题:
public class ThreadSyncTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class Number implements Runnable {
private int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
if(number <= 100) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
} else {
break;
}
}
}
}
}
获取Thread状态信息——从Java层面简单监控:
public class AllStackTrace {
public static void main(String[] args) {
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());
}
}
}
}
在JDK 1.7 以后,新增了一个命令行工具jcmd
。
它是一个多功能的工具,可以用来实现前面除了jstat
之外所有命令的功能。
比如:用它来导出堆、内存使用、查看Java进程、导出线程信息、执行GC、JVM运行时间等。
官方文档:
https://docs.oracle.com/en/java/javase/11/tools/jcmd.html
jcmd
拥有jmap
的大部分功能,并且在Oracle官方网站上也推荐使用jcmd
命令代替jmap
命令。
列出所有的JVM进程
jcmd -l
针对指定的进程,列出支持的所有命令
jcmd <pid> help
常见支持命令:
# Thefollowingcommandsareavailable:
JFR.stop
JFR.start
JFR.dump
JFR.check
Thread.print
ManagementAgent.stop
ManagementAgent.start_local
ManagementAgent.start
GC.rotate_log
GC.class_stats
GC.class_histogram
GC.heap_dump
GC.finalizer_info
GC.heap_info
GC.run_finalization
GC.run
VM.classloader_stats
VM.native_memory
VM.check_commercial_features
VM.unlock_commercial_features
VM.uptime
VM.dynlibs
VM.flags
VM.system_properties
VM.command_line
VM.version
help
显示指定进程的指令命令的数据
jcmd <pid> 具体命令
举例(无法 ”间隔时间无限输出“):
# 类比:jstack
jcmd <pid> Thread.print
# 类比:jmap -histo
jcmd <pid> GC.class_histogram
# 类比:jmap -dump
jcmd <pid> GC.heap_dump <filepath/filename.hprof>
之前的指令只涉及到监控本机的Java应用程序,而在这些工具中,一些监控工具也支持对远程计算机的监控(如jps
、jstat
)。为了启动远程监控,则需要配合使用jstatd
工具。
命令jstatd
是一个RMI服务端程序,它的作用相当于代理服务器,建立本地计算机与远程监控工具的通信。jstatd
服务器将本机的Java应用程序信息传递到远程计算机。
使用上一章的命令行工具或组合能帮助我们获取目标Java应用性能相关的基础信息,但它们存在下列局限:
为此,JDK提供了一些内存泄漏的分析工具,如jconsole、jvisualvm等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求。所以这里我们介绍的工具相对多一些、丰富一些。
图形化综合诊断工具
jconsole:
官方介绍:
https://docs.oracle.com/javase/7/docs/technotes/tools/share/jconsole.html
官方教程:
https://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html
jdk/bin目录下,启动jconsole.exe即可
不需要使用jps命令查询
使用JConsole丽娜姐一个正在本地系统运行的JVM,并且需要执行程序、运行JConsole为同一个用户。JConsole使用文件系统的授权通过RMI连接器连接到平台的MBean服务器上,这种从本地连接的监控能力只有Sun的JDK具有。
使用下面的URL通过RMI连接到一个JMX代理,service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi
。JConsole为建立连接,需要在环境变量中设置mx.remote.credentials
来指定用户名和密码,从而进行授权。
使用一个特殊的URL连接JMX代理。一般情况使用自己定制的连接器而非RMI提供的连接器来连接JMX代理,或是一个使用JDK1.4的实现了JMX和JMX Romote的应用。
测试代码:
/**
* -Xms600m -Xmx600m -XX:SurvivorRatio=8
*/
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 100)];
public static void main(String[] args) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ArrayList<HeapInstanceTest> list = new ArrayList<>();
while (true) {
list.add(new HeapInstanceTest());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
概述图:
死锁检测:
JDK/bin
目录下),完全免费。*.nbm
,然后再Plugin对话框的已下载界面下,添加已下载的插件。也可以再可用插件页面下,在线安装插件(建议安装:VisualGC)
本地安装插件:
IDEA启动插件安装:
配置相关路径:
监控本地Java进程的CPU、类、线程等
bin/catalina.sh
文件,来凝结远程的Tomcat.../conf
中添加jmxremote.access
和jmxremote.password
文件Heap-Dump
生成Heap-Dump文件:
读取Heap-Dump文件:
Thread-Dump
生成Thread-Dump文件:
读取Thread-Dump文件:
CPU抽样:
内存抽样:
MAT(Memory Analyzer Tool)工具是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。
MAT是基于Eclipse开发的,不仅可以单独使用,还可以作为插件的形式嵌入Eclipse中使用。作为一款免费的性能分析工具,使用非常方便。
下载地址:
https://www.eclipse.org/mat/download.php
只要确保机器上装有JDK并配置好相关的环境变量,MAT可正常启动。
还可以在Eclipse中以插件形式安装。
MAT可以分析heap dump文件。在进行内润分析时,只要获得了反映当前设备内存映像的hprof文件,通过MAT打开就可以直观地看到当前的内存信息。
一般说来,这些内存信息包含:
说明1:
缺点:MAT不是一个万能的工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如:Sun, HP, SAP 所采用的 HPROF 二进制堆存储文件,以及 IBM 的 PHD 堆存储文件都能被很好的解析。
说明2:
最吸引人的还是能快速为开发人员生成 内存泄漏报表,方便定位问题、分析问题。虽然MAT有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从MAT展现给我们的信息当中通过经验和直觉来判断才能发现。
**方式一:**通过jmap
工具生成,可以生成任意一个Java进程的dump文件;
**方式二:**通过配置JVM参数生成
-XX:+HeapDumpOnOutOfMemoryError
或-XX:+HeapDumpBeforeFullGC
-XX:HeapDumpPath
所代表的含义就是当程序出现OutOfMemoryError
时,将会在相应的目录下生成一份dump文件。如果不指定选项-XX:HeapDumpPath
则在当前目录下生成dump文件。对比:考虑到生产环境中几乎不可能在线对其进行分析,大都是采用离线分析,因此使用 jmap+MAT 工具是最常见的组合。
**方法三:**使用Visual VM可以导出堆dump文件。
**方法四:**使用MAT既可以打开一个已有的堆快照,也可以通过MAT直接从 活动的Java程序 中导出堆快照。该功能将借助jps列出正在运行的Java进程,以供选择并获取快照。
展示了各个类的实例数目以及这些实例的 Shallow Heap 或 Retained Heap 的总和。
两不同时刻dump文件作比较:
查看系统中的Java线程
查看局部变量的信息
长周期对象引用短周期对象,导致短周期对象无法被GC,则将此短周期对象视为内存泄漏。
以String为例:2个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占据24字节(JDK 7)
这24字节为String对象的浅堆大小。它与String的value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。
注意:浅堆指对象本身占有的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的(直接/间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间。
此规则适用于字符串常量池(常量池中常量若仅被1个字符串引用持有, 则计入深堆, 当数量多于1时不计入)
另外一个常用的概念是对象的实际大小。这里,对象的实际大小定义为一个对象 所能触及的 所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小。与深堆相比,似乎这个在日常开发中更为直观和被人接受,但实际上,这个概念与垃圾回收无关。
下图显示了一个简单的对象引用关系图,对象A引用了C和D,对象B引用了C和E。那么对象A的浅堆大小只是A本身,不包含C和D,而A的实际大小为A、C、D之和。但A的深堆大小为A与D之和,由于对象C还可以被对象B访问到,因此不在对象A的深堆范围内。
/**
* -XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=D:\student.hprof
*/
public class StudentTrace {
static List<WebPage> webPages = new ArrayList<>();
public static void createWebPages() {
for (int i = 0; i < 100; i++) {
WebPage wp = new WebPage();
wp.setUrl("http://www." + 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) {
this.id = id;
this.name = name;
}
// 省略setter、getter方法...
public void visit(WebPage wp) {
if (wp != null) {
history.add(wp);
}
}
}
class WebPage {
private String url;
private String content;
// 省略setter、getter方法...
}
考虑Lily同学(stu7):
15个WebPage,每个对应152字节,合计15 * 152 = 2280字节(elementData的实际大小)
关于elementData深堆大小如何计算得出?
能被7整除,且能被3整除;以及能被7整除,且能被5整除的数值有:0, 21, 42, 63, 84, 35, 70,共7个数,合计7 * 152 = 1064字节。
2280 - 1064 + 72 = 1216 + 72 = 1288 (字节)
支配树(Dominator Tree)
支配树的概念源自图论。
MAT提供了一个称为支配树(Dominator Tree)的对象图。支配树体现了对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为 对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的 直接支配者。支配树是基于对象间的引用图所建立的,它有以下基本性质:
如下图所示:
同理,对象E支配对象G。到达对象H的可以通过对象D,也可以通过对象E,因此对象D和E都不能支配对象H,而经过对象C既可以到达D也可以达到E,因此C为对象H的直接支配者。
在MAT中,单击工具栏上的对象支配树按钮,可以打开对象支配树视图。
下图显示了对象支配树视图的一部分。该截图显示部分Lily学生的history队列的直接支配对象。即当Lilly对象被回收,也会一并回收的所有对象。显然能被3或5整除的网页不会出现在该列表中,因为它们同时被另外两名学生对象引用。
Tomcat是最常用的Java Servlet容器之一,同时也可以当作单独的Web服务器使用。Tomcat本身使用Java实现,并运行于Java虚拟机之上。在大规模请求时,Tomcat有可能会因为无法承受压力而发生内存溢出错误。这里根据一个被压垮的Tomcat堆快照文件,分析Tomcat在崩溃时的内部情况。
session对象,它占用了约17MB空间。
可以看到session对象为ConcurrentHashMap,其内部分为16个Segment。从深堆大小看,每个Segment都比较平均,大约为1MB,合计17MB。
当前堆中有9941个session对象,并且每个session的深堆为1592字节,合计约15MB,达到当前堆大小的50%。
根据当前session总数,可以计算每秒的平均压力为:9941/(1403324677648-1403324645728)*1000=311(次/秒)。
由此推断,在发生Tomcat堆溢出时,Tomcat在连续30秒的时间内,平均每秒接受了约311次不同客户端的请求,创建了合计9941个session。
可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)
发生内存泄漏:
- 是否还被使用? 是
- 是否还被需要? 否
严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一些不太好的实践(疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做 宽泛意义上的 “内存泄漏”。
对象X引用对象Y,X的生命周期比Y的生命周期长;
那么当Y生命周期结束的时候,X依然引用着Y,这时候,垃圾回收器是不会回收对象Y的。
如果对象X还引用着生命周期比较短的A、B、C,对象A又引用着对象a、b、c,这样就可能造成大量无用对象不能被回收,进而占据了内存资源,造成内存泄漏,直至内存溢出。
内存泄漏(Memory Leak)
内存溢出(Out Of Memory)
可见,内存泄漏和内存溢出的关系:内存泄漏的增多,最终会导致内存溢出。
**经常发生:**发生内存泄漏的代码会被多次执行,每次执行,泄漏一块内存。
**偶然发生:**在某些特定情况下才会发生。
**一次性:**发生内存泄漏的方法只会执行一次。
**隐式泄露:**一直占着内存不释放,直到执行结束;严格地说不算内存泄漏,因为最终释放了,但是如果执行时间极长,也可能导致内存耗尽。
静态集合类,如HashMap、LinkedList等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束前将不能被释放,从而造成内存泄漏。
简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
public class MemoryLeak {
static List list = new ArrayList();
public void oomTest() {
Object obj = new Object();
list.add(obj);
}
}
补充:缓存集合可使用WeakHashMap代替普通HashMap。
单例模式,与静态集合导致内存泄漏的原因类似,因为单例的静态特性,它的生命周期和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();
Result rs = stmt.executeQuery("...");
} catch (Exceptoin e) { //异常日志
} finally {
// 1、关闭结果集 Statement
// 2、关闭声明的对象 ResultSet
// 3、关闭连接 Connection
}
}
变量不合理的作用域。一般而言,一个变量定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能会导致内存泄漏。
public class UsingRandom {
private String msg;
public void receiveMsg() {
readFromNet(); // 从网络中接收数据保存到msg中
saveDB(); // 将msg保存到数据库中
}
}
上述伪代码中,通过readFromNet方法将接收的消息保存在变量msg中,然后调用savevDB方法把msg内容保存到数据库中,此时msg已失去效用,由于msg生命周期与对象生命周期相同,此时msg无法及时回收,造成内存泄漏。
实际上,msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也随之结束,此时就可以回收了。
还有一种方法,在使用完msg后,将msg置为null,这样垃圾回收器也会回收msg的内存空间,保留了对象中msg的引用,但需考虑并发问题。
改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了。
否则,对象修改后的哈希值与最初存储进HashSet集合中的哈希值不同。届时,即使在contains方法使用该对象的当前引用作为参数区HashSet集合中检索对象,也无法找到此对象,这样也会导致无法从HashSet集合中单独删除当前对象,造成内存泄漏。
这也是String为什么被设置为了不可变类型,我们可以放心地将String存入HashSet,或将String当作HashMap的key值。
当我们想将自己定义的类保存到HashSet时,需要保证对象的 hashCode 不可变。
举例1:
public class ChangeHashCode {
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
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 instanceof Point)) return false;
Point point = (Point) obj;
return x == point.x;
}
// 省略toString方法...
}
结果:
举例2:
public class ChangeHashCode1 {
public static void main(String[] args) {
HashSet<Person> 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")); // 无法正常进行去重判断(HashSet 误认为:先前的元素不存在)
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;
return id == person.id &&
name.equals(person.name);
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
// 省略toString方法...
}
结果:
内存泄漏的另一个常见来源是缓存,一旦将对象引用放入缓存中,很容易被遗忘。
举例:项目在一次上线时,启动奇慢直至卡住,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。
对于这个问题可以使用WeakHashMap替换HashMap作为缓存,此种Map的特点:当初了自身有对key的引用外,此key没有其他引用,则会自动丢弃此值。
测试代码:
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() {
// ref1~ref4存在本地方法表,方法运行结束,强引用消失(图中的“断开”)
String ref1 = new String("object1");
String ref2 = new String("object2");
String ref3 = new String("object3");
String ref4 = new String("object4");
// 元素置入Map
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 : map.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中的缓存元素被回收了。
内存泄漏还可能来源于监听器与其他回调,如果客户端在实现的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;
// }
public void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
分析
上述程序并没有明显错误,但是这段程序有一个内存泄漏,随着GC活动的增加,或内存占用的不断增加,程序性能的降低就会表现出来,严重时可导致内存泄漏,但是这种失败情况相对较少。
代码的主要问题在pop方法,下面通过这张图示展示:
假设这个栈一直增长,增长后如下图所示:
当进行大量pop操作时,由于引用未进行置空,GC是不会释放的,如下图所示:
从上图可以看出,如果栈先增长再收缩,那么从栈中弹出的对象将不会被当作垃圾回收,即使程序不再使用栈中的这些对象,它们也不会回收,因为栈中仍然保存着对象的引用,俗称 过期引用,这个内存泄漏很隐蔽。
解决方法
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
一旦引用过期,清空这些引用,将其置空。
MAT支持一种类似于SQL的查询语言OQL(Object Query Language)。OQL实用类SQL语法,可以在堆中进行对象的查找和筛选。
在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.ljw.demo.User
“DISTINCT” 关键字用于在结果集中去除重复对象。
SELECT DISTINCT OBJECTS classof(s) FROM java.lang.String s
From子句用于指定查询范围,它可以指定类名、正则表达式、对象地址。
SELECT * FROM java.lang.String s
使用正则表达式,限定搜索范围,输出所有com.ljw
包下所有类的实例:
SELECT * FROM "com\.ljw\..*"
也可以直接使用类的地址进行搜索。使用类的地址的好处是可以区分被不同ClassLoader加载的同一种类型。
SELECT * FROM 0x37a0b4d
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 AND v.@retainedHeapSize > 1000
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 INSTANTCEOF java.util.Vector
在运行Java时,有时想测试运行时内存占用内存情况,这时候就需要使用测试工具查看了。在eclipse里有 Eclipse Memory Analyzer Tool(MAT)插件可以测试,而在IDEA中也有这样一个插件,就是JProfiler。
JProfiler 是由 ej-technologices 公司开发的一款Java应用性能诊断工具。功能强大,但是收费。
官网下载地址:https://www.ej-technologies.com/products/jprofiler/overview.html
① 方法调用
② 内存分配
③ 线程&锁
④ 高级子系统
官网下载地址:https://www.ej-technologies.com/download/jprofiler/files
配置IDE:
**安装JProfiler插件: **
(也可手动下载 插件→本地安装)
配置JProfiler安装路径:
JProfiler数据采集方式分为两种 :Sampling(样本采集)和 Instrumentation(重构模式)
注:JProfiler本身没有指出数据的采集类型,这里的采集类型是针对方法调用的采集类型。因为JProfiler的绝大多数核心功能都依赖方法调用采集的数据,所以可以直接认为是JProfiler的数据采集类型。
遥感检测 Telemetries(查看JVM的运行信息)
Live Memory 内存剖析:class/class instance 的相关信息。
例如:对象的个数、大小,对象创建的方法执行栈,对象创建的热点。
所有对象 All Objects
显示所有加载的类的列表和在堆上分配的实例数。只有Java 1.5(JVMTI)才会显示此视图。
记录对象 Record Objects
查看特定时间段对象的分配,并记录分配的调用堆栈。
分配访问树 Allocation Call Tree
显示一棵请求树或方法、类、包或对已选择类有带注释的分配信息的J2EE组件。
分配热点 Allocation Hot Spots
显示一个列表,包括方法、类、包或分配已选类的J2EE组件。可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。
类追踪器 Class Tracker
类追踪视图可以包含任意数量的图表,显示选定的类和包的实例与时间。
分析:内存中的对象的情况
更改取样频次:
类 Classes
分配 Allocations
索引 References
时间 Time
检查 Inspections
图表 Graph
PS:在工具栏点击 “Go To Start” 可以使堆内存重新计数也就是回到初始状态。
查看Picture-incoming:
展示指向关系:
查看图表:
向前溯源:
JProfiler提供不同的方法来记录访问树以优化性能和细节。线程或者线程组状况可以被所有的视图
访问树 Call Tree
热点 Hot Spots
访问图 Call Graph
方法统计 Method Statistics
JProfiler通过对线程历史的监控 判断其运行状态,并监控是否有线程阻塞产生,还能将一个线程所管理的方法以树状形式呈现,对线程进行剖析。
线程历史 Thread History
线程监控 Thread Monitor
线程转储 Thread Dumps
线程分析主要关心三个方面:
Web容器的线程最大数
(比如:Tomcat的线程容量应该略大于最大并发数。)
线程阻塞
线程死锁
获取所有线程持有锁的情况以及锁的信息。
观察JVM的内部线程并查看状态:
死锁探测:
测试代码:
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];
private String info = "Hello,JProfiler!";
}
存在GC,情况较好:
测试代码:
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,JProfiler!";
static ArrayList list = new ArrayList(); // 核心问题代码
}
beanList
伴随着每次while循环结束而销毁,但Bean.list
为 static 类变量,并不会销毁,从而导致内存占用越来越大。
查看对象情况,发现charp[]类型数值增长迅猛:
JVM不断地在进行GC:
绿色框图代表堆大小,程序不断地向JVM申请堆动态扩容,但GC若干次,占用内存的大小始终在持续上升,增长迅猛:
通过Heap Walker 溯源,发现问题根源:
问题根源为Bean.list
static,可从此入手,对程序代码进行优化重构。
先前,我们介绍了JDK自带的JVisualVM等免费工具,以及商业化工具JProfiler。
这两款工具在业界知名度也比较高,它们的 优点:可以图形界面上看到各维度的性能数据,使用者根据这些数据进行综合分析,然后判断哪里出现了性能问题。
但是这两款工具也有 缺点:都必须在服务端项目进程中配置相关的监控参数。然后工具通过远程连接到项目进程,获取相关的数据。这样就会带来一些不便,比如线上环境的网络是隔离的,本地的监控工具根本连接不上线上环境。并且类似于JProfiler这样的商业工具,是需要付费的。
有这样一款工具,不需要远程连接,也不需要配置监控参数,同时也提供了丰富的性能监控数据:Alibaba 开源分析工具 Arthas(阿尔萨斯)
Arthas(阿尔萨斯)是Alibaba开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。
Arthas支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的Tab自动补全功能,进一步方便进行问题的定位和判断。
它可以解决以下问题:
官方使用文档:
https://arthas.aliyun.com/doc/
**安装方式以:**可以直接在Linux上通过命令下载
可以在官方 Github 上进行下载,如果速度较慢,可以尝试国内的码云 Gitee 下载。
Github 下载
wget https://arthas.github.io/arthas/arthas-boot.jar
Gitee 下载
wget https://arthas.gitee.io/arthas-boot.jar
安装方式二:
在浏览器直接访问 https://alibaba.github.io/arthas/arthas-boot.jar,等待下载成功后,上传到Linux服务器上。
arthas-agent: 基于JavaAgent技术的代理
bin: 一些启动脚本
arthas-boot: Java版本的一键安装启动脚本
arthas-client: telnet client代码
arthas-common: 一些共用的工具类和枚举类
arthas-core: 核心库,各种arthas命令的交互和实现
arthas-demo: 示例代码
arthas-memorycompiler: 内存编译器代码:Fork from https://github.com/skalogs/SkaETL/tree/master/compiler
arthas-packaging: maven打包相关资源
arthas-site: arthas站点
arthas-spy: 编制到目标类中的各个切面
static: 静态资源
arthas-testcase: 测试
Arthas 只是一个Java程序,所以可以直接用java -jar
运行。
执行成功后,arthas提供了一种命令行的交互方式,arthas会检测当前服务器上的Java进程,并将进程列表展示出来,用户输入对应的编号(1、2、3、4…)进行选择,然后回车。
方式一:
java -jar arthas-boot.jar
选择进程 (输入[]内编号 (不是PID) , 回车)
**方式二:**运行时选择Java进程PID
java -jar arthas-boot.jar [PID]
cat ~/logs/arthas/arthas.log
java -jar arthas-boot.jar -h
除了在命令行查看外,Arthas目前还支持 Web Console。在成功启动连接进程之后就已经自动启动,可以直接访问 http://127.0.0.1:8163/ 访问,页面上的操作模式和控制台完全一样。
最后一行 [arthas@7457]$
证明打开进入了监控客户端,在这里就可以执行相关命令进行查看了。
quit\exit
:退出当前客户端shop\shutdown
:关闭arthas服务器,并退出所有客户端help
:查看命令帮助信息cat
:打印文件内容(类比linux的cat命令)echo
:打印参数(类比linux的echo命令)grep
:匹配查找(类比linux的grep命令)tee
:复制标准输入到标准输出和指定的文件(类比linux的tee命令)pwd
:返回当前的工作目录(类比linux的tee命令)cls
:清空当前屏幕区域(类比linux的clear命令/win的cls命令)session
:查看当前会话的信息reset
:重置增强类,将被Arthas增强过的类全部还原,Arthas服务端关闭时重置所有增强过的类version
:输出当前目标Java进程所加载的Arthas的版本号history
:打印命令历史(类比linux的history命令)quit
:退出当前Arthas客户端,其他Arthas客户端不受影响stop
:关闭Arthas服务端,所有Arthas客户端全部退出keymap
:Arthas快捷键列表及自定义快捷键dashboard
:当前系统的实时数据面板
-i|-i -n
:指定采样时间间隔(如:dashboard -i 1000 -n 4
)thread
:查看当前JVM的线程堆栈信息
-b
:寻找阻塞线程-i|-i -n
:指定采样时间间隔-state
:查看指定线程状态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命令的head dump功能(文件扩展名:*.hprof
)
heapdump [filePath]
:dump到指定文件heapdump --live
:只dump live对象heapdump
:dump到临时文件sc
:查看JVM已加载的类信息
class-pattern
:类名表达式匹配-d
:输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的ClassLoader等详细信息。如果一个类被多个ClassLoader所加载,则会出现多次。-E
:开启正则表达式匹配,默认为通配符匹配-f
:输出当前类的成员变量信息(需要配合参数-d
一起使用)-x
:指定输出静态变量时属性的变量深度,默认为0,即直接使用toString输出补充:
- class-pattern支持全限定名,如com.test.ABC,也支持com/test/ABC这样的格式,这样,我们从异常堆栈将类名拷贝时就无需手动做分隔符替换了
- sc默认开启了子类匹配功能,也就是说当前类的子类也会被搜索出来,想要精确的匹配,需打开
options disable-sub-class true
开关
sm
:查看已加载类的方法信息(sm命令只能看到由当前类所声明 (declaring) 的方法,父类则无法看到)
class-pattern
:类名表达式匹配method-pattern
:方法名表达式匹配-d
:展示每个方法的详细信息-E
:开启正则表达式匹配,默认为通配符匹配jad
:反编译指定已加载类的源码
mc
:Memory Compiler/内存编译器,内存编译.java
文件为.class
文件
retransform
:加载外部的.class
文件,retransform到JVM里
redefine
:加载外部的.class
文件,redefine到JVM里(推荐使用 retransform
命令)
dump
:dump 已加载类的 byte code 到特定目录
classloader
:查看classloader的继承树,urls,类加载信息(了解当前系统中有多少类加载器,以及每个类加载器加载的类数量,协助判断是否有类加载器泄漏)
-t
:查看ClassLoader的继承树-l
:按类加载实例查看统计信息-c
:用classloader对应的hashcode 来查看对应的jar urls这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测。
因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行
stop
或将增强过的类执行reset
命令。
monitor
:方法执行监控【非实时返回命令】
对匹配class-pattern/method-pattern的类、方法的调用进行监控,涉及方法的调用次数、执行时间、失败率等。
class-pattern
:类名表达式匹配method-pattern
:方法名表达式匹配-c
:统计周期,默认值120秒监控项 | 说明 |
---|---|
timestamp | 时间戳 |
class | Java类 |
method | 方法(构造方法、普通方法) |
total | 调用次数 |
success | 成功次数 |
fail | 失败次数 |
rt | 平均RT |
fail-rate | 失败率 |
watch
:方法执行数据观测
观察指定方法的调用情况。能观察到的范围:返回值、抛出异常、入参,通过编写 groovy 表达式进行对应变量的查看。
class-pattern
:类名表达式匹配method-pattern
:方法名表达式匹配express
:观察表达式condition-express
:条件表达式-b
:在方法调用之前观察 (默认关闭)-e
:在方法异常之后观察 (默认关闭)-s
:在方法返回之后观察 (默认关闭)-f
:在方法结束之后 (正常结束&异常返回)观察 (默认开启)-x
:指定输出结果的属性遍历深度,默认为1#cost
:方法执行耗时说明:这里重点说明观察表达式,观察表达式 主要由ognl表达式组成,所以可以写{paramas, returnObj}
,只要是一个合法的ongl表达式,都能被正常支持。
trace
:方法内部调用路径,并输出方法路径上的每个节点上耗时。
trace
命令能主动搜索 class-pattern/method-pattern
对用的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。trace
能方便地帮助定位和发现因RT高而导致地性能问题缺陷,但其每次只能跟踪一级方法地调用链路。trace
在执行过程中本身是会有一定的性能开销,在统计的报告中并未向 JProfiler 一样预先减去其自身的开销。所以统计出来有些许不准,渲染路径上调用的类、方法越多,性能偏差越大。class-pattern
:类名表达式匹配method-pattern
:方法名表达式匹配condition-express
:条件表达式-n
:命令执行次数#cost
:方法执行耗时stack
:输出当前方法被调用的调用路径
class-pattern
:类名表达式匹配method-pattern
:方法名表达式匹配conditon-express
:条件表达式-n
:执行次数限制#cost
:方法执行耗时tt
:方法执行数据的时空隧道 (TimeTunnel),记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测。
-t
:表明希望记录下类 *Test 的 print 方法的每次执行情况。-n
:指定需要记录的次数,达到次数会主动中断tt
命令的记录过程。-s
:筛选指定方法的调用信息。-i
:参数后紧随 INDEX 编号查看到它的详细信息。-p
:重做一次调用,通过 --replay-times
指定调用次数,通过 --replay-interval
指定多次调用间隔 (单位ms,默认1000ms)profiler
:使用 async-profiler 对应用采样,生成火焰图
grep
:搜索满足条件的结果
plaintext
:将命令的结果去除ANSI颜色
wc
:按行统计输出结果
使用 >
将结果重写向到日志文件,使用 &
指定命令是后台运行,session断开不影响任务执行(生命周期默认为1天)
jobs
:列出所有job
kill
:强制终止任务
fg
:将暂停的任务拉到前台执行
bg
:将暂停的任务放到后台执行
在 Oracle 收购 Sun 之前,Oracle的 JRockit 虚拟机提供了一款叫做 JRockit Mission 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 Flight Recorder) 已经开源。但在之前的Java版本,JFR属于 Commercial Feature,需要通过Java 虚拟机参数-XX:+UnlockCommercialFeatures
开启。
官方Github-OpenJDK:
https://github.com/JDKMissionControl/jmc
Mission Control 位于 %JAVA_HOME%/bin/jmc.exe
,打开这款软件。
Java Mission Control(JMC),Java官方提供的性能强劲的工具。是一个对于Java应用程序进行管理、监视、概要分析和故障排除的工具套件。
它包含一个GUI客户端,以及众多用来收集Java虚拟机性能数据的插件,如 JMX Console(能够访问用来存放虚拟机各个子系统运行数据的MXBeans),以及虚拟机内置的高效profiling工具Java Flight Recorder(JFR)
JMC 的另一个优点就是:采用取样,而非传统的代码植入技术,对应用性能的影响非常非常小,完全可以开着JMC来做压测(唯一影响:Full GC 次数增多)
如果时远程服务器,需开启JMX。
-Dcom.sun.managment.jmxremote.port=${YOUR ROOT}
-Dcom.sun.managment.jmxremote
-Dcom.sun.managment.jmxremote.authenticate=false
-Dcom.sun.managment.jmxremote.ssl=false
-Djava.rmi.server.hostname=${YOUR HOST/IP}
概览界面:
触发器:达到设定条件报警
Java Flight Recorder 是 JMC 的其中一个组件。
Java Flight Recorder 能够以极低的性能开销收集 Java 虚拟机的性能数据。
JFR 的性能开销很小,在默认配置下平均低于1%,与其他工具相比,JFR能够直接访问虚拟机内的数据,并且不会影响虚拟机的优化。因此,它非擦黄给你适用于生产环境下满负荷运行的Java程序。
Java Flight Recorder 和 JDK Mission Control共同构建了一个完整的工具链。JDK Mission Control可对Java Flight Recorder连续收集低水平和详细的运行时信息进行高效、详细的分析。
当启动时,JFR将记录运行过程中发生的一系列事件。其中包括Java层面的事件,如线程事件、锁事件,以及Java虚拟机内部的事件,如新建对象、垃圾回、即时编译事件。
按照发生时机以及持续事件来划分,JFR的事件共有四种类型,它们分别为以下四种。
取样事件的其中一个常见的例子便是方法抽样(Method Sampling),即每隔一段时间统计各线程的栈轨迹。如果在这些抽样取得的轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法。
在运行目标Java程序中添加-XX:StartFlightRecord=参数
。
比如:下面命令中,JFR将会在Java虚拟机启动5s后(对应delay=5s)收集数据,持续20s(对应duration=20s)。当收集完毕后,JFR会将收集得到的数据保存至指定的文件中(对应filename=myrecording.jfr
)
java -XX:StartFlightRecorder=delay=5s,duration=20s, filename=myrecording.jfr,settings=profile MyApp
由于 JFR 将持续收集数据,如果不加以限制,那么JFR可能会填满硬盘的所有空间。因此,我们有必要对这种模式下所收集的数据进行限制。
java -XX:StartFlightRecording=maxage=10m,maxsize=100m,name=SomeLabel MyApp
通过jcmd
来让JFR开始收集数据、停止收集数据,或者保存所收集的数据,对应的子命令分别为JFR.start
、JFR.stop
、JFR.dump
。
jcmd <PID> JFR.strat 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
要采用取样,必须先添加参数:
-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder
取样时间默认为1分钟,可自行按需调整,事件设置选为profiling,然后可以设置取样profile哪些信息,比如:
Flame Graphs(火焰图)
在追求极致性能的场景下,了解你的程序运行过程中CPU在干什么很重要,火焰图就是一种非常直观的展示CPU在程序整个声明周期过程中时间分配的工具。
火焰图对于现代的程序员不应该陌生,这个工具可以非常直观地显示出调用栈中的CPU消耗瓶颈。
火焰图,简单那通过x轴横条宽度来度量时间指标。y轴代表线程栈的层次。
案例:
使用JDK 自身提供的工具进行JVM调优可将TPS由2.5提高到20(提升了7倍),并准确定位系统瓶颈。
系统瓶颈:应用里静态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。
那么。如何在海量业务代码中准确定位这些性能代码?这里使用阿里开源工具 TProfiler 来定位这些性能代码,成功解决了GC过阈频繁的性能瓶颈,并最终在上次优化的基础上将TPS再提升了4倍,即提升到了100。
Java运行时的追踪工具
常见的动态追踪工具有Btrace、HouseMD(项目已停止开发)、Greys-Anatomy、Byteman(JBoss出品)
BTrace是SUN Kenai云计算开发平台下的一个开源项目,旨在为Java提供安全可靠的动态跟踪分析工具。
BTrace is a safe, dynamic tracing tool for the Java platform. BTrace can be used to dynamically trace a running Java program (similar to DTrace for OpenSolaris applications and OS). BTrace dynamically instruments the classes of the target application to inject tracing code (“bytecode tracing”).
BTrace,一个Java平台的安全动态追踪工具。可以用来动态地追踪一个运行的Java程序。BTrace动态调整目标应用程序的类似注入跟踪代码(“字节码跟踪”)
Youkit
JProbe
Spring Insight
当启动时,JFR将记录运行过程中发生的一系列事件。其中包括Java层面的事件,如线程事件、锁事件,以及Java虚拟机内部的事件,如新建对象、垃圾回、即时编译事件。
按照发生时机以及持续事件来划分,JFR的事件共有四种类型,它们分别为以下四种。
取样事件的其中一个常见的例子便是方法抽样(Method Sampling),即每隔一段时间统计各线程的栈轨迹。如果在这些抽样取得的轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法。
在运行目标Java程序中添加-XX:StartFlightRecord=参数
。
比如:下面命令中,JFR将会在Java虚拟机启动5s后(对应delay=5s)收集数据,持续20s(对应duration=20s)。当收集完毕后,JFR会将收集得到的数据保存至指定的文件中(对应filename=myrecording.jfr
)
java -XX:StartFlightRecorder=delay=5s,duration=20s, filename=myrecording.jfr,settings=profile MyApp
由于 JFR 将持续收集数据,如果不加以限制,那么JFR可能会填满硬盘的所有空间。因此,我们有必要对这种模式下所收集的数据进行限制。
java -XX:StartFlightRecording=maxage=10m,maxsize=100m,name=SomeLabel MyApp
通过jcmd
来让JFR开始收集数据、停止收集数据,或者保存所收集的数据,对应的子命令分别为JFR.start
、JFR.stop
、JFR.dump
。
jcmd <PID> JFR.strat 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
[外链图片转存中…(img-gPasm3Ef-1645806723516)]
[外链图片转存中…(img-GUi5DEO6-1645806723516)]
[外链图片转存中…(img-3pOsgAZq-1645806723516)]
要采用取样,必须先添加参数:
-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder
取样时间默认为1分钟,可自行按需调整,事件设置选为profiling,然后可以设置取样profile哪些信息,比如:
Flame Graphs(火焰图)
在追求极致性能的场景下,了解你的程序运行过程中CPU在干什么很重要,火焰图就是一种非常直观的展示CPU在程序整个声明周期过程中时间分配的工具。
火焰图对于现代的程序员不应该陌生,这个工具可以非常直观地显示出调用栈中的CPU消耗瓶颈。
[外链图片转存中…(img-FNZhaW90-1645806723517)]
火焰图,简单那通过x轴横条宽度来度量时间指标。y轴代表线程栈的层次。
案例:
使用JDK 自身提供的工具进行JVM调优可将TPS由2.5提高到20(提升了7倍),并准确定位系统瓶颈。
系统瓶颈:应用里静态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。
那么。如何在海量业务代码中准确定位这些性能代码?这里使用阿里开源工具 TProfiler 来定位这些性能代码,成功解决了GC过阈频繁的性能瓶颈,并最终在上次优化的基础上将TPS再提升了4倍,即提升到了100。
Java运行时的追踪工具
常见的动态追踪工具有Btrace、HouseMD(项目已停止开发)、Greys-Anatomy、Byteman(JBoss出品)
BTrace是SUN Kenai云计算开发平台下的一个开源项目,旨在为Java提供安全可靠的动态跟踪分析工具。
BTrace is a safe, dynamic tracing tool for the Java platform. BTrace can be used to dynamically trace a running Java program (similar to DTrace for OpenSolaris applications and OS). BTrace dynamically instruments the classes of the target application to inject tracing code (“bytecode tracing”).
BTrace,一个Java平台的安全动态追踪工具。可以用来动态地追踪一个运行的Java程序。BTrace动态调整目标应用程序的类似注入跟踪代码(“字节码跟踪”)
Youkit
JProbe
Spring Insight