【JVM · 调优】监控及诊断工具

一. 概述

1. 背景说明

1.1 生产环境中的问题

  1. 生产环境发生了内存溢出该如何处理?
  2. 生产环境应该给服务器分配多少内存合适?
  3. 如何对垃圾回收器的性能进行调优?
  4. 生产环境CPU负载飙高该如何处理?
  5. 生产环境应该给应用分配多少线程合适?
  6. 不加log,如何确定请求是否执行了某一行代码?
  7. 不加log,如何实时查看某个方法的入参与返回值?

1.2 为什么要调优?

  • 防止出现OOM
  • 解决OOM
  • 减少Full GC出现的频率

1.3 不同阶段的考虑

  • 上线前
  • 项目运行阶段
  • 线上出现OOM

2. 调优概述

2.1 监控的依据

  • 运行日志
  • 异常堆栈
  • GC日志
  • 线程快照
  • 堆转储快照

2.2 调优的大方向

  • 合理地编写代码
  • 充分并合理地使用硬件资源
  • 合理地进行JVM调优

3. 性能优化的步骤

3.1 第1步 (发现问题):性能监控

一种以 非强行/入侵 方式 收集/查看 应用运营性能数据的活动。

监控通常是指一种在生产、质量评估、开发环境下实施的带有 预防主动性 的活动。

当应用相关干系人提出性能问题却 没有提供足够多的线索 时,首先我们需要进行性能监控,随后是性能分析。

常见性能问题:

  • GC 频繁
  • CPU Load过高
  • OOM
  • 内存泄漏
  • 死锁
  • 程序响应时间较长

3.2 第2步 (排查问题):性能分析

一种以 侵入方式 收集运行性能数据的活动,它会影响应用的吞吐量或响应性。

性能分析是针对性能问题的答复结果,关注的范围通常比性能监控更加集中。

性能分析很少在生产环境下进行,通常是在质量评估、系统测试/开发环境 下进行,是监控之后的步骤。

常用分析手段:

  • 打印GC日志,通过GCX Viewer或gceasy分析日志信息
  • 灵活运用命令行工具 (jstack、jmap、jinfo等)
  • dump出堆文件,使用内存分析工具分析文件
  • 使用 Arthas / jconsole / JVisualVM 来实时查看JVM状态
  • jstack查看对栈信息

3.3 第3步 (解决问题):性能调优

一种为改善应用响应性或吞吐量而更改参数、源代码、属性配置的活动,性能调优是在性能监控、性能分析之后的活动。

常见调优策略:

  • 适当增加内存,根据业务背景选择垃圾回收器
  • 优化代码,控制内存使用
  • 增加机器,分散节点压力
  • 合理设置线程池线程数量
  • 使用中间件提高程序效率,比如:缓存、消息队列 等
  • 其他

4. 性能评价/测试指标

4.1 停顿时间(响应时间)

提交请求和返回该请求的响应之间使用的时间,一般比较关注平均响应时间常用操作的响应时间列表:

操作 响应时间
打开一个站点 几秒
数据库查询一条记录(有索引) 十几毫秒
机械磁盘一次寻址定位 4毫秒
从机械磁盘顺序读取1M数据 2毫秒
从SSD硬盘顺序读取1M数据 0.3毫秒
从远程分布式换成Redis读取一个数据 0.5毫秒
从内存读取1M数据 十几微秒
Java程序本地方法调用 几微秒
网络传输2KB数据 1微秒

在垃圾回收环节中:

  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。-XX:MaxGCPauseMillis

4.2 吞吐量

对单位时间内完成的工作量(请求)的量度。

在GC中:运行用户代码的时间占总运行时间的比例(总运行时间:程序的运行时间 + 内存回收的时间)

吞吐量 = 1 - 1 / (1 + n),其中-XX:GCTimeRatio=n

4.3 并发数

同一时刻,对服务器有实际交互的请求数。

  • 1000人同时在线,估计并发数在5%~15%之间,也就是同时并发量在50~150之间。

4.4 内存占用

Java堆区所占用的内存大小

4.5 相互间的关系

以高速公路通行状况为例:

  • 吞吐量:每天通过高速公路收费站的车辆数(或收费站收取的高速费用)
  • 并发数:高速公路上正在行驶的车辆数
  • 响应时间:车速

二. 监控及诊断工具(命令行)

1. 概述

性能诊断是软件工程师日常工作中需要经常面对和解决的问题,在用户体验至上的今天,解决好应用的性能问题能带来非常大的收益。

Java作为最流行的编程语言之一,其应用性能诊断一直受到业界广泛关注。可能造成Java应用出现性能问题的因素非常多,例如:线程控制、磁盘读写、数据库访问、网络I/O、垃圾收集等。想要定位这些问题,一款优秀的性能诊断工具必不可少。

※ 简单命令行工具

在我们刚接触Java学习时,最先了解的两个命令是javajavac,那么除此之外,还有没有其他的命令可以供我们使用呢?在JDK的bin目录,还有一系列辅助工具。这些辅助工具用来获取目标JVM的不同方面、不同层次的信息,帮助开发人员更好地解决Java应用的一些疑难杂症。

Open JDK 11 源码:

https://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/jdk.jcmd/share/classes/sun/tools

2. jps:查看正在运行的Java进程

2.1 基本情况

jps (Java Process Status)

显示指定系统内所有的HotSpot虚拟机进程 (查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。

说明:对于本地虚拟机进程而言,进程的本地虚拟机ID与操作系统的进程ID是一致的。

2.2 测试

public class ScannerTest {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String info = sc.nextLine();	// 阻塞
    }
}

测试结果

查看PID(进程ID):
【JVM · 调优】监控及诊断工具_第1张图片

2.3 基本语法

它的基本语法为:

jps [options] [hostid]

我们还可以通过追加参数,来打印额外的信息。

① options 参数
  • -q:仅仅显示LVMID(local virtual machine id),即本地虚拟机唯一ID,不显示主类名称等。

  • -l:输出程序的主类的全类名 或 如果进程执行的是jar包,则输出jar完整路径。

  • -m:输出进程启动时传递给主类main()的参数。

  • -v:列出虚拟机进程启动时的JVM参数。比如:-Xms20m -Xmx50m 是启动程序指定的jvm参数。


说明:以上参数可以综合使用。

补充:如果某Java进程关闭了默认开启的UserPerfData参数(即使用参数-XX:-UseParfData),那么jps命令(以及下面介绍的jstat)将无法探知该Java进程。

② hostid 参数

RMI注册表中注册的主机名。

如果想要远程监控主机上的Java程序,需要安装jstatd

对于具有更严格的安全实践的网络场所而言,可能使用一个自定义的策略文件来显示对特定的可信主机或网络的访问,尽管 这种技术容易收到IP地址欺诈攻击

如果安全问题无法使用一个定制的策略文件来处理,那么最安全的操作是不运行jstatd服务器,而是在本地使用jstatjps工具。

3. jstat:查看JVM统计信息

3.1 基本情况

jstat (JVM Statistics Monitoring Tool):用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地/远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题、内存泄漏问题。

官方文档:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html

3.2 基本语法

它的基本语法为:

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

查看命令相关参数:

jstat -hjstat -help

① option 参数

选项option可以由以下值构成。

  • 类装载相关:

    • -class:显示ClassLoader的相关信息:类的装载/卸载数量、总空间、类装载所消耗的时间等

      表头信息 解释说明
      Loaded 装载类的数量
      Bytes(Loaded) 装载类所占用的字节数
      Unloaded 卸载类的数量
      Bytes(Unloaded) 卸载类所占用的字节数
      Time 装载和卸载类所花费的时间

jstat

  • 垃圾回收相关:

    • -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 类名和方法名用来标识编译的方法。类名使用/做为一个命名空间分隔符。
② interval 参数

用于指定输出统计数据的周期,即:查询间隔。单位:毫秒(ms)

③ count 参数

用于指定查询的总次数。

④ -t 参数

可以在输出信息前加上一个Timestamp列,显示程序的运行时间。单位:秒(s)

经验:

我们可以比较Java进程的启动时间以及总GC时间(GCT列),或者两次测量的间隔时间以及总GC时间的增量,来得出GC占运行时间的比例。

如果该比例超过20%,则说明目前堆的压力比较大;如果该比例超过90%,则说明堆里几乎没有可用空间,随时都可能抛出OOM异常。

⑤ -h 参数

可以在周期性数据输出时,输出多少行数据后输出一个表头信息。

3.3 补充

jstat还可以用来判断是否出现内存泄漏。

  • 第1步:在长时间运行的Java程序中,我们可以运行jstat命令连续获取多行性能数据,并取这几行数据中 OU 列(即已占用的老年代内存)的最小值。
  • 第2步:然后,我们每隔一段较长的时间重复一次上述操作,来获得多组 OU 最小值。如果这些值呈上涨趋势,则说明该 Java程序 的老年代内存已使用量在不断上涨,这意味着无法回收的对象在不断增加,因此很有可能存在内存泄漏。

4. jinfo:实时查看和修改JVM配置参数

4.1 基本情况

jinfo(Configuration Info for Java)

查看虚拟机配置参数信息,也可以用于调整虚拟机的配置参数。

在很多情况下,Java程序不会指定所有的Java虚拟机参数。而开发人员可能不知道某一个具体的Java虚拟机参数的默认值。此时,可能需要查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了jinfo工具,开发人员就可以很方便地找到Java虚拟机参数的当前值。

【JVM · 调优】监控及诊断工具_第2张图片

官方文档:

https://docs.oracle.com/en/java/javase/11/tools/jinfo.html

4.2 基本语法

它的基本语法为:

jinfo [options] pid

说明:Java 进程ID (pid) 必须要加上

[options]:

选项 选项说明
no option 输出全部的参数和系统属性
-flag name 输出对应名称的参数
-flag [+|-]name 开启/关闭对应名称的参数, 只有被标记为manageable的参数才可以被动态修改
-flag name=value 设定对应名称的参数
-flags 输出全部的参数
-sysprops 输出系统属性
4.2.1 查看

jinfo -sysprops PID

  • 可以查看由System.getPropertoes()取得的参数

jinfo -flags PID

  • 查看曾经赋值过的一些参数

jinfo -flag 具体参数 PID

  • 查看某个Java进程的具体参数的值
4.2.2 修改

jinfo不仅可以查看运行时某一个Java虚拟机参数的实际取值,甚至可以在运行时修改部分参数,并使之立即生效。

但是,并非所有参数都支持动态修改。参数只有被标记为manageable的flag可以被实时修改。这个修改能力极其有限。

查看被标记为manageable的参数:(grep命令仅限Linux)

java -XX:+PrintFlagsFinal -version | grep manageable

针对boolean类型

  • jinfo -flag [+|-]具体参数 PID

针对非boolean类型

  • jinfo -flag 具体参数=具体参数值 PID

4.3 扩展

查看所有JVM启动的初始值

java -XX:+PrintFlagsInitial

查看所有JVM参数的最终值

java -XX:+PrintFlagsFinal

查看那些已经被用户或者JVM设置过的详细的XX参数的名称和值

java -XX:+PrintCommandLineFlags

5. jmap:导出内存映像文件 & 内存使用情况

5.1 基本情况

jmap(JVM Memory Map)

作用:

  • 获取dump文件(堆转储快照文件, 二进制文件)
  • 获取目标Java进程的内存相关信息,包括Java堆各区域的使用情况、堆中对象的统计信息、类加载信息等。

开发人员可以在控制台输入命令jamp -help查阅jmap工具的具体使用方式和一些标准选项配置。

官方文档:

https://docs.oracle.com/en/java/javase/11/tools/jmap.html

5.2 基本语法

它的基本语法为:

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版本的影响。

5.3 使用1:导出内存映像文件

一般来说,使用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

说明:

  1. 通常在写Heap Dump文件前,会触发一次Full GC,所以Heap Dump文件里保存的都是FullGC后留下的对象信息。
  2. 由于生成dump文件比较耗时,需耐心等待。尤其是大内存镜像生成 dump文件,则需要耗费更长的时间来完成。
5.3.1 手动方式
jmap -dump:format=b,file=<filename.hprof> <pid>
jmap -dump:live,format=b,file=<filename.hprof> <pid>
5.3.2 自动方式

当程序发生OOM退出系统时,一些瞬时信息随着程序的终止而消失,而重视OOM问题往往比较困难或耗时。此时若能在OOM时,自动导出dump文件就显得非常迫切。

这里介绍一种比较常见的取得对快照文件的方法:

  • -XX:+HeapDumpOnOutOfMemoryError:在程序发生OOM时,导出应用程序的当前堆快照。
  • -XX:HeapDumpPath=:课可以指定堆快照的保存位置。
-XX:+HeapDumpOnOutOfMemeryError
-XX:HeapDumpPath=<filename.hprof>

比如:

-Xmx100m -XX:+HeapDumpOnOutOfMemery -XX:HeapDumpPath=D:\m.hprof

5.4 使用2:显示堆内存相关信息

  • jmap -head :查看Heap空间比例分配、使用情况
  • jmap -histo :查看Heap中各类型→实例数目、实例所占空间情况
jmap -head <pid>
jmap -histo <pid>

5.5 使用3:其他作用

查看系统的ClassLoader信息

jmap -permstat pid

查看堆积在finalizer队列中的对象

jmap -finalizerinfo pid

5.6 小结

由于jmap将访问堆中的所有对象,为了保证在此过程中不被应用线程干扰,jmap需要借助安全点机制,让所有线程停留在不改变堆中数据的状态。也就是说,由jmap导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。

举例:假设在编译生成的机器码中,某些对象的生命周期在两个安全点之间,那么:live选项将无法探知到这些对象。

另外,如果某个线程长时间无法跑到安全点,jmap将一直等下去。与前面讲的jstat则不同,垃圾回收器会主动将jstat所需要的摘要数据保存至固定位置之中,而jstat只需直接读取即可。

6. jhat:JDK自带堆分析工具

6.1 基本情况

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 代替。

6.2 基本语法

它的基本语法是:

jhat [option] [dumpfile]

【JVM · 调优】监控及诊断工具_第3张图片

查看localhost:7000

【JVM · 调优】监控及诊断工具_第4张图片

OQL语句查询对象:

【JVM · 调优】监控及诊断工具_第5张图片

[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

7. jstack:打印JVM中线程快照

7.1 基本情况

jstack(JVM Stack Trace)

用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。线程快照就是当前虚拟机指定进程的每一条线程正在执行的方法堆栈的集合。

生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如:线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack显示各个线程调用的对栈情况。

官方文档:

https://docs.oracle.com/en/java/javase/11/tools/jstack.html


在 thread dump 中,要留意下面几种状态:

  • 死锁,DeadLock(重点关注)
  • 等待资源,Waiting on condition(重点关注)
  • 等待获取监视器,Waiting on monitor entry(重点关注)
  • 阻塞,Blocked(重点关注)
  • 执行中,Runnable
  • 暂停,Suspended
  • 对象等待中,Object.wait() 或 TIMED_WAITING
  • 停止,Parked

7.2 基本语法

它的基本语法为:

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

    }
}

【JVM · 调优】监控及诊断工具_第6张图片

死锁(DeadLock)发现:
【JVM · 调优】监控及诊断工具_第7张图片

时段等待(睡眠):

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

【JVM · 调优】监控及诊断工具_第8张图片

7.3 补充

获取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());
            }
        }
    }
}

Java代码层面:
【JVM · 调优】监控及诊断工具_第9张图片

8. jcmd:多功能命令行

8.1 基本情况

在JDK 1.7 以后,新增了一个命令行工具jcmd

它是一个多功能的工具,可以用来实现前面除了jstat之外所有命令的功能。

比如:用它来导出堆、内存使用、查看Java进程、导出线程信息、执行GC、JVM运行时间等。

官方文档:

https://docs.oracle.com/en/java/javase/11/tools/jcmd.html

jcmd拥有jmap的大部分功能,并且在Oracle官方网站上也推荐使用jcmd命令代替jmap命令。

8.2 基本语法

列出所有的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>

9. jstatd:远程主机信息收集

之前的指令只涉及到监控本机的Java应用程序,而在这些工具中,一些监控工具也支持对远程计算机的监控(如jpsjstat)。为了启动远程监控,则需要配合使用jstatd工具。

命令jstatd是一个RMI服务端程序,它的作用相当于代理服务器,建立本地计算机与远程监控工具的通信。jstatd服务器将本机的Java应用程序信息传递到远程计算机。

【JVM · 调优】监控及诊断工具_第10张图片

三. 监控及诊断工具(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 Misson Control,内置Java Flight Recorder。能够以极低的性能开销收集Java虚拟机的性能数据。
  • 第三方工具
    • MAT:MAT(Memory Analyzer Tool)是基于Eclipse的内存分析工具,一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。
      • Eclipse插件形式
    • JProfiler:商业软件,需付费使用,功能强大。
    • Arthas:Alibaba开源的Java诊断工具,深受开发者喜爱。
    • Btrace:Java运行时追踪工具,可以在不停机的情况下,追踪指定的方法调用、构造函数调用、系统内存等信息。

2. jConsole

2.1 基本概述

jconsole:

  • 从Java5开始,在JDK中自带的Java监控和管理控制台。
  • 用于对JVM中内存、线程、类等的监控,一款基于JMX(Java Management Extensions)的GUI性能监控工具。

官方介绍:

https://docs.oracle.com/javase/7/docs/technotes/tools/share/jconsole.html

官方教程:

https://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html

2.2 启动

jdk/bin目录下,启动jconsole.exe即可

不需要使用jps命令查询

2.3 三种连接方式

2.3.1 Local

使用JConsole丽娜姐一个正在本地系统运行的JVM,并且需要执行程序、运行JConsole为同一个用户。JConsole使用文件系统的授权通过RMI连接器连接到平台的MBean服务器上,这种从本地连接的监控能力只有Sun的JDK具有。

2.3.2 Remote

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

2.3.3. Advanced

使用一个特殊的URL连接JMX代理。一般情况使用自己定制的连接器而非RMI提供的连接器来连接JMX代理,或是一个使用JDK1.4的实现了JMX和JMX Romote的应用。

2.4 主要作用

  • 查看内存(手动请求执行GC)
  • 查看线程(死锁检测)
  • 类检测
  • VM 概要
  • MBean

测试代码:

/**
 * -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();
            }
        }
    }
}

概述图:

【JVM · 调优】监控及诊断工具_第11张图片


死锁检测:

【JVM · 调优】监控及诊断工具_第12张图片

3. Visual VM

3.1 基本概述

  • Visual VM,一款功能强大的多合一故障诊断和性能监控的可视化工具。
  • 它集成了多个JDK命令行工具,使用 Visual VM 可用于显示虚拟机进程及进程的配置和环境信息 (jps, jinfo),监控应用程序的CPU、GC、堆、方法区、线程的信息(jstat, jstack)等,甚至代替JConsole。
  • 在JDK 1.7后,Visual VM作为JDK的一部分发布(JDK/bin目录下),完全免费。
  • 此外,Visual VM 也可作为独立地软件安装(首页:https://visualvm/github.io)

【JVM · 调优】监控及诊断工具_第13张图片

3.2 插件的安装

  • Visual VM的一大特点是支持插件扩展,而且插件安装非常方便。我们可以通过离线下载插件文件*.nbm,然后再Plugin对话框的已下载界面下,添加已下载的插件。也可以再可用插件页面下,在线安装插件(建议安装:VisualGC
    • 插件地址:http://visualvm.github.io/plugins.html

本地安装插件:

【JVM · 调优】监控及诊断工具_第14张图片

IDEA启动插件安装:

【JVM · 调优】监控及诊断工具_第15张图片

配置相关路径:

【JVM · 调优】监控及诊断工具_第16张图片

3.3 连接方式

3.3.1 本地连接

监控本地Java进程的CPU、类、线程等

3.3.2 远程连接
  1. 确定远程服务器的IP地址
  2. 添加JMX(通过JMX技术具体监控远端服务器某个Java进程)
  3. 修改bin/catalina.sh文件,来凝结远程的Tomcat
  4. .../conf中添加jmxremote.accessjmxremote.password文件
  5. 将服务器地址改为公网IP地址
  6. 设置阿里云安全策略和防火墙策略
  7. 启动Tomcat,查看Tomcat启动日志和端口监听
  8. JMX中输入端口号、用户名、密码登录

3.4 主要功能

  • 生成/读取堆内存快照
  • 查看JVM参数和系统属性
  • 查看运行中的虚拟机进程
  • 生成/读取线程快照
  • 程序资源的实时监控
  • 其他功能
    • JMX代理连接
    • 远程环境监控
    • CPU分析 & 内存分析

Heap-Dump

生成Heap-Dump文件:

【JVM · 调优】监控及诊断工具_第17张图片

【JVM · 调优】监控及诊断工具_第18张图片

读取Heap-Dump文件:

【JVM · 调优】监控及诊断工具_第19张图片

【JVM · 调优】监控及诊断工具_第20张图片


Thread-Dump

生成Thread-Dump文件:

【JVM · 调优】监控及诊断工具_第21张图片

【JVM · 调优】监控及诊断工具_第22张图片

读取Thread-Dump文件:

【JVM · 调优】监控及诊断工具_第23张图片

【JVM · 调优】监控及诊断工具_第24张图片


CPU抽样:

【JVM · 调优】监控及诊断工具_第25张图片

内存抽样:

【JVM · 调优】监控及诊断工具_第26张图片

4. eclipse MAT

4.1 基本概述

MAT(Memory Analyzer Tool)工具是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。

MAT是基于Eclipse开发的,不仅可以单独使用,还可以作为插件的形式嵌入Eclipse中使用。作为一款免费的性能分析工具,使用非常方便。

下载地址:

https://www.eclipse.org/mat/download.php

【JVM · 调优】监控及诊断工具_第27张图片

只要确保机器上装有JDK并配置好相关的环境变量,MAT可正常启动。

还可以在Eclipse中以插件形式安装。

4.2 获取堆dump文件

4.2.1 dump文件内容

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

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

  • 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。
  • 所有的类信息,包括classloader、类名称、父类、静态变量等。
  • GCRoot到所有的这些对象的引用路径。
  • 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)
4.2.2 两点说明

说明1:

缺点:MAT不是一个万能的工具,它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式,例如:Sun, HP, SAP 所采用的 HPROF 二进制堆存储文件,以及 IBM 的 PHD 堆存储文件都能被很好的解析。

说明2:

最吸引人的还是能快速为开发人员生成 内存泄漏报表,方便定位问题、分析问题。虽然MAT有如此强大的功能,但是内存分析也没有简单到一键完成的程度,很多内存问题还是需要我们从MAT展现给我们的信息当中通过经验和直觉来判断才能发现。

4.2.3 获取dump

**方式一:**通过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进程,以供选择并获取快照。

4.3 分析堆dump文件

【JVM · 调优】监控及诊断工具_第28张图片

【JVM · 调优】监控及诊断工具_第29张图片

4.3.1 histogram

展示了各个类的实例数目以及这些实例的 Shallow Heap 或 Retained Heap 的总和。

【JVM · 调优】监控及诊断工具_第30张图片

两不同时刻dump文件作比较:

【JVM · 调优】监控及诊断工具_第31张图片

4.3.2 thread overview

查看系统中的Java线程

【JVM · 调优】监控及诊断工具_第32张图片

查看局部变量的信息

【JVM · 调优】监控及诊断工具_第33张图片

长周期对象引用短周期对象,导致短周期对象无法被GC,则将此短周期对象视为内存泄漏。

4.3.3 获得对象相互引用的关系

引用关系

  • with outgoing references:查看当前引用的出引用(发散出的引用)

    【JVM · 调优】监控及诊断工具_第34张图片

  • with incoming references:查看当前引用的入引用(到达的引用, 即被引用)

    【JVM · 调优】监控及诊断工具_第35张图片

4.3.4 浅堆&深堆
※ Shallow Heap - 浅堆
  • 浅堆(Shallow Heap):指一个对象所消耗对的内存。在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8字节。根据堆快照格式不同,对象的大小可能会向8字节进行对齐。

以String为例:2个int值共占8字节,对象引用占用4字节,对象头8字节,合计20字节,向8字节对齐,故占据24字节(JDK 7)

String

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

※ Retained Heap - 深堆
  • 保留集(Retained Set):对象A的保留集指当对象A被垃圾回收后,可以被释放的所有对象集合(包括对象A本身)。即对象A的保留集可以被认为是只能通过对象A被直接/间接访问到的所有对象的集合。即仅被对象A所持有的对象的集合。
  • 深堆(Retained Heap):深堆指对象的保留集中所有对象的浅堆大小之和。

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

此规则适用于字符串常量池(常量池中常量若仅被1个字符串引用持有, 则计入深堆, 当数量多于1时不计入)

补充:对象实际大小

另外一个常用的概念是对象的实际大小。这里,对象的实际大小定义为一个对象 所能触及的 所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小。与深堆相比,似乎这个在日常开发中更为直观和被人接受,但实际上,这个概念与垃圾回收无关。

下图显示了一个简单的对象引用关系图,对象A引用了C和D,对象B引用了C和E。那么对象A的浅堆大小只是A本身,不包含C和D,而A的实际大小为A、C、D之和。但A的深堆大小为A与D之和,由于对象C还可以被对象B访问到,因此不在对象A的深堆范围内。

【JVM · 调优】监控及诊断工具_第36张图片

案例分析:StudentTrace
/**
 * -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方法...
}

【JVM · 调优】监控及诊断工具_第37张图片

【JVM · 调优】监控及诊断工具_第38张图片

考虑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 (字节)

  • 这72个字节是什么?
    • 15个elementData元素 * 引用4字节 = 60字节
    • 60 + 对象头8字节 + 数组长度4字节 = 72字节

【JVM · 调优】监控及诊断工具_第39张图片

4.3.5 支配树

支配树(Dominator Tree)

支配树的概念源自图论。

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。

【JVM · 调优】监控及诊断工具_第40张图片

同理,对象E支配对象G。到达对象H的可以通过对象D,也可以通过对象E,因此对象D和E都不能支配对象H,而经过对象C既可以到达D也可以达到E,因此C为对象H的直接支配者。


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

支配树视图

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

【JVM · 调优】监控及诊断工具_第41张图片

4.4 案例:Tomcat堆溢出分析

4.4.1 说明

Tomcat是最常用的Java Servlet容器之一,同时也可以当作单独的Web服务器使用。Tomcat本身使用Java实现,并运行于Java虚拟机之上。在大规模请求时,Tomcat有可能会因为无法承受压力而发生内存溢出错误。这里根据一个被压垮的Tomcat堆快照文件,分析Tomcat在崩溃时的内部情况。

4.4.2 分析过程

【JVM · 调优】监控及诊断工具_第42张图片

【JVM · 调优】监控及诊断工具_第43张图片

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。

※ 内存泄漏问题

① 内存泄漏的理解与分类

内存泄漏(Memory Leak)

【JVM · 调优】监控及诊断工具_第44张图片

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

发生内存泄漏:

  • 是否还被使用?
  • 是否还被需要?

内存泄漏(Memory Leak)的理解

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

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

【JVM · 调优】监控及诊断工具_第45张图片

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

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

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


内存泄漏与内存溢出关系

内存泄漏(Memory Leak)

  • 申请了内存用完了不释放,如共有 1024M 内存,分配了 512M 的内存一直不回收,那么可以用的内存只有 512M 了,仿佛泄漏掉了一部分。

内存溢出(Out Of Memory)

  • 申请内存时,没有足够的内存可以使用。

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

泄漏的分类

**经常发生:**发生内存泄漏的代码会被多次执行,每次执行,泄漏一块内存。

**偶然发生:**在某些特定情况下才会发生。

**一次性:**发生内存泄漏的方法只会执行一次。

**隐式泄露:**一直占着内存不释放,直到执行结束;严格地说不算内存泄漏,因为最终释放了,但是如果执行时间极长,也可能导致内存耗尽。

② Java中内存泄漏的8种情况

1)静态集合类

静态集合类,如HashMap、LinkedList等。如果这些容器为静态的,那么它们的生命周期与JVM程序一致,则容器中的对象在程序结束前将不能被释放,从而造成内存泄漏。

简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

public class MemoryLeak {
    static List list = new ArrayList();
    
   	public void oomTest() {
        Object obj = new Object();
        list.add(obj);
    }
}

补充:缓存集合可使用WeakHashMap代替普通HashMap。

2)单例模式

单例模式,与静态集合导致内存泄漏的原因类似,因为单例的静态特性,它的生命周期和JVM的生命周期一样长。

所以,如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。

3)内部类持有外部类

内部类持有外部类,如果一个外部类实例对象的方法返回了一个内部类的实例对象。

这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象不会被垃圾回收,这也会造成内存泄漏。

4)各种连接(数据库/网络/IO)

各种连接,如数据库连接、网络连接、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
    }
}
5)变量不合理的作用域

变量不合理的作用域。一般而言,一个变量定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为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的引用,但需考虑并发问题。

6)改变哈希值

改变哈希值,当一个对象被存储进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方法...
}

结果:

结果

7)缓存泄露

内存泄漏的另一个常见来源是缓存,一旦将对象引用放入缓存中,很容易被遗忘。

举例:项目在一次上线时,启动奇慢直至卡住,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。

对于这个问题可以使用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);
        }
    }
}

结果:

【JVM · 调优】监控及诊断工具_第46张图片

【JVM · 调优】监控及诊断工具_第47张图片

上面代码和图示演示了WeakhashMap如何自动释放缓存对象,当init方法执行完成后,局部变量字符串引用ref1、ref2、ref3、ref4都会消失,此时只有静态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];    // 只是让指针下移,未实现置空
    }

//    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方法,下面通过这张图示展示:

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

【JVM · 调优】监控及诊断工具_第48张图片

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

【JVM · 调优】监控及诊断工具_第49张图片

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

解决方法

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

一旦引用过期,清空这些引用,将其置空。

【JVM · 调优】监控及诊断工具_第50张图片

※ 支持使用OQL语言查询对象信息

MAT支持一种类似于SQL的查询语言OQL(Object Query Language)。OQL实用类SQL语法,可以在堆中进行对象的查找和筛选。

【JVM · 调优】监控及诊断工具_第51张图片

① 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

在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子句

From子句用于指定查询范围,它可以指定类名、正则表达式、对象地址。

SELECT * FROM java.lang.String s

使用正则表达式,限定搜索范围,输出所有com.ljw包下所有类的实例:

SELECT * FROM "com\.ljw\..*"

也可以直接使用类的地址进行搜索。使用类的地址的好处是可以区分被不同ClassLoader加载的同一种类型。

SELECT * FROM 0x37a0b4d

③ WHERE子句

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

5. JProfiler

5.1 基本概述

5.1.1 介绍

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

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

官网下载地址:https://www.ej-technologies.com/products/jprofiler/overview.html

5.1.2 特点
  • 使用方便、界面操作友好(简单且强大)
  • 对被分析的应用影响小(提供模板)
  • CPU、Thread、Memory分析功能尤其强大
  • 支持对 jdbc、noSql、jsp、servlet、socket等 进行分析
  • 支持多种模式(离线/在线)的分析
  • 支持监控本地、远程的JVM
  • 跨平台,拥有多种操作系统的安装版本

【JVM · 调优】监控及诊断工具_第52张图片

5.1.3 主要功能

① 方法调用

  • 对方法调用的分析可以帮助了解应用程序正在做什么,并找到提高其性能的方法。

② 内存分配

  • 通过分析堆上对象、引用链、垃圾收集帮助修复内存泄漏问题,优化内存使用。

③ 线程&锁

  • JProfiler提供多种针对线程和锁的分析视图帮助发现多线程问题。

④ 高级子系统

  • 许多性能问题都发生在更高的语义级别上。
  • 例如:对于JDBC调用,可能希望找出执行最慢的SQL语句。JProfiler支持对这些子系统进行集成分析。

5.2 安装与配置

5.2.1 下载与安装

官网下载地址:https://www.ej-technologies.com/download/jprofiler/files

【JVM · 调优】监控及诊断工具_第53张图片

5.2.2 JProfiler中配置IDEA

【JVM · 调优】监控及诊断工具_第54张图片

配置IDE:

【JVM · 调优】监控及诊断工具_第55张图片

5.2.3 IDEA集成JProfiler

**安装JProfiler插件: **

(也可手动下载 插件→本地安装)

【JVM · 调优】监控及诊断工具_第56张图片

配置JProfiler安装路径:

【JVM · 调优】监控及诊断工具_第57张图片

5.3 具体使用

【JVM · 调优】监控及诊断工具_第58张图片

5.3.1 数据采集方式

【JVM · 调优】监控及诊断工具_第59张图片

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

  • Instumentation :这是JProfiler全功能模式。在Class加载之前,JProfiler把相关功能代码写入到需要分析的class的bytecode中,对正在运行的JVM有一定影响。
    • 优点:功能强大。在此设置中,调用堆栈信息是准确的。
    • 缺点:若要分析的class较多,则对引用的性能影响较大,CPU开销可能很高 (取决于Filter的控制)。因此使用此模式一般配合Filter使用,值对特定的类或包进行分析。
  • Sampling :类似于样本统计,每隔一定时间(5ms) 将每个线程栈中的方法栈中的信息统计出来。
    • 优点:对CPU的开销非常低,对应用影响小 (即使不配置任何Filter)
    • 缺点:一些数据/特性不能提供 (例如:方法的调用次数、执行时间)

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

5.3.2 遥感检测:Telemetries

【JVM · 调优】监控及诊断工具_第60张图片

遥感检测 Telemetries(查看JVM的运行信息)

  • 整体视图 Overview:显示堆内存、CPU、线程、GC等活动的视图
  • 内存 Memory:显示一张关于内存变化的活动时间表
  • 记录的对象 Record Objects:显示一张关于活动对象与数组的图表的活动时间表
  • 记录吞吐量 Record Throughput:显示一段时间累计的JVM生产和释放的活动时间表
  • 垃圾回收活动 GC Activity:显示一张关于垃圾回收活动的活动时间表
  • 类 Classes:显示一个与已装载类的图表的活动时间表
  • 线程 Thread:显示一个与动态线程图表的活动时间表
  • CPU负载 CPU Load:显示一段时间中CPU的负载图表
5.3.3 内存视图:Live Memory

【JVM · 调优】监控及诊断工具_第61张图片

Live Memory 内存剖析:class/class instance 的相关信息。

例如:对象的个数、大小,对象创建的方法执行栈,对象创建的热点。

  • 所有对象 All Objects

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

  • 记录对象 Record Objects

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

  • 分配访问树 Allocation Call Tree

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

  • 分配热点 Allocation Hot Spots

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

  • 类追踪器 Class Tracker

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


分析:内存中的对象的情况

  • 频繁创建的Java对象:死循环、循环次数过多

  • 存在大对象:读取文件时,byte[]应该边读边写(长时间不写出会导致 byte[] 过大)

  • 存在内存泄漏

    【JVM · 调优】监控及诊断工具_第62张图片


更改取样频次:

【JVM · 调优】监控及诊断工具_第63张图片

【JVM · 调优】监控及诊断工具_第64张图片

5.3.4 堆遍历:Heap Walker

类 Classes

  • 显示所有类和它们的实例,可以右击具体的类 “Used Selected Instance” 实例进一步跟踪。

分配 Allocations

  • 为所有记录对象显示分配树和分配热点。

索引 References

  • 为单个对象和 “显示到垃圾回收根目录的路径” 提供索引图的显示功能。还能提供合并输入视图和输出视图的功能。

时间 Time

  • 显示一个对已记录对象的解决时间的柱状图。

检查 Inspections

  • 显示了一个数量的操作,将分析当前对象集在某种条件下的子集,实质是一个筛选的过程。

图表 Graph

  • 需要在references视图和biggest视图手动添加对象到图表,它可以显示对象的传入和传出引用,能方便地找到垃圾收集器的根源。

PS:在工具栏点击 “Go To Start” 可以使堆内存重新计数也就是回到初始状态。


查看Picture-incoming:

【JVM · 调优】监控及诊断工具_第65张图片

【JVM · 调优】监控及诊断工具_第66张图片

【JVM · 调优】监控及诊断工具_第67张图片


展示指向关系:

【JVM · 调优】监控及诊断工具_第68张图片

查看图表:

【JVM · 调优】监控及诊断工具_第69张图片

向前溯源:

【JVM · 调优】监控及诊断工具_第70张图片

5.3.5 CPU视图:CPU Views

JProfiler提供不同的方法来记录访问树以优化性能和细节。线程或者线程组状况可以被所有的视图

访问树 Call Tree

  • 显示一个积累的自顶向下的树,树中包含所有在JVM中已记录的访问队列。JDBC、JMS、JNDI服务请求都被注释在请求树中。请求树可以根据Servlet和JSP对URL的不同需要进行拆分。

热点 Hot Spots

  • 显示消耗时间最多的方法的列表。对每个热点都能显示回溯树。该热点可以按照方法请求,JDBC、JMS、JNDI服务请求以及按照URL请求来进行计算。

访问图 Call Graph

  • 显示一个从已选方法、类、包或J2EE组件开始的访问队列的图。

方法统计 Method Statistics

  • 显示一段时间内记录的方法的调用时间细节。
5.3.6 线程视图:Threads

JProfiler通过对线程历史的监控 判断其运行状态,并监控是否有线程阻塞产生,还能将一个线程所管理的方法以树状形式呈现,对线程进行剖析。

线程历史 Thread History

  • 显示一个与线程活动和线程状态在一起的活动时间表。

线程监控 Thread Monitor

  • 显示一个列表,包括所有的活动线程以及它们目前的活动状况。

线程转储 Thread Dumps

  • 显示所有线程的堆栈跟踪。

线程分析主要关心三个方面:

  1. Web容器的线程最大数

    (比如:Tomcat的线程容量应该略大于最大并发数。)

  2. 线程阻塞

  3. 线程死锁

5.3.7 监视器&锁:Monitor & locks

获取所有线程持有锁的情况以及锁的信息。

观察JVM的内部线程并查看状态:

  • 死锁探测图表 Current Locking Graph:显示JVM中的当前死锁图表。
  • 目前使用的检测器 Current Monitors:显示目前使用的检测器并且包括它们的关联线程。
  • 锁定历史图表 Locking History Graph:显示记录在JVM中的锁定方法。
  • 历史检测记录 Monitor History:显示重大的等待事件和阻塞事件的历史记录。
  • 监控器使用统计 Monitor Usage Statistics:显示分组监测,线程和监测类的统计监测数据。

死锁探测:

【JVM · 调优】监控及诊断工具_第71张图片

5.4 案例分析

5.4.1 案例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];
    private String info = "Hello,JProfiler!";
}

存在GC,情况较好:

【JVM · 调优】监控及诊断工具_第72张图片

5.4.2 案例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,JProfiler!";
    static ArrayList list = new ArrayList();	// 核心问题代码
}

beanList伴随着每次while循环结束而销毁,但Bean.liststatic 类变量,并不会销毁,从而导致内存占用越来越大。


查看对象情况,发现charp[]类型数值增长迅猛:

【JVM · 调优】监控及诊断工具_第73张图片

JVM不断地在进行GC:

【JVM · 调优】监控及诊断工具_第74张图片

绿色框图代表堆大小,程序不断地向JVM申请堆动态扩容,但GC若干次,占用内存的大小始终在持续上升,增长迅猛:

【JVM · 调优】监控及诊断工具_第75张图片

通过Heap Walker 溯源,发现问题根源:

【JVM · 调优】监控及诊断工具_第76张图片

问题根源为Bean.liststatic,可从此入手,对程序代码进行优化重构。

6. Arthas

6.1 基本概述

6.1.1 背景

先前,我们介绍了JDK自带的JVisualVM等免费工具,以及商业化工具JProfiler。

这两款工具在业界知名度也比较高,它们的 优点:可以图形界面上看到各维度的性能数据,使用者根据这些数据进行综合分析,然后判断哪里出现了性能问题。

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

有这样一款工具,不需要远程连接,也不需要配置监控参数,同时也提供了丰富的性能监控数据:Alibaba 开源分析工具 Arthas(阿尔萨斯)

6.1.2 概述

Arthas(阿尔萨斯)是Alibaba开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。

Arthas支持JDK 6+,支持Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的Tab自动补全功能,进一步方便进行问题的定位和判断。

它可以解决以下问题:

  • 这个类从哪个jar包加载的?为什么会报各种类相关的Exception?
  • 我改的代码为什么没有执行到?难道是我没commit?分支搞错了吗?
  • 遇到问题无法在线上debug,难道只能通过加日志再重新发布吗?
  • 线上遇到某个用户的数据处理有问题,但线上同样无法debug,线下无法加载!
  • 是否有一个全局视角来查看系统的运行状况?
  • 有什么办法可以监控到JVM的实时运行状态?
  • 怎么快速定位应用的热点,生成火焰图?
6.1.3 基于哪些工具开发而来
  • greys-anatomy: Arthas代码基于Greys二次开发而来
  • termd: Arthas的命令行基于termd开发
  • crash: Arthas的文本渲染功能基于crash中的文本渲染功能开发
  • cli: Arthas的命令行 界面基于vert.x提供的cli库进行开发
  • compiler: Arthas内存编译器代码来源
  • Apache Commons Net: Arthas里的 Telnet Client 的代码来源
  • JavaAgent: 运行在main方法之前的 拦截器,它内定的方法名叫 premain,也就是说先执行 premain 方法再执行 main 方法
  • ASM: 一个通用的Java字节码操作和分析框架。它可以用于修改现有的类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从它们构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但是主要关注性能。因为它被设计和实现得尽可能小和快,所以非常适合在动态系统中使用(也可以以静态方式使用, 例如在编译器中)

官方使用文档:

https://arthas.aliyun.com/doc/

6.2 安装与使用

6.2.1 安装

**安装方式以:**可以直接在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服务器上。

6.2.2 工程目录

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: 测试

6.2.3 启动

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]
6.2.4 查看日志
cat ~/logs/arthas/arthas.log
6.2.5 查看帮助
java -jar arthas-boot.jar -h
6.2.6 web console

除了在命令行查看外,Arthas目前还支持 Web Console。在成功启动连接进程之后就已经自动启动,可以直接访问 http://127.0.0.1:8163/ 访问,页面上的操作模式和控制台完全一样。

6.2.7 退出

最后一行 [arthas@7457]$ 证明打开进入了监控客户端,在这里就可以执行相关命令进行查看了。

  • 使用quit\exit:退出当前客户端
  • 使用shop\shutdown:关闭arthas服务器,并退出所有客户端

6.3 相关诊断指令

6.3.1 基础指令
  • 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快捷键列表及自定义快捷键
6.3.2 JVM相关
  • 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到临时文件
6.3.3 class/classloader相关
  • sc:查看JVM已加载的类信息

    • class-pattern:类名表达式匹配
    • -d:输出当前类的详细信息,包括这个类所加载的原始文件来源、类的声明、加载的ClassLoader等详细信息。如果一个类被多个ClassLoader所加载,则会出现多次。
    • -E:开启正则表达式匹配,默认为通配符匹配
    • -f:输出当前类的成员变量信息(需要配合参数-d一起使用)
    • -x:指定输出静态变量时属性的变量深度,默认为0,即直接使用toString输出

    补充:

    1. class-pattern支持全限定名,如com.test.ABC,也支持com/test/ABC这样的格式,这样,我们从异常堆栈将类名拷贝时就无需手动做分隔符替换了
    2. 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
6.3.4 monitor/watch/trace相关

这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测。

因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 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)
6.3.5 其他
  • profiler:使用 async-profiler 对应用采样,生成火焰图

  • grep:搜索满足条件的结果

  • plaintext:将命令的结果去除ANSI颜色

  • wc:按行统计输出结果

  • 使用 >将结果重写向到日志文件,使用 & 指定命令是后台运行,session断开不影响任务执行(生命周期默认为1天)

  • jobs:列出所有job

  • kill:强制终止任务

  • fg:将暂停的任务拉到前台执行

  • bg:将暂停的任务放到后台执行

7. Java Mission Control

7.1 历史

在 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

7.2 启动

Mission Control 位于 %JAVA_HOME%/bin/jmc.exe,打开这款软件。

7.3 概述

Java Mission Control(JMC),Java官方提供的性能强劲的工具。是一个对于Java应用程序进行管理、监视、概要分析和故障排除的工具套件。

它包含一个GUI客户端,以及众多用来收集Java虚拟机性能数据的插件,如 JMX Console(能够访问用来存放虚拟机各个子系统运行数据的MXBeans),以及虚拟机内置的高效profiling工具Java Flight Recorder(JFR)

JMC 的另一个优点就是:采用取样,而非传统的代码植入技术,对应用性能的影响非常非常小,完全可以开着JMC来做压测(唯一影响:Full GC 次数增多)

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

如果时远程服务器,需开启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}

概览界面:

【JVM · 调优】监控及诊断工具_第77张图片

触发器:达到设定条件报警

【JVM · 调优】监控及诊断工具_第78张图片

7.5 Java Flight Recorder

Java Flight Recorder 是 JMC 的其中一个组件。

Java Flight Recorder 能够以极低的性能开销收集 Java 虚拟机的性能数据。

JFR 的性能开销很小,在默认配置下平均低于1%,与其他工具相比,JFR能够直接访问虚拟机内的数据,并且不会影响虚拟机的优化。因此,它非擦黄给你适用于生产环境下满负荷运行的Java程序。

Java Flight Recorder 和 JDK Mission Control共同构建了一个完整的工具链。JDK Mission Control可对Java Flight Recorder连续收集低水平和详细的运行时信息进行高效、详细的分析。

7.5.1 事件类型

当启动时,JFR将记录运行过程中发生的一系列事件。其中包括Java层面的事件,如线程事件、锁事件,以及Java虚拟机内部的事件,如新建对象、垃圾回、即时编译事件。

按照发生时机以及持续事件来划分,JFR的事件共有四种类型,它们分别为以下四种。

  1. 瞬时事件(Instance Event),用户关心它们发生与否,例如异常、线程启动事件。
  2. 持续事件(Duration Event),用户关心它们的持续时间,例如垃圾回收事件。
  3. 计时事件(Timed Event),时长超出指定阈值的持续事件。
  4. 取样事件(Sample Event),周期性取样事件。

取样事件的其中一个常见的例子便是方法抽样(Method Sampling),即每隔一段时间统计各线程的栈轨迹。如果在这些抽样取得的轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法。

7.5.2 启动方式
方式一:使用 -XX:StartFlightRecord=参数

在运行目标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.*子命令

通过jcmd来让JFR开始收集数据、停止收集数据,或者保存所收集的数据,对应的子命令分别为JFR.startJFR.stopJFR.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
方式三:JMC的JFR插件

【JVM · 调优】监控及诊断工具_第79张图片

【JVM · 调优】监控及诊断工具_第80张图片

【JVM · 调优】监控及诊断工具_第81张图片

7.5.3 Java Flight Recorder 取样分析

要采用取样,必须先添加参数:

-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder

取样时间默认为1分钟,可自行按需调整,事件设置选为profiling,然后可以设置取样profile哪些信息,比如:

  • 加上对象数量的统计:Java Visual Machine → GC → Detailed → Object Count/Object Count after GC。
  • 方法调用采样的间隔从10ms改为1ms (但不能低于1ms,否则会影响性能):Java Visul Machine → Profiling → Method Profiling Sample/Method Sampling Information。

8. 其他工具

8.1. Flame Graphs

Flame Graphs(火焰图)

在追求极致性能的场景下,了解你的程序运行过程中CPU在干什么很重要,火焰图就是一种非常直观的展示CPU在程序整个声明周期过程中时间分配的工具。

火焰图对于现代的程序员不应该陌生,这个工具可以非常直观地显示出调用栈中的CPU消耗瓶颈。

【JVM · 调优】监控及诊断工具_第82张图片

火焰图,简单那通过x轴横条宽度来度量时间指标。y轴代表线程栈的层次。

8.2 TProfiler

案例:

使用JDK 自身提供的工具进行JVM调优可将TPS由2.5提高到20(提升了7倍),并准确定位系统瓶颈。

系统瓶颈:应用里静态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。

那么。如何在海量业务代码中准确定位这些性能代码?这里使用阿里开源工具 TProfiler 来定位这些性能代码,成功解决了GC过阈频繁的性能瓶颈,并最终在上次优化的基础上将TPS再提升了4倍,即提升到了100。

  • TProfiler 配置部署、远程操作、日志阅读都不太复杂,操作较为简单。
  • TProfiler 最重要的特性就是能够统计出指定时间段内 JVM 的Top Method,这些Top Method极有可能就是造成JVM性能瓶颈的元凶。这是其他大多数JVM调优工具所不具备的,包括JRockit Mission Control。
  • TProfiler的下载:下载地址

8.3 Btrace

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动态调整目标应用程序的类似注入跟踪代码(“字节码跟踪”)

8.4 其他常用工具

  • Youkit

  • JProbe

  • Spring Insight

7.5.1 事件类型

当启动时,JFR将记录运行过程中发生的一系列事件。其中包括Java层面的事件,如线程事件、锁事件,以及Java虚拟机内部的事件,如新建对象、垃圾回、即时编译事件。

按照发生时机以及持续事件来划分,JFR的事件共有四种类型,它们分别为以下四种。

  1. 瞬时事件(Instance Event),用户关心它们发生与否,例如异常、线程启动事件。
  2. 持续事件(Duration Event),用户关心它们的持续时间,例如垃圾回收事件。
  3. 计时事件(Timed Event),时长超出指定阈值的持续事件。
  4. 取样事件(Sample Event),周期性取样事件。

取样事件的其中一个常见的例子便是方法抽样(Method Sampling),即每隔一段时间统计各线程的栈轨迹。如果在这些抽样取得的轨迹中存在一个反复出现的方法,那么我们可以推测该方法是热点方法。

7.5.2 启动方式
方式一:使用 -XX:StartFlightRecord=参数

在运行目标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.*子命令

通过jcmd来让JFR开始收集数据、停止收集数据,或者保存所收集的数据,对应的子命令分别为JFR.startJFR.stopJFR.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
方式三:JMC的JFR插件

[外链图片转存中…(img-gPasm3Ef-1645806723516)]

[外链图片转存中…(img-GUi5DEO6-1645806723516)]

[外链图片转存中…(img-3pOsgAZq-1645806723516)]

7.5.3 Java Flight Recorder 取样分析

要采用取样,必须先添加参数:

-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder

取样时间默认为1分钟,可自行按需调整,事件设置选为profiling,然后可以设置取样profile哪些信息,比如:

  • 加上对象数量的统计:Java Visual Machine → GC → Detailed → Object Count/Object Count after GC。
  • 方法调用采样的间隔从10ms改为1ms (但不能低于1ms,否则会影响性能):Java Visul Machine → Profiling → Method Profiling Sample/Method Sampling Information。

8. 其他工具

8.1. Flame Graphs

Flame Graphs(火焰图)

在追求极致性能的场景下,了解你的程序运行过程中CPU在干什么很重要,火焰图就是一种非常直观的展示CPU在程序整个声明周期过程中时间分配的工具。

火焰图对于现代的程序员不应该陌生,这个工具可以非常直观地显示出调用栈中的CPU消耗瓶颈。

[外链图片转存中…(img-FNZhaW90-1645806723517)]

火焰图,简单那通过x轴横条宽度来度量时间指标。y轴代表线程栈的层次。

8.2 TProfiler

案例:

使用JDK 自身提供的工具进行JVM调优可将TPS由2.5提高到20(提升了7倍),并准确定位系统瓶颈。

系统瓶颈:应用里静态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题。

那么。如何在海量业务代码中准确定位这些性能代码?这里使用阿里开源工具 TProfiler 来定位这些性能代码,成功解决了GC过阈频繁的性能瓶颈,并最终在上次优化的基础上将TPS再提升了4倍,即提升到了100。

  • TProfiler 配置部署、远程操作、日志阅读都不太复杂,操作较为简单。
  • TProfiler 最重要的特性就是能够统计出指定时间段内 JVM 的Top Method,这些Top Method极有可能就是造成JVM性能瓶颈的元凶。这是其他大多数JVM调优工具所不具备的,包括JRockit Mission Control。
  • TProfiler的下载:下载地址

8.3 Btrace

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动态调整目标应用程序的类似注入跟踪代码(“字节码跟踪”)

8.4 其他常用工具

  • Youkit

  • JProbe

  • Spring Insight

你可能感兴趣的:(Java,#,JVM,java,性能优化,jvm)