JVM 之 命令行工具

摘要

JVM 的参数(三类)

  • 标准参数: 以-开头,所有 HotSpot 都⽀持。例如java -version。这类参数可以使⽤java -help 或者java -?全部打印出来

  • ⾮标准参数: 以-X开头,是特定 HotSpot版本⽀持的指令。例如java -Xms200M -Xmx200M。这类指令可以⽤java -X 全部打印出来。

  • 不稳定参数: 这也是 JVM调优的噩梦。以-XX 开头,这些参数是跟特定HotSpot版本对应的,很有可能换个版本就没有了。详细的⽂档资料也特别少。JDK8 中的以下⼏个指令可以帮助开发者了解 JDK8 中的这⼀类不稳定参数。

1
2
3
4
java -XX:+PrintFlagsInitial -version   # 所有参数的默认值
java -XX:+PrintFlagsFinal -version # 所有参数最终⽣效的值。
java -XX:+UnlockExperimentalVMOptions -XX:+PrintFlagsFinal -version # 所有参数最终⽣效的值,包含实验性参数。
java -XX:+PrintCommandLineFlags -version # 当前命令生效的值,可以看到是⽤的哪种GC。 JDK1.8默认⽤的ParallelGC

运行 java -XX:+UnlockExperimentalVMOptions -XX:+PrintFlagsFinal -version 返回值说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Global flags]
intx ActiveProcessorCount = -1 {product}
uintx AdaptiveSizeDecrementScaleFactor = 4 {product}
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AdaptiveSizePausePolicy = 0 {product}
uintx AdaptiveSizePolicyCollectionCostMargin = 50 {product}
uintx AdaptiveSizePolicyInitializingSteps = 20 {product}
uintx AdaptiveSizePolicyOutputInterval = 0 {product}
uintx AdaptiveSizePolicyWeight = 10 {product}
uintx AdaptiveSizeThroughPutPolicy = 0 {product}
uintx AdaptiveTimeWeight = 25 {product}
bool AdjustConcurrency = false {product}
bool AggressiveHeap = false {product}
bool AggressiveOpts = false {product}
intx AliasLevel = 3 {C2 product}
………………………………
# 格式:flag类型,flag名称,flag值,flag来源与作用域修饰符
  • flag类型:

jdk8

标识符 含义 描述说明
intx 有符号整数(int extended) 用于表示带符号整数型参数
uintx 无符号整数(unsigned int extended) 表示非负整数型参数
uint64_t 无符号 64 位整数 适用于需要更大整数值的参数,比如内存大小、纳秒时间戳等
bool 布尔型 truefalse,表示开关型参数。+表示开启,-表示关闭。 如 -XX:+UseG1GC,表示开启 G1 回收器功能。
double 双精度浮点型 用于表示浮点数值类型的 JVM 参数
ccstr 常量 C 字符串指针 表示字符串参数(不可变)
ccstrlist C 字符串列表 表示以逗号分隔的字符串列表

jdk11后出现更多的类型

标识符 含义 描述说明
int 有符号整数,通常是 32 位 JVM 内部使用的普通 int 参数
uint 无符号整数,通常是 32 位 少量用于特殊场景的参数
size_t 内存大小 表示内存大小参数(单位一般为字节),jdk11后出现,原来是 uintx
  • flag来源与作用域修饰符

jdk8

标记 含义
{product} 正式产品参数(用户可用,可通过 -XX: 进行设置)
{pd product} 平台相关的正式参数(platform-dependent),在某些操作系统/CPU 上可用
{C1 product} 仅在使用 C1 编译器(优化编译器) 时有效的参数
{C2 product} 仅在使用 C2 编译器(优化编译器) 时有效的参数
{manageable} 可通过 JMX 动态管理的参数(运行时可调整)
{ARCH product} 与 CPU 架构相关的产品参数(如 x86、ARM 等)
{lp64_product} 仅在 64 位 JVM 上才有效的产品参数
{experimental} 实验性的 参数,可能在将来的版本中删除或更改。需要开启 -XX:+UnlockExperimentalVMOptions

jdk11后出现更多的类型

标记 含义
{C1 pd product} 平台相关的正式参数,仅在使用 C1 编译器(优化编译器) 时有效的参数
{C2 pd product} 平台相关的正式参数,仅在使用 C2 编译器(优化编译器) 时有效的参数
{JVMCI product} 此参数适用于 JVMCI 或 Graal 编译器相关功能,jdk21添加

另外,jdk11+ 中,最后还会多出一列,其作用是说明该参数的设置来源

来源标识 含义说明
{default} 使用的是该参数的默认值(没有被用户或系统设置)
{command line} 用户通过命令行显式设置的参数(如 -XX:+UseG1GC
{ergonomic} JVM 根据系统环境自动选择的值(自适应设置)

jps: 查看当前jvm中的进程

1
2
3
4
5
6
7
8
# 查看java进程ID
jps
# 查看java进程信息,包括ID和启动类或jar的名称
jps -l
# 查看启动java进程时传递给jvm的参数设置
jps -v
# 查看java进程信息,同时显示启动java进程时传递给jvm的参数设置
jps -lv

jinfo: 查看JVM参数

  • jinfo 是 JDK 自带的命令行工具之一,用于 查看和修改正在运行中的 Java 进程的配置信息,主要包括 JVM 参数、系统属性等信息。

  • 在 Java 9+ 之后被标记为 deprecated,建议改用 jcmd 替代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 查看正在运行的 JVM 参数
jinfo <PID>
# jcmd 命令,分成三个
jcmd <pid> VM.flags # VM Flags
jcmd <pid> VM.system_properties # System Properties
jcmd <pid> VM.command_line # Command Line

# 查看指定进程的系统属性(-sysprops)
jinfo -sysprops <PID>
# jcmd 命令
jcmd <pid> VM.system_properties

# 动态修改 JVM 参数(仅支持部分参数)
jinfo -flag [+|-]<flagname> <pid>
## 例:打开 GC 日志
jinfo -flag +PrintGC <pid>

# 检查是否开启了某个 JVM 特性(如 UseG1GC)
jinfo -flag UseG1GC <pid>

jstat: 查看指定JAVA进程的运行状态

1
2
3
4
5
6
7
8
9
# 查看运行状态
jstat -gc <PID>
# 查看运行状态,每隔5000毫秒输出一次,共输出20次
jstat -gc <PID> 5000 20
# 输出
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
512.0 512.0 0.0 224.0 29696.0 27138.2 105984.0 96531.8 122368.0 114450.4 15360.0 14018.9 733 5.099 4 0.833 5.931
512.0 512.0 0.0 224.0 29696.0 27203.9 105984.0 96531.8 122368.0 114450.4 15360.0 14018.9 733 5.099 4 0.833 5.931
512.0 512.0 0.0 224.0 29696.0 27220.4 105984.0 96531.8 122368.0 114450.4 15360.0 14018.9 733 5.099 4 0.833 5.931
  • 内存区统计(单位:KB)

列名 含义说明
S0C Survivor 0 区的容量(KB)
S1C Survivor 1 区的容量(KB)
S0U Survivor 0 区已使用内存(KB)
S1U Survivor 1 区已使用内存(KB)
EC Eden 区的容量(KB)
EU Eden 区已使用内存(KB)
OC Old Generation(老年代)的容量(KB)
OU Old Generation 已使用内存(KB)
MC Metaspace(元空间)的容量(KB)
MU Metaspace 已使用内存(KB)
CCSC Compressed Class Space 的容量(KB)
CCSU Compressed Class Space 已使用内存(KB)
  • GC 次数与耗时(从应用程序启动到采样时)

列名 含义说明
YGC Young GC(Minor GC) 的累计次数
YGCT Young GC 累计耗时(单位:秒)
FGC Full GC 的累计次数
FGCT Full GC 累计耗时(单位:秒)
GCT GC 总耗时(YGCT + FGCT,总和)

jstack: 查看指定JAVA进程中各线程的调用堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 打印线程的标准栈信息(栈帧 + 线程状态)
jstack <PID>
# 在标准输出基础上,额外打印线程拥有的锁(monitor)和 waited on 锁对象信息
jstack -l <PID>
# 输出到文件
jstack -l <PID> > jstack.tdump # 这个文件名后缀只是为了方便 jvisualvm 查看

# 输出
"idle-connection-reaper" #68 daemon prio=5 os_prio=0 tid=0x00007f38b6ae2800 nid=0x62e9 waiting on condition [0x00007f385b0f5000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at software.amazon.awssdk.http.apache.internal.conn.IdleConnectionReaper$ReaperTask.run(IdleConnectionReaper.java:151)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:750)

Locked ownable synchronizers:
- <0x00000000c561cfd8> (a java.util.concurrent.ThreadPoolExecutor$Worker)
  • jstack输出内容含义

字段 含义
"idle-connection-reaper" 线程名称。这个线程是 AWS SDK 中用于清理空闲连接的后台线程。
#68 线程 ID(Java 层分配的编号)。
daemon 表示这是一个 守护线程。JVM 退出时不会等待守护线程运行结束。
prio=5 Java 层的线程优先级(默认 1–10,5 是默认)。
os_prio=0 操作系统层线程优先级(取决于操作系统和 JVM 的实现)。
tid=0x00007f38b6ae2800 线程 ID(Java 层使用的地址标识)。
nid=0x62e9 Native ID,操作系统分配的线程 ID(十六进制表示),可以通过命令printf "%d\n" 0x62e9转换为10进制。
ps -Lp <PID>top -Hp <PID> 展示的线程id是10进制
waiting on condition 线程状态说明:正在等待某种条件,一般指 sleep()wait() 等。
[0x00007f385b0f5000] 栈帧的内存地址。
java.lang.Thread.State: TIMED_WAITING (sleeping) 线程状态。
at java.lang.Thread.sleep(Native Method)
…………………………
线程调用栈信息,包括调用方法、调用参数、调用栈帧。
  • 另外还有如下信息,表示此线程持有的可拥有同步器(例如 ReentrantLock),具体内容如下:

1
2
Locked ownable synchronizers:
- <0x00000000c561cfd8> (a java.util.concurrent.ThreadPoolExecutor$Worker)
内容 含义
<0x00000000c561cfd8> 持有锁的对象地址。可以在其他线程中查找这个地址,以判断死锁等问题。
(a ThreadPoolExecutor$Worker) 表示该锁属于 ThreadPoolExecutor 的某个 Worker 线程(这是线程池的工作线程)。
  • 线程状态

状态 含义 常见原因 / 示例方法 示例 jstack 输出
RUNNABLE 线程正在运行或准备运行,等待 CPU 时间片。 线程活跃运行中 "Thread-0" #1 prio=5 os_prio=0 tid=0x00007f8d9c001000 nid=0x1a runnable [0x00007f8dbf7fe000]
BLOCKED (on object monitor) 等待获取对象的监视器锁(同步锁),即等待进入同步块或方法。 synchronized 同步块或方法 "Thread-1" #2 prio=5 os_prio=0 tid=0x00007f8d9c002000 nid=0x1b waiting for monitor entry [0x00007f8dbeaff000]
WAITING (on object monitor) 无限期等待其他线程执行某操作。 Object.wait()
Thread.join()
LockSupport.park()
Condition.await()
"Thread-2" #3 prio=5 os_prio=0 tid=0x00007f8d9c003000 nid=0x1c waiting on condition [0x00007f8dbebff000]
TIMED_WAITING (on object monitor) 等待固定时间,直到条件满足或超时。 Object.wait(long)
Thread.sleep(long)
Condition.awaitNanos(long)
DelayQueue.take()
"Thread-3" #4 prio=5 os_prio=0 tid=0x00007f8d9c004000 nid=0x1d timed_waiting [0x00007f8dbebff000]
TERMINATED 线程已运行完毕并退出(通常不在 jstack 中显示)。 - -

使用jstack检查死锁

  • 死锁是指多个线程互相等待对方释放锁,导致无法继续运行的情况。

直接查找 Found one Java-level deadlock 提示

1
2
3
4
5
6
7
8
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000005c0a098 (object 0x00000000d5c10a70, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x0000000005c0a128 (object 0x00000000d5c10aa0, a java.lang.Object),
which is held by "Thread-1"
  • 表示两个线程互相等待对方释放锁,造成死锁。

  • 目前只支持找出 synchronized 关键字阻塞住的线程,如果是 java.util.concurrent.Lock 目前还不支持。

手动判断死锁的特征(如果没有上面的提示)

  • 如果 JVM 没有显式提示,你可以通过以下死锁特征进行手动识别:

特征 描述
线程处于 BLOCKED 状态 线程被锁阻塞在 monitor 上。
waiting to locklocked 出现交叉 一个线程正在等待另一个线程持有的锁,而另一个线程也在等待当前线程的锁。
没有能继续执行的线程 多个线程都处于 BLOCKED 状态,且相互依赖。

在 Java 8 之后支持获取 JVM 内部线程

  • VM 内部线程包括下面几种:
线程类型 示例名称
JIT(Just-In-Time)编译线程 C1 CompilerThread0, C2 CompilerThread0
GC 线程 GC Thread0, G1 Young RemSet Sampling
其它内部线程 VM Periodic Task Thread, VM Thread, Service Thread
  • 自 JDK 7 及以上版本开始,HotSpot JVM 默认启用了:分层编译(Tiered Compilation),即同时使用 C1 和 C2 编译器。

编译器 特性 用途
C1(Client Compiler) 编译速度快,优化少 程序启动时使用,提升启动速度
C2(Server Compiler) 编译速度慢,优化强 热点代码达到一定门槛后使用,提升长期运行性能

JVM 启动后,先解释执行字节码;热点代码首先由 C1 编译器处理;如果进一步变热,交由 C2 编译器优化;整个过程由 JVM 自动管理,不需要手动干预。

  • 是否可以指定只用某个编译器?是的,你可以用 JVM 参数控制,但真没必要。

参数 含义
-client 使用 C1 编译器(适合启动快)
-server 使用 C2 编译器(适合长时间运行的服务)
-XX:-TieredCompilation 禁用分层编译,仅使用 -client-server 指定的编译器

jmap: 分析运行中 Java 进程的内存使用情况

  • jmap 比较消耗内存,所以不推荐生产环境使用。

jmap -histo <pid>: 查看类实例数量及占用内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ jmap -histo 25238
25238:

num #instances #bytes class name
----------------------------------------------
1: 106175 10838440 [C
2: 94344 3019008 java.util.concurrent.ConcurrentHashMap$Node
3: 105529 2532696 java.lang.String
4: 11203 2496608 [B
5: 20178 2430136 [Ljava.lang.Object;
6: 21642 2399904 java.lang.Class
…………………………
Total 857252 51328200

# num: 序号
# instances: 实例数量
# bytes: 实例字节数
# class name: 类名, [C is a char[],[S is a short[],[I is a int[],[B is a byte[],[L is a List
# Total: 总数

jmap -heap <pid>: 查看堆使用情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
$ jmap -heap 25238
Attaching to process ID 25238, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.371-b11

using thread-local object allocation.
Parallel GC with 2 thread(s)

Heap Configuration: # 堆配置
MinHeapFreeRatio = 0 # 最小空闲百分比
MaxHeapFreeRatio = 100 # 最大空闲百分比
MaxHeapSize = 1010827264 (964.0MB) # 最大堆大小
NewSize = 21495808 (20.5MB) # 新生代大小
MaxNewSize = 336592896 (321.0MB) # 新生代最大大小
OldSize = 43515904 (41.5MB) # 老年代大小
NewRatio = 2 # 新生代与老年代比例
SurvivorRatio = 8 # 幸存者比例
MetaspaceSize = 21807104 (20.796875MB) # 元空间大小
CompressedClassSpaceSize = 1073741824 (1024.0MB) # 压缩类空间大小,用于存储所有 类指针(Klass pointer)
MaxMetaspaceSize = 17592186044415 MB # 元空间最大大小
G1HeapRegionSize = 0 (0.0MB) # G1堆大小

Heap Usage: # 堆使用情况
PS Young Generation # 年轻生代
Eden Space: # Eden 区
capacity = 30932992 (29.5MB) # Eden 区大小
used = 16705104 (15.931228637695312MB) # Eden 区使用情况
free = 14227888 (13.568771362304688MB) # Eden 区剩余情况
54.00416487354343% used # Eden 区使用百分比
From Space: # 幸存者区 survivor0
capacity = 524288 (0.5MB)
used = 0 (0.0MB)
free = 524288 (0.5MB)
0.0% used
To Space: # 幸存者区 survivor1
capacity = 524288 (0.5MB)
used = 0 (0.0MB)
free = 524288 (0.5MB)
0.0% used
PS Old Generation # 老年代
capacity = 193462272 (184.5MB)
used = 43292112 (41.28657531738281MB)
free = 150170160 (143.2134246826172MB)
22.377547597497458% used

38722 interned Strings occupying 4184168 bytes. # 堆中字符串数量及占用内存

jmap -dump:live,format=b,file=<file> <pid>: 创建一个快照,并保存到文件中

1
2
3
4
# 创建一个快照,并保存到当前目录下
$ jmap -dump:live,format=b,file=test.hprof 25238
Dumping heap to /tmp/test.hprof ...
Heap dump file created

jcmd: 可以替代jmap命令,jdk1.8+推荐使用

  • jcmdJDK 的一个命令行工具,用于管理 JVMjcmd 可以查看 JVM 的运行状态、查看堆内存信息、触发 GC、查看线程信息、查看类信息等等。

  • 生产环境不建议使用jmap,因其会占用大量内存,推荐使用 jcmd

jcmd <pid> GC.heap_info: 查看堆使用情况

  • 类似 jmap -heap <pid>

1
2
3
4
5
6
7
8
9
10
$ jcmd 25238 GC.heap_info
25238:
PSYoungGen total 30208K, used 23593K [0x00000000ebf00000, 0x00000000edd80000, 0x0000000100000000)
eden space 29696K, 78% used [0x00000000ebf00000,0x00000000ed5d2560,0x00000000edc00000)
from space 512K, 43% used [0x00000000edd00000,0x00000000edd38000,0x00000000edd80000)
to space 512K, 0% used [0x00000000edc80000,0x00000000edc80000,0x00000000edd00000)
ParOldGen total 105984K, used 96547K [0x00000000c3c00000, 0x00000000ca380000, 0x00000000ebf00000)
object space 105984K, 91% used [0x00000000c3c00000,0x00000000c9a48f10,0x00000000ca380000)
Metaspace used 114450K, capacity 122262K, committed 122368K, reserved 1157120K
class space used 14018K, capacity 15294K, committed 15360K, reserved 1048576K

jcmd <pid> GC.class_histogram: 查看类加载情况

  • 类似 jmap -histo <pid>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ jcmd 25238 GC.class_histogram
25238:

num #instances #bytes class name
----------------------------------------------
1: 106175 10838440 [C
2: 94344 3019008 java.util.concurrent.ConcurrentHashMap$Node
3: 105529 2532696 java.lang.String
4: 11203 2496608 [B
5: 20178 2430136 [Ljava.lang.Object;
6: 21642 2399904 java.lang.Class
…………………………
Total 857252 51328200

# num: 序号
# instances: 实例数量
# bytes: 实例字节数
# class name: 类名, [C is a char[],[S is a short[],[I is a int[],[B is a byte[],[L is a List
# Total: 总数

jcmd <pid> GC.heap_dump /path/to/heap.hprof: 导出 heap dump

  • 替代 jmap -dump:format=b,file=heap.hprof <pid>,但仍会有停顿(触发FullGC),dump 过程慢,生产环境慎重使用

1
2
3
4
5
6
7
$ jcmd 25238 GC.heap_dump ./heap.hprof
25238:
Heap dump file created
# 注意这里使用的是相对目录,./heap.hprof,但此时 "./" 并不是运行命令时所在的目录,而是指 Java 进程的工作目录,如果你不知道工作目录,可以通过如下命令查看
jcmd <pid> VM.system_properties | grep "user.dir"
# linux下也可以通过如下命令查看
ls -l /proc/<pid>/cwd
  • 因为生产环境不推荐使用 jmapjcmd 导出 heap dump,所以我们需要在生产环境中配置如下jvm参数

1
2
-XX:+HeadDumpOnOutOfMemoryError # 内存溢出(OOM)时会自动保存堆内存快照文件
-XX:HeapDumpPath=/path/to/dump.hprof # 指定堆内存快照文件保存路径,如果不配置,则默认保存在 Java 进程的工作目录下,文件名称默认为 java_<pid>.hprof

jhat: 分析 Java Heap Dump 文件(.hprof)的工具

  • jhat(Java Heap Analysis Tool)是 JDK 附带的一个用于**分析 Java Heap Dump 文件(.hprof)**的工具。它可以将堆快照作为 HTTP 服务加载,并允许你通过浏览器交互式地查看和查询对象信息。

  • 不过需要注意的是:
    🔺 从 JDK 9 起,jhat 已被官方废弃,推荐使用更强大的工具如 VisualVMEclipse MATjcmd + 外部工具

1
2
3
4
# 默认端口7000
jhat <heap dump file>
# 指定端口
jhat -port 8000 <heap dump file>

jvisualvm: 图形化的 JVM 监控与分析工具

  • jvisualvm(全称:Java VisualVM)是 Java 官方提供的一款 图形化的 JVM 监控与分析工具,用于观察、分析和调试正在运行的 Java 程序。它非常适合开发和测试环境中进行内存分析、线程分析、GC 行为观察等任务。

  • 功能概览

功能 描述
进程监控 查看本地或远程 JVM 进程的内存、CPU、线程等运行状态
内存使用分析 查看堆内存使用情况、GC 次数和时间、类实例分布等
线程分析 查看线程状态、线程栈、是否存在死锁
GC 分析 图形化展示 GC 活动、频率与耗时
堆转储分析 导入 .hprof 文件进行对象实例、类、引用关系分析
CPU 分析 分析方法调用路径、耗时、热点代码(需手动启用 CPU profiler)
插件支持 可以通过插件安装更多功能(如 Visual GC)
  • 启动 jvisualvm

1
2
# 不要在生产环境使用,如果是分析服务端 .hprof 文件,可以导出到本地后在本地启动
$ jvisualvm
  • 在 jvisualvm 中导入 .hprof 文件

1
2
$ jvisualvm --openfile <heap dump file>
# 或者 先打开 jvisualvm 后,点击 File -> Open File -> 选择 .hprof 文件
  • ⚠️ 注意事项

注意点 说明
不适用于大规模生产环境监控 因为其分析过程可能会对 JVM 有轻微影响
.hprof 文件过大时加载缓慢 可配合 Eclipse MAT 使用
CPU/Memory Profiler 会增加系统负担 使用时谨慎,建议只在测试环境启用

Eclipse MAT: Java Heap Dump 分析工具

  • Eclipse MAT(Eclipse Memory Analyzer Tool)是一个用于分析 Java 堆快照的工具,它提供了许多功能来帮助开发人员理解 Java 应用程序中的内存问题。

MaxOS系统安装Eclipse MAT后,启动报错,报错信息为:The JVM shared library “/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/…/lib/server/libjvm.dylib” does not contain the JNI_CreateJavaVM symbol.

  • 解决方法:
    1.在应用列表,找到MemoryAnalyzer.app,然后右键单击后,选择显示包内容,进入Contents目录,找到Info.plist文件
    2.打开Info.plist文件后,可以看到注释<string>-vm</string>配置项,我们需要做的就是打开这个配置项,并且将其设置为我们系统的Java路径,最新版需要jdk17+
1
2
<string>-vm</string>
<string>/Users/hanqf/develop_soft/jdk17/bin/java</string>

Arthas : 推荐

  • Arthas 是阿里巴巴开源的一款 Java 诊断工具,专为线上诊断而设计。它可以帮助开发者在不重启、不修改代码的情况下,排查生产环境中 Java 应用的问题。

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

  • 这里要注意一点,就是Arthas从4.x版本开始,需要依赖JDK8+,如果是JDK6/7,可以下载最后一个3.x版本 3.7.2

  • 为了方便我们学习,Arthas还为我们提供了一个练习场,Arthas Playground

Arthas 核心特点

  • 无需重启、侵入性低
    Arthas 可以 attach 到正在运行的 JVM 上,不需要重启服务或修改源代码。

  • 命令式交互体验
    类似 Linux shell 的操作方式,支持 tab 补全、上下键历史命令等,非常直观。

  • 实时查看和监控
    可查看方法参数、返回值、调用栈、执行耗时、JVM 线程、内存等实时数据。

  • 多种连接方式
    支持命令行终端、本地 shell、Web 页面、telnet 等多种连接方式。

  • 支持多种 JVM 版本
    支持 Java 6+,包括 OpenJDK、Oracle JDK、Alibaba Dragonwell 等。

Arthas 常用命令

启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java -jar arthas-boot.jar
## 输出
java -jar arthas-boot.jar --target-ip 0.0.0.0
[INFO] JAVA_HOME: /Library/Java/JavaVirtualMachines/jdk1.8.0_271.jdk/Contents/Home/jre
[INFO] arthas-boot version: 4.0.5
[INFO] Process 43959 already using port 3658
[INFO] Process 43959 already using port 8563
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 43959 demo-arthas-spring-boot.jar
[2]: 50851 org.jetbrains.idea.maven.server.RemoteMavenServer36
[3]: 50813 org.sonarsource.sonarlint.core.backend.cli.SonarLintServerCli
[4]: 50655 com.intellij.idea.Main
# 启动后,会自动进入交互模式,此时输入要监控的进程序号
1 # 比如这里输入 1 回车
## 输出
[INFO] arthas home: /Users/hanqf/.arthas/lib/4.0.5/arthas
[INFO] The target process already listen port 3658, skip attach.
[INFO] arthas-client connect 0.0.0.0 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'

wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 4.0.5
main_class demo-arthas-spring-boot.jar
pid 43959
start_time 2025-05-14 14:57:18.031
currnt_time 2025-05-14 14:59:22.004

[arthas@43959]$ # 此时进入 arthas 命令行交互状态

# 退出 arthas
[arthas@43959]$ stop # 退出 arthas
  • arthas 命令有很多,当常用的也就几个,另外如果记不住某个命令怎么用了可以通过如下命令查询

1
<command> -h 或者 help <command>

dashboard : 实时查看 JVM 运行状态

1
2
3
4
5
6
7
8
9
10
11
12
# 查看 JVM 运行状态, 默认每隔 5 秒刷新 CPU、内存、线程、GC 等信息
# 默认会一直刷新状态,按 `q` 或 `Ctrl+C` 退出
dashboard

# 每隔 2 秒刷新一次
dashboard -i 2000

# 刷新 10 次后停止
dashboard -n 10

# 每隔 2 秒刷新一次,刷新 10 次后停止
dashboard -i 2000 -n 10

thread : 查看当前 JVM 线程信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 查看当前 JVM 线程信息,按 CPU 使用率倒序
thread
# 查看所有线程信息
thread --all

# 查看指定线程的堆栈信息
thread 18
## 输出
"http-nio-80-exec-1" Id=18 WAITING on java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@51866c16
at sun.misc.Unsafe.park(Native Method)
- waiting on java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@51866c16
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)

# 查看最忙的前5个线程的堆栈信息
thread -n 5
# 查看全部线程的堆栈信息
thread -n -1

# 指定 cpu 使用率统计的采样间隔,单位为毫秒,默认值为 200
thread -i 500

# 找出当前阻塞其他线程的线程
thread -b

# 查看指定状态的线程
thread --state WAITING

sc : 查看 JVM 已加载的类信息

  • Search-Class 的简写,这个命令能搜索出所有已经加载到 JVM 中的 Class 信息

1
2
3
4
5
6
7
8
# 通配符匹配,打印搜索到的类全名
sc *.demo.*

# 打印类的详细信息
sc -d com.example.demo.arthas.AdminFilterConfig

# 打印出类的 Field 信息
sc -d -f com.example.demo.arthas.AdminFilterConfig

sm : 查看已加载类的方法信息

  • Search-Method 的简写,这个命令能搜索出所有已经加载到 JVM 中的 Method 信息

1
2
3
4
5
6
7
8
# 打印指定类中的方法
sm java.lang.String
# 展示方法的详细信息
sm -d java.lang.String
# 展示指定方法的详细信息
sm -d java.lang.String substring
# 模糊匹配
sm -d java.lang.String eq*

jad : Java源码反编译工具

  • 验证生产环境下的代码是否与提交的代码一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 打印出类的源码,类名
jad com.example.demo.arthas.user.UserController
## 输出
jad com.example.demo.arthas.user.UserController

ClassLoader:
+-org.springframework.boot.loader.LaunchedURLClassLoader@5b2133b1
+-sun.misc.Launcher$AppClassLoader@5c647e05
+-sun.misc.Launcher$ExtClassLoader@452b3a41

Location:
file:/Users/hanqf/Desktop/arhtas_dir/demo-arthas-spring-boot.jar!/BOOT-INF/classes!/

/*
* Decompiled with CFR.
*
* Could not load the following classes:
* com.example.demo.arthas.user.User
* org.slf4j.Logger
* org.slf4j.LoggerFactory
* org.springframework.web.bind.annotation.GetMapping
* org.springframework.web.bind.annotation.PathVariable
* org.springframework.web.bind.annotation.RestController
*/
package com.example.demo.arthas.user;

import com.example.demo.arthas.user.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);

@GetMapping(value={"/user/{id}"})
public User findUserById(@PathVariable Integer id) {
/*15*/ logger.info("id: {}", (Object)id);
/*17*/ if (id != null && id < 1) {
throw new IllegalArgumentException("id < 1");
}
return new User(id.intValue(), "name" + id);
}
}

Affect(row-cnt:1) cost in 411 ms.
1
2
3
4
5
6
7
8
9
10
11
12
13
# 反编译后的源码会有类加载器的信息,如果只希望打印源码,可以加上 --source-only
jad --source-only com.example.demo.arthas.user.UserController
# 不显示行号
jad --source-only com.example.demo.arthas.user.UserController --lineNumber false

# 输出到文件
jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java

# 打印出类中某个方法的源码,类名 + 方法名
jad com.example.demo.arthas.user.UserController findUserById

# 反编译时指定dump class文件目录
jad --source-only com.example.demo.arthas.user.UserController --lineNumber false -d /tmp/jad
  • 当有多个 ClassLoader 都加载了这个类时,jad 命令会输出对应 ClassLoader 实例的 hashcode,然后你只需要重新执行 jad 命令,并使用参数 -c 就可以反编译指定 ClassLoader 加载的那个类了;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ jad org.apache.log4j.Logger

Found more than one class for: org.apache.log4j.Logger, Please use jad -c hashcode org.apache.log4j.Logger
HASHCODE CLASSLOADER
69dcaba4 +-monitor's ModuleClassLoader
6e51ad67 +-java.net.URLClassLoader@6e51ad67
+-sun.misc.Launcher$AppClassLoader@6951a712
+-sun.misc.Launcher$ExtClassLoader@6fafc4c2
2bdd9114 +-pandora-qos-service's ModuleClassLoader
4c0df5f8 +-pandora-framework's ModuleClassLoader

Affect(row-cnt:0) cost in 38 ms.

# 指定类加载器
$ jad org.apache.log4j.Logger -c 69dcaba4

mc : Memory Compiler/内存编译器,编译.java文件生成.class

1
2
3
4
5
6
7
8
# 编译,这里一定要指定类加载器,否则会报错,因为你编译的类中可能import其它类,不指定就会提示找不到对应的类。
# 类加载器可以通过 jad 命令获取
mc --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader /tmp/UserController.java
# 指定类加载器的编号
mc -c 5b2133b1 /tmp/UserController.java

# 将 UserController.class 文件输出到指定目录, -d 指定输出目录
mc --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader /tmp/UserController.java -d /tmp
  • mc 命令有可能失败。如果编译失败可以在本地编译好.class文件,再上传到服务器。

retransform : 重新加载类(热修改代码)

1
2
3
4
5
6
7
8
9
10
11
12
retransform /tmp/com/example/demo/arthas/user/UserController.class

# 查看 通过 retransform 重新加载的 class
retransform -l

# 删除通过 retransform 重新加载的 class,通过 id 删除
retransform -d 1
# 删除所有
retransform --deleteAll

# 删除后要重新触发 retransform,否则内存中依旧使用的是删除前的 class
retransform --classPattern com.example.demo.arthas.user.UserController

sysprop : 查看当前 JVM 的系统属性

1
2
3
4
5
6
7
8
9
10
11
# 打印系统属性
sysprop

# 指定单个 key,key支持自动补全
sysprop java.version

# 通过grep过滤
sysprop | grep user

# 设置 或 修改 系统属性
sysprop testKey testValue

sysenv : 查看当前 JVM 的环境变量

1
2
3
4
5
6
# 列出所有环境变量
sysenv
# 指定单个 key,key支持自动补全
sysenv PATH
# 通过grep过滤
sysenv | grep PATH

jvm : 查看当前 JVM 的信息

1
jvm
  • THREAD 相关

1
2
3
4
5
COUNT: JVM 当前活跃的线程数
DAEMON-COUNT: JVM 当前活跃的守护线程数
PEAK-COUNT: 从 JVM 启动开始曾经活着的最大线程数
STARTED-COUNT: 从 JVM 启动开始总共启动过的线程次数
DEADLOCK-COUNT: JVM 当前死锁的线程数
  • FILE-DESCRIPTOR 文件描述符相关

1
2
MAX-FILE-DESCRIPTOR-COUNT:JVM 进程最大可以打开的文件描述符数
OPEN-FILE-DESCRIPTOR-COUNT:JVM 当前打开的文件描述符数

watch : 函数执行数据观测

  • 让你能方便的观察到指定函数的调用情况。能观察到的范围为:返回值、抛出异常、入参,通过编写 OGNL 表达式进行对应变量的查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 观察指定类中某个方法的调用情况,className +  methodName + 返回值表达式
# 开启后,当方法被调用时,会按照指定的 `返回值表达式` 输出结果,返回值表达式 可以不指定,默认为 {params, target, returnObj}
watch com.example.demo.arthas.user.UserController findUserById '{params, throwExp}'

# 观察全部方法,通配符匹配
watch com.example.demo.arthas.user.UserController '*' '{params, throwExp}'

# 默认输出结果没有展开,可以加上 -x 指定输出结果的属性遍历深度,默认为 1,最大值是 4
watch com.example.demo.arthas.user.UserController '*' '{params, throwExp}' -x 2

# 仅当抛出异常时才打印,-e 在函数异常之后观察
watch com.example.demo.arthas.user.UserController '*' '{params, throwExp}' -x 2 -e

# 参数条件过滤,params[0] > 100 表示方法入参中第一个参数的值大于 100 时才触发打印
watch com.example.demo.arthas.user.UserController * returnObj 'params[0] > 100'

# 按请求耗时进行过滤,#cost>200 表示耗时大于200ms
watch com.example.demo.arthas.user.UserController * '{params, returnObj}' '#cost>200'

# 退出watch
q
  • watch 命令表达式支持变量说明表

名称 类型/说明 作用/含义
loader ClassLoader 方法所在类的类加载器对象
clazz Class<?> 方法所属的类(Class 对象)
method java.lang.reflect.Method 被调用的方法的反射对象
target Object 方法的调用目标对象(即 this 对象),静态方法中为 null
params Object[] 方法的入参数组
returnObj Object 方法的返回值
throwExp Throwable 方法抛出的异常
isBefore boolean 当前执行位置是否是方法调用之前BEFORE 阶段)
isThrow boolean 当前方法是否以异常结束(THROWS 阶段)
isReturn boolean 当前方法是否正常返回(RETURN 阶段)

jobs : 查看运行中的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 如下命令会转到后台运行 ,与linux命令类似,命令最后加上 `&` 即可
watch com.example.demo.arthas.user.UserController * '{params, throwExp}' 'throwExp != null' >> a.log &
# 查看后台任务
jobs
## 输出
[1]*
Running watch com.example.demo.arthas.user.UserController * '{params, throwExp}' 'throwExp != null' >> a.log &
execution count : 0
start time : Thu May 15 15:10:04 CST 2025
timeout date : Fri May 16 15:10:04 CST 2025
session : ded56dfd-5683-4427-ac08-792f8fb75e5e (current)
# 结束任务
kill 1 # 1为后台任务id

# 将后台任务转到前台
fg 1 # 1为后台任务id

# 暂停任务,但此时会将arthas进程都挂起,所以并不好用
ctrl+z

# 将前台任务转到后台继续执行
bg 1 # 1为前台任务id

logger : 查看 logger 信息,更新 logger level

1
2
3
4
5
6
7
8
9
10
11
# 查看 logger 信息
logger
# 查看指定名字的logger信息
logger -n ROOT

# 查看没有 appender 的 logger 的信息,不加这个参数只会打印有 appender 的 logger 的信息
logger --include-no-appender

# 更新 logger level,-c classLoader 的 hashcode
logger --name ROOT --level debug -c 5b2133b1
logger --name ROOT --level error --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader

classloader : 查看 classloader 的继承树,urls,类加载信息

1
2
3
4
5
6
7
# 查看 classloader 的信息,加载class的数量,hashcode,classloader 的 parent
classloader -l
# 树形展示
classloader -t

# 查找资源,比如查找指定名称的文件
classloader -c 5b2133b1 -r application.properties