Skip to content

GC 调优工具之原生命令行工具以及 Arthas

JDK 为我们提供的工具 这些工具在 windows 上,就是这些 exe,其他的平台不同。C:\Program Files\Java\jdk1.8.0_251\bin

在 linux 中,一般自带了 OpenJdk,一般情况下 JPS 等命令不能用,要么选择去安装 JPS 等插件,要么把 OpenJdk 卸载,去重新安装 Oracle 的 JDK,我推荐后者。

命令行工具

jps

列出当前机器上正在运行的虚拟机进程,JPS 从操作系统的临时目录上去找(所以有一些信息可能显示不全)。

  • -q: 仅仅显示进程,
  • -m: 输出主函数传入的参数. 下的 hello 就是在执行程序时从命令行输入的参数
  • -l: 输出应用程序主类完整 package 名称或 jar 完整名称.
  • -v: 列出 jvm 参数, -Xms20m -Xmx50m 是启动程序指定的 jvm 参数

jstat

是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。 常用参数: -class (类加载器) -compiler (JIT) -gc (GC 堆状态) -gccapacity (各区大小) -gccause (最近一次 GC 统计和原因) -gcnew (新区统计) -gcnewcapacity (新区大小) -gcold (老区统计) -gcoldcapacity (老区大小) -gcpermcapacity (永久区大小) -gcutil (GC 统计汇总) -printcompilation (HotSpot 编译统计)

比如说:我们要统计 GC,就是垃圾回收,那么只需要使用这样的命令。 jstat -gc 13616 (这个 13616 是 JVM 的进程,通过 JPS 命令得到),这样统计出来是的实时值。所以很多情况下,我们为了看变化值的,可以这么玩。

假设需要每 250 毫秒查询一次进程 13616 垃圾收集状况,一共查询 10 次,那命令应当是:jstat-gc 13616 250 10 S0C:第一个幸存区(From 区)的大小 S1C:第二个幸存区(To 区)的大小 S0U:第一个幸存区的使用大小 S1U:第二个幸存区的使用大小 EC:伊甸园(Eden)区的大小 EU:伊甸园(Eden)区的使用大小 OC:老年代大小 OU:老年代使用大小 MC:方法区大小 MU:方法区使用大小 CCSC:压缩类空间大小 CCSU:压缩类空间使用大小 YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间 FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间 GCT:垃圾回收消耗总时间

jinfo

查看和修改虚拟机的参数。命令:jinfo -sysprops 11728 jinfo –sysprops 可以查看由 System.getProperties()取得的参数 jinfo –flag 未被显式指定的参数的系统默认值 jinfo –flags(注意 s)显示虚拟机的参数

VM 参数分类

JVM 的命令行参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

该 java 命令支持各种选项,这些选项可以分为以下几类:

  • 标准选项
  • 非标准选项
  • 高级运行时选项
  1. 标准: - 开头,所有的 HotSpot 都支持

保证 Java 虚拟机(JVM)的所有实现都支持标准选项。它们用于执行常见操作,例如检查 JRE 版本,设置类路径,启用详细输出等

  1. 非标准:-X 开头,特定版本 HotSpot 支持特定命令

非标准选项是特定于 Java HotSpot 虚拟机的通用选项,因此不能保证所有 JVM 实现都支持它们,并且它们可能会发生变化。这些选项以开头-X。 -Xms30m -Xmx30m -Xss1m

  1. 高级选项:以开头-XX:

这些是开发人员选项,用于调整 Java HotSpot 虚拟机操作的特定区域,这些区域通常具有特定的系统要求,并且可能需要对系统配置参数的特权访问。也不能保证所有 JVM 实现都支持它们,并且它们可能会发生变化。

在 windows 上可以通过以下 java -XX:+PrintFlagsFinal –version 查询所有-XX 的

注意:manageable 的参数,代表可以运行时修改。

演示例子如下: 首先我们得知 PrintGC 这个 XX 参数是可以运行时修改的。

jinfo –flag -[参数] pid 可以修改参数

bash
# 查看 PrintGC 是否开启,减号为关闭状态
C:\Source\code\gitee\quickboot>jinfo -flag PrintGC 13588
-XX:-PrintGC

# 开启
C:\Source\code\gitee\quickboot>jinfo -flag +PrintGC 11728
# 再次查看,变成了加号
C:\Source\code\gitee\quickboot>jinfo -flag PrintGC 11728
-XX:+PrintGC

总结:通过 jinfo 命令,我可以在生产上临时打开一下 GC 日志或者进行一些数据的配置。(不需要重启应用条件下),也是我们去排查问题的一个关键命令。

jmap

用于生成堆转储快照(一般称为 heapdump 或 dump 文件)。jmap 的作用并不仅仅是为了获取 dump 文件,它还可以查询 finalize 执行队列、Java 堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。和 jinfo 命令一样,jmap 有不少功能在 Windows 平台下都是受限的,除了生成 dump 文件的 -dump 选项和用于查看每个类的实例、空间占用统计的-histo 选项在所有操作系统都提供之外,其余选项都只能在 Linux/Solaris 下使用。

-heap 打印 heap 的概要信息

jmap –heap <pid>

Heap Configuration: ##堆配置情况,也就是 JVM 参数配置的结果[平常说的 tomcat 配置 JVM 参数,就是在配置这些]
    MinHeapFreeRatio = 40 ##最小堆使用比例
    MaxHeapFreeRatio = 70 ##最大堆可用比例
    MaxHeapSize = 2147483648 (2048.0MB) ##最大堆空间大小
    NewSize = 268435456 (256.0MB) ##新生代分配大小
    MaxNewSize = 268435456 (256.0MB) ##最大可新生代分配大小
    OldSize = 5439488 (5.1875MB) ##老年代大小
    NewRatio = 2 ##新生代比例
    SurvivorRatio = 8 ##新生代与 suvivor 的比例
    PermSize = 134217728 (128.0MB) ##perm 区永久代大小
    MaxPermSize = 134217728 (128.0MB) ##最大可分配 perm 区 也就是永久代大小
Heap Usage: ##堆使用情况【堆内存实际的使用情况】
    New Generation (Eden + 1 Survivor Space): ##新生代(伊甸区 Eden 区 + 幸存区 survior(1+2)空间)
    capacity = 241631232 (230.4375MB) ##伊甸区容量
    used = 77776272 (74.17323303222656MB) ##已经使用大小
    free = 163854960 (156.26426696777344MB) ##剩余容量
    32.188004570534986% used ##使用比例
Eden Space: ##伊甸区
    capacity = 214827008 (204.875MB) ##伊甸区容量
    used = 74442288 (70.99369812011719MB) ##伊甸区使用
    free = 140384720 (133.8813018798828MB) ##伊甸区当前剩余容量
    34.65220164496263% used ##伊甸区使用情况
From Space: ##survior1 区
    capacity = 26804224 (25.5625MB) ##survior1 区容量
    used = 3333984 (3.179534912109375MB) ##surviror1 区已使用情况
    free = 23470240 (22.382965087890625MB) ##surviror1 区剩余容量
    12.43827838477995% used ##survior1 区使用比例
To Space: ##survior2 区
    capacity = 26804224 (25.5625MB) ##survior2 区容量
    used = 0 (0.0MB) ##survior2 区已使用情况
    free = 26804224 (25.5625MB) ##survior2 区剩余容量
    0.0% used ## survior2 区使用比例
PS Old Generation: ##老年代使用情况
    capacity = 1879048192 (1792.0MB) ##老年代容量
    used = 30847928 (29.41887664794922MB) ##老年代已使用容量
    free = 1848200264 (1762.5811233520508MB) ##老年代剩余容量
    1.6416783843721663% used ##老年代使用比例

-histo 打印每个 class 的实例数目,内存占用,类全名信息.

jmap –histo <pid>

jmap –histo:live <pid> 如果 live 子参数加上后,只统计活的对象数量.

但是这样显示太多了,一般在 linux 上会这么操作, Windows 不支持

bash
jmap –histo:live 1196 | head -20

(这样只会显示排名前 20 的数据)

不太重要的参数

-finalizerinfo 打印正等候回收的对象的信息,还有 jmap –clstats 这个命令最好也不要去使用

-dump 生成的堆转储快照(比较重要)

jmap -dump:live,format=b,file=heap.bin <pid>

Sun JDK 提供 jhat(JVM Heap Analysis Tool)命令与 jmap 搭配使用,来分析 jmap 生成的堆转储快照。

jhat(不推荐使用)

jhat dump 文件名

后屏幕显示“Server is ready.”的提示后,用户在浏览器中键入 http://localhost:7000/就可以访问详情

使用 jhat 可以在服务器上生成堆转储文件分析(一般不推荐,毕竟占用服务器的资源,比如一个文件就有 1 个 G 的话就需要大约吃一个 1G 的内存资源)

jstack

(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。 在代码中可以用 java.lang.Thread 类的 getAllStackTraces() 方法用于获取虚拟机中所有线程的 StackTraceElement 对象。使用这个方法可以通过简单的几行代码就完成 jstack 的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈。(并发编程中的线程安全课程中有具体的案例)

一般来说 jstack 主要是用来排查是否有死锁的情况,这块内容在并发编程(包括二期)中有详细的讲解。

命令工具总结

生产服务器推荐开启

  • -XX:-HeapDumpOnOutOfMemoryError 默认关闭,建议开启,在 java.lang.OutOfMemoryError 异常出现时,输出一个 dump 文件,记录当时的堆内存快照。
  • -XX:HeapDumpPath=./java_pid<pid>.hprof 用来设置堆内存快照的存储文件路径,默认是 java 进程启动位置。

调优之前开启、调优之后关闭

  • -XX:+PrintGC 调试跟踪之打印简单的 GC 信息参数:
  • -XX:+PrintGCDetails, +XX:+PrintGCTimeStamps 打印详细的 GC 信息
  • -Xlogger:logpath 设置 gc 的日志路,如: -Xlogger:log/gc.log, 将 gc.log 的路径设置到当前目录的 log 目录下.

应用场景:将 gc 的日志独立写入日志文件,将 GC 日志与系统业务日志进行了分离,方便开发人员进行追踪分析。

考虑使用

  • -XX:+PrintHeapAtGC, 打印堆信息

    • 参数设置: -XX:+PrintHeapAtGC
    • 应用场景: 获取 Heap 在每次垃圾回收前后的使用状况
  • -XX:+TraceClassLoading

    • 参数方法: -XX:+TraceClassLoading
    • 应用场景: 在系统控制台信息中看到 class 加载的过程和具体的 class 信息,可用以分析类的加载顺序以及是否可进行精简操作。
  • -XX:+DisableExplicitGC 禁止在运行期显式地调用 System.gc()

可视化工具

JMX(Java Management Extensions,即 Java 管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX 可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。

管理远程进程需要在远程程序的启动参数中增加: -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

JConsole

可以查看堆,内存,线程等信息。

JVisualvm

类似于 JConsole, 但这个工具可以安装额外的插件。如:Visual GC 插件,可以查看新生代,老年代等内存变化。

插件中心地址: https://visualvm.github.io

但是注意版本问题,不同的 JDK 所带的 visualvm 是不一样的,下载插件时需要下对应的版本。 一般来说,这个工具是本机调试用,一般生产上来说,你一般是用不了的(除非启用远程连接)

Arthas(阿尔萨斯)

官方文档参考:https://alibaba.github.io/arthas/ Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。

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

下载和安装

下载文档:https://arthas.aliyun.com/doc/download.html 下载地址:https://arthas.aliyun.com/download/latest_version?mirror=aliyun 不需要安装,就是一个 jar 包

快速入门

方法一:直接 java -jar arthas-boot.jar。选择 attach 的进程绑定

bash
# 启动 jar
C:\Users\mengweijin\Desktop\arthas-packaging-3.5.0-bin>java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.5.0
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 11728 com.github.mengweijin.mybatisplus.demo.MybatisPlusDemoApplication
  [2]: 14608 org.jetbrains.jps.cmdline.Launcher
  [3]: 13588
# 绑定Java 进程,这里输入数字:1
1
[INFO] arthas home: C:\Users\mengweijin\Desktop\arthas-packaging-3.5.0-bin
[INFO] Try to attach process 11728
[INFO] Found java home from System Env JAVA_HOME: C:\Program Files\Java\jdk1.8.0_251
[INFO] Attach process 11728 success.
[INFO] arthas-client connect 127.0.0.1 3658
  ,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---.
 /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-'
|  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-.
|  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |
`--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----'


wiki       https://arthas.aliyun.com/doc
tutorials  https://arthas.aliyun.com/doc/arthas-tutorials.html
version    3.5.0
main_class
pid        11728
time       2021-03-16 19:49:25

# 绑定成功,这里的命令行前缀已经变成了 arthas
[arthas@11728]$

方法二:通过 jps 命令快速查找 java 进程,再次直接绑定 java -jar arthas-boot.jar pid

常用命令

dashboard

注意在 arthas 中,有 tab 键填充功能,所以比较好用。但是这个界面是实时刷新的,一般 5s 刷新一次,使用 q 键退出刷新(没有退出 arthasq)

bash
# 输入命令,即可打开 dashboard。5s 刷新一次
[arthas@11728]$ dashboard

thread

这个命令和 jstack 很相似,但是功能更加强大,主要是查看当前 JVM 的线程堆栈信息。同时可以结合使用 thread –b 来进行死锁的排查死锁。 参数解释:

  • -n 指定最忙的前 n 个线程并打印堆栈
  • -b 找出阻塞当前线程的线程
  • -i 指定 cpu 占比统计的采样间隔,单位为毫秒

实战演示

  • thread –h 显示帮助
  • thread –b 找出阻塞当前线程的线程
  • thread -i 1000 -n 3 每过 1000 毫秒进行采样,显示最占 CPU 时间的前 3 个线程
  • thread --state WAITING 查看处于等待状态的线程
bash
[arthas@11728]$ thread -b
No most blocking thread found!

如果有死锁,会有红色的字提醒着,这个阻塞的线程已经被另外一个线程阻塞。

jvm

显示 jvm 相关信息。

Jad

反编译指定已加载类的源码

bash
[arthas@11728]$ jad com.github.mengweijin.mybatisplus.demo.entity.User

ClassLoader:
+-sun.misc.Launcher$AppClassLoader@18b4aac2
  +-sun.misc.Launcher$ExtClassLoader@4b4523f8

Location:
/C:/Source/code/gitee/quickboot/sample-quickboot-mybatis-plus/target/classes/
# 此处省略反编译后的文件内容。

trace

使用 trace 命令可以跟踪统计方法耗时。 继续跟踪耗时高的方法,然后再次访问

比如使用一个 Springboot 项目控制层 getUser 方法调用了 userService.get(uid);,这个方法中分别进行 check、service、redis、mysql 等操作操作。就可以根据这个命令跟踪出来哪里的耗时最长。

bash
[arthas@11728]$ trace com.github.mengweijin.mybatisplus.demo.controller.UserController getUser
Press Q or Ctrl+C to abort.
# 然后在浏览器中请求接口,这里就可以接收到监控到的信息
Affect(class count: 2 , method count: 2) cost in 532 ms, listenerId: 1
`---ts=2021-03-16 20:35:35;thread_name=http-nio-18080-exec-5;id=21;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@1322b575
    `---[37.9038ms] com.github.mengweijin.mybatisplus.demo.controller.UserController$$EnhancerBySpringCGLIB$$d68ae9c2:getUser()
        `---[36.6589ms] org.springframework.cglib.proxy.MethodInterceptor:intercept() #95
            `---[19.222ms] com.github.mengweijin.mybatisplus.demo.controller.UserController:getUser()
                `---[18.7934ms] com.github.mengweijin.mybatisplus.demo.service.UserService:list() #40

monitor

每 10 秒统计一次 com.github.mengweijin.mybatisplus.demo.controller.UserController 类的 getUser 方法执行情况

bash
[arthas@11728]$ monitor -c 10 com.github.mengweijin.mybatisplus.demo.controller.UserController getUser
Press Q or Ctrl+C to abort.
# 然后在浏览器中请求接口,这里就可以接收到监控到的信息
Affect(class count: 2 , method count: 2) cost in 176 ms, listenerId: 2
 timestamp         class                       method                     total    success   fail     avg-rt(ms)  fail-rate
-----------------------------------------------------------------------------------------------------------------------
 2021-03-16 20:45  com.github.mengweijin.myba  getUser                    1        1         0        9.10     0.00%
 :22               tisplus.demo.controller.Us
                   erController$$EnhancerBySp
                   ringCGLIB$$d68ae9c2
 2021-03-16 20:45  com.github.mengweijin.myba  getUser                    1        1         0        5.30     0.00%
 :22               tisplus.demo.controller.Us
                   erController

watch

观察方法的入参出参信息。查看入参和出参

bash
[arthas@11728]$ watch com.github.mengweijin.mybatisplus.demo.controller.UserController getUserById '{params[0],returnObj}'
Press Q or Ctrl+C to abort.
Affect(class count: 2 , method count: 2) cost in 189 ms, listenerId: 4
method=com.github.mengweijin.mybatisplus.demo.controller.UserController.getUserById location=AtExit
ts=2021-03-16 20:50:16; [cost=33.4166ms] result=@ArrayList[
    @Long[1], # 入参
    null, # 返回值
]

命令汇总

命令介绍
dashboard当前系统的实时数据面板
thread查看当前 JVM 的线程堆栈信息
watch方法执行数据观测
trace方法内部调用路径,并输出方法路径上的每个节点耗时
stack输出当前方法被调用的调用路径
tt方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测
monitor方法执行监控
jvm查看当前 JVM 信息
vmoption查看,更新 JVM 诊断相关的参数
sc查看 JVM 已加载的类信息
sm查看已加载类的方法信息
jad反编译指定已加载类的源码
classloader查看 classloader 的继承树, urls,类加载信息
heapdump类似 jmap 命令的 heap dump 功能

动态追踪技术底层分析

动态追踪技术是一个可以不用重启线上 java 项目来进行问题排查的技术,比如前面讲的 Arthas 就属于一种动态追踪的工具。它里面提供的 monitor 还有 watch 等命令就是动态的追踪技术。

当然我们学技术要知其然还要知其所以然,Arthas 工具的基础,就是 Java Agent 技术,可以利用它来构建一个附加的代理程序,用来协助检测性能,还可以替换一些现有功能,甚至 JDK 的一些类我们也能修改,有点像 JVM 级别的 AOP 功能。

Java Agent 技术

既然作为 JVM 的 AOP,就必须要有 AOP 的功能,所以 Java Agent 提供了两个类似于 AOP 的方法

  • 一个方法叫做 premain 方法,可以在 main 运行之前的进行一些操作(Java 入口是一个 main 方法)
  • 一个是 agentmain 方法,是控制类运行时的行为(Arthas 使用的就是这种) 但在一个 JVM 中,只会调用一个

要构建一个 agent 程序,大体可分为以下步骤:

  • 使用字节码增强工具,编写增强代码;
  • 在 manifest 中指定 Premain-Class/Agent-Class 属性;
  • 使用参数加载或者使用 attach 方式改变 app 项目中的内容;

Agentmain

这种模式一般用在一些诊断工具上。使用 jdk/lib/tools.jar 中的工具类中的 Attach API,可以动态的为运行中的程序加入一些功能。它的主要运行步骤如下:

  • 获取机器上运行的所有 JVM 进程 ID;
  • 选择要诊断的 jvm;
  • 将 jvm 使用 attach 函数链接上;
  • 使用 loadAgent 函数加载 agent,动态修改字节码;
  • 卸载 jvm。
java
public static void main(String[] args) throws IOException, AttachNotSupportedException {
    List<VirtualMachineDescriptor> list = VirtualMachine.list();
    for (VirtualMachineDescriptor vmd: list) {
        if(vmd.displayName().endsWith("MainRun")){
            VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
            String version = virtualMachine.getSystemProperties().getProperty("java.version");
            // virtualMachine.loadAgent("xxx.jar", "...");
            System.out.println("Version: " + version);
            virtualMachine.detach();
        }
    }
}

Java Attach API

Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它, 开发者可以方便的监控一个 JVM,运行一个外加的代理程序。Attach API 只有 2 个主要的类,都在 com.sun.tools.attach 包(在 jdk 的 lib 目录下 tools.jar 里面)里面:

VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动 作的相反行为,从 JVM 上面解除一个代理)等等;

VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。

Java Attach API 是一个 API 接口,JDK 提供的,它可以将应用程序连接到另一个目标虚拟机。然后,您的应用程序可以将代理应用程序装入目标虚拟机,例如,用于执行监视状态之类的任务。 JVM Attach API 功能上非常简单,主要功能如下:

  • Attach 到其中一个 JVM 上,建立通信管道
  • 让目标 JVM 加载 Agent

使用入门: 不过我在 IDE 使用它的时候,要注意到,这个 API 属于 JDK 的包,所以使用它项目中必须要引用它。 首先项目中引用到 tools.jar(这个包在 JDK 的安装目录的 lib 下面)

  1. JVM 进程号,通过 jps 命令获取
  2. 使用 VirtualMachine.attach()向目标 JVM ”附着”
  3. 利用 VirtualMachine 和 VirtualMachineDescriptor 就可以获取 VM 相关的信息。
java
public static void main(String[] args) throws IOException, AttachNotSupportedException {
    // VM进程号,通过 jps 命令获取
    // attach 向目标 JVM 附着(Attach)代理工具程序
    VirtualMachine virtualMachine = VirtualMachine.attach("12345");
    // get system properties in target VM
    String version = virtualMachine.getSystemProperties().getProperty("java.version");
    System.out.println("Version: " + version);
    // VirtualMachineDescriptor 是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能
    List<VirtualMachineDescriptor> list = VirtualMachine.list();
    // 从 JVM 上面解除代理
    virtualMachine.detach();
}