这篇文章中介绍下如何应用 JDK 自带工具来剖析和定位 Java 程序的问题。
应用 JDK 自带工具查看 JVM 状况
JDK 自带了很多命令行甚至是图形界面工具,帮忙咱们查看 JVM 的一些信息。比方,在我的机器上运行 ls 命令,能够看到 JDK 8 提供了十分多的工具或程序:
接下来,我会与你介绍些罕用的监控工具。你也能够先通过上面这张图理解下各种工具的根本作用:
为了测试这些工具,咱们先来写一段代码:启动 10 个死循环的线程,每个线程调配一个 10MB 左右的字符串,而后休眠 10 秒。能够设想到,这个程序会对 GC 造成压力:
// 启动 10 个线程
IntStream.rangeClosed(1, 10).mapToObj(i -> new Thread(() -> {while (true) {
// 每一个线程都是一个死循环,休眠 10 秒,打印 10M 数据
String payload = IntStream.rangeClosed(1, 10000000)
.mapToObj(__ -> "a")
.collect(Collectors.joining("")) + UUID.randomUUID().toString();
try {TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {e.printStackTrace();
}
System.out.println(payload.length());
}
})).forEach(Thread::start);
TimeUnit.HOURS.sleep(1);
批改 pom.xml,配置 spring-boot-maven-plugin 插件打包的 Java 程序的 main 办法类:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication
</mainClass>
</configuration>
</plugin>
而后应用 java -jar 启动过程,设置 JVM 参数,让堆最小最大都是 1GB:
java -jar common-mistakes-0.0.1-SNAPSHOT.jar -Xms1g -Xmx1g
实现这些筹备工作后,咱们就能够应用 JDK 提供的工具,来察看剖析这个测试程序了。
jps
首先,应用 jps 失去 Java 过程列表,这会比应用 ps 来的不便:
➜ ~ jps
12707
22261 Launcher
23864 common-mistakes-0.0.1-SNAPSHOT.jar
15608 RemoteMavenServer36
23243 Main
23868 Jps
22893 KotlinCompileDaemon
jinfo
而后,能够应用 jinfo 打印 JVM 的各种参数:
➜ ~ jinfo 23864
Java System Properties:
#Wed Jan 29 12:49:47 CST 2020
...
user.name=zhuye
path.separator=\:
os.version=10.15.2
java.runtime.name=Java(TM) SE Runtime Environment
file.encoding=UTF-8
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
...
VM Flags:
-XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=268435456 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=2576351232 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5835340 -XX:NonProfiledCodeHeapSize=122911450 -XX:ProfiledCodeHeapSize=122911450 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
VM Arguments:
java_command: common-mistakes-0.0.1-SNAPSHOT.jar -Xms1g -Xmx1g
java_class_path (initial): common-mistakes-0.0.1-SNAPSHOT.jar
Launcher Type: SUN_STANDARD
查看第 15 行和 19 行能够发现,咱们设置 JVM 参数的形式不对,-Xms1g 和 -Xmx1g 这两个参数被当成了 Java 程序的启动参数,整个 JVM 目前最大内存是 4GB 左右,而不是 1GB。最近整顿了一份最新的面试材料,外面收录了 2021 年各个大厂的面试题,打算跳槽的小伙伴不要错过,点击支付吧!
因而,当咱们狐疑 JVM 的配置很不失常的时候,要第一工夫应用工具来确认参数。除了应用工具确认 JVM 参数外,你也能够打印 VM 参数和程序参数:
System.out.println("VM options");
System.out.println(ManagementFactory.getRuntimeMXBean().getInputArguments().stream().collect(Collectors.joining(System.lineSeparator())));
System.out.println("Program arguments");
System.out.println(Arrays.stream(args).collect(Collectors.joining(System.lineSeparator())));
把 JVM 参数放到 -jar 之前,重新启动程序,能够看到如下输入,从输入也能够确认这次 JVM 参数的配置正确了:
➜ target git:(master) ✗ java -Xms1g -Xmx1g -jar common-mistakes-0.0.1-SNAPSHOT.jar test
VM options
-Xms1g
-Xmx1g
Program arguments
test
jvisualvm
而后,启动另一个重量级工具 jvisualvm 察看一下程序,能够在概述面板再次确认 JVM 参数设置胜利了:
持续察看监督面板能够看到,JVM 的 GC 流动根本是 10 秒产生一次,堆内存在 250MB 到 900MB 之间稳定,流动线程数是 22。咱们能够在监督面板看到 JVM 的根本状况,也能够间接在这里进行手动 GC 和堆 Dump 操作:
jconsole 如果心愿看到各个内存区的 GC 曲线图,能够应用 jconsole 察看。jconsole 也是一个综合性图形界面监控工具,比 jvisualvm 更不便的一点是,能够用曲线的模式监控各种数据,包含 MBean 中的属性值:
jstat
同样,如果没有条件应用图形界面(毕竟在 Linux 服务器上,咱们次要应用命令行工具),又心愿看到 GC 趋势的话,咱们能够应用 jstat 工具。
jstat 工具容许以固定的监控频次输入 JVM 的各种监控指标,比方应用 -gcutil 输入 GC 和内存占用汇总信息,每隔 5 秒输入一次,输入 100 次,能够看到 Young GC 比拟频繁,而 Full GC 根本 10 秒一次:
➜ ~ jstat -gcutil 23940 5000 100
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 100.00 0.36 87.63 94.30 81.06 539 14.021 33 3.972 837 0.976 18.968
0.00 100.00 0.60 69.51 94.30 81.06 540 14.029 33 3.972 839 0.978 18.979
0.00 0.00 0.50 99.81 94.27 81.03 548 14.143 34 4.002 840 0.981 19.126
0.00 100.00 0.59 70.47 94.27 81.03 549 14.177 34 4.002 844 0.985 19.164
0.00 100.00 0.57 99.85 94.32 81.09 550 14.204 34 4.002 845 0.990 19.196
0.00 100.00 0.65 77.69 94.32 81.09 559 14.469 36 4.198 847 0.993 19.659
0.00 100.00 0.65 77.69 94.32 81.09 559 14.469 36 4.198 847 0.993 19.659
0.00 100.00 0.70 35.54 94.32 81.09 567 14.763 37 4.378 853 1.001 20.142
0.00 100.00 0.70 41.22 94.32 81.09 567 14.763 37 4.378 853 1.001 20.142
0.00 100.00 1.89 96.76 94.32 81.09 574 14.943 38 4.487 859 1.007 20.438
0.00 100.00 1.39 39.20 94.32 81.09 575 14.946 38 4.487 861 1.010 20.442
其中,S0 示意 Survivor0 区占用百分比,S1 示意 Survivor1 区占用百分比,E 示意 Eden 区占用百分比,O 示意老年代占用百分比,M 示意元数据区占用百分比,YGC 示意年老代回收次数,YGCT 示意年老代回收耗时,FGC 示意老年代回收次数,FGCT 示意老年代回收耗时。
jstat 命令的参数泛滥,蕴含 -class、-compiler、-gc 等。Java 8、Linux/Unix 平台 jstat 工具的残缺介绍,你能够查看这里。jstat 定时输入的个性,能够不便咱们继续察看程序的各项指标。
持续来到线程面板能够看到,大量以 Thread 结尾的线程根本都是有节奏的 10 秒运行一下,其余工夫都在休眠,和咱们的代码逻辑匹配:
点击面板的线程 Dump 按钮,能够查看线程刹时的线程栈:
jstack
通过命令行工具 jstack,也能够实现抓取线程栈的操作:
➜ ~ jstack 23940
2020-01-29 13:08:15
Full thread dump Java HotSpot(TM) 64-Bit Server VM (11.0.3+12-LTS mixed mode):
...
"main" #1 prio=5 os_prio=31 cpu=440.66ms elapsed=574.86s tid=0x00007ffdd9800000 nid=0x2803 waiting on condition [0x0000700003849000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(java.base@11.0.3/Native Method)
at java.lang.Thread.sleep(java.base@11.0.3/Thread.java:339)
at java.util.concurrent.TimeUnit.sleep(java.base@11.0.3/TimeUnit.java:446)
at org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication.main(CommonMistakesApplication.java:41)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(java.base@11.0.3/Native Method)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(java.base@11.0.3/NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(java.base@11.0.3/DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(java.base@11.0.3/Method.java:566)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:87)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:51)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:52)
"Thread-1" #13 prio=5 os_prio=31 cpu=17851.77ms elapsed=574.41s tid=0x00007ffdda029000 nid=0x9803 waiting on condition [0x000070000539d000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(java.base@11.0.3/Native Method)
at java.lang.Thread.sleep(java.base@11.0.3/Thread.java:339)
at java.util.concurrent.TimeUnit.sleep(java.base@11.0.3/TimeUnit.java:446)
at org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication.lambda$null$1(CommonMistakesApplication.java:33)
at org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication$$Lambda$41/0x00000008000a8c40.run(Unknown Source)
at java.lang.Thread.run(java.base@11.0.3/Thread.java:834)
...
抓取后能够应用相似 fastthread(https://fastthread.io/)这样的在线剖析工具来剖析线程栈。
jcmd
最初,咱们来看一下 Java HotSpot 虚拟机的 NMT 性能。
通过 NMT,咱们能够察看细粒度内存应用状况,设置 -XX:NativeMemoryTracking=summary/detail 能够开启 NMT 性能,开启后能够应用 jcmd 工具查看 NMT 数据。
咱们重新启动一次程序,这次加上 JVM 参数以 detail 形式开启 NMT:
-Xms1g -Xmx1g -XX:ThreadStackSize=256k -XX:NativeMemoryTracking=detail
在这里,咱们还减少了 -XX:ThreadStackSize 参数,并将其值设置为 256k,也就是冀望把线程栈设置为 256KB。咱们通过 NMT 察看一下设置是否胜利。
启动程序后执行如下 jcmd 命令,以概要模式输入 NMT 后果。能够看到,以后有 32 个线程,线程栈总共保留了差不多 4GB 左右的内存。咱们明明配置线程栈最大 256KB 啊,为什么会呈现 4GB 这么夸大的数字呢,到底哪里出了问题呢?
➜ ~ jcmd 24404 VM.native_memory summary
24404:
Native Memory Tracking:
Total: reserved=6635310KB, committed=5337110KB
- Java Heap (reserved=1048576KB, committed=1048576KB)
(mmap: reserved=1048576KB, committed=1048576KB)
- Class (reserved=1066233KB, committed=15097KB)
(classes #902)
(malloc=9465KB #908)
(mmap: reserved=1056768KB, committed=5632KB)
- Thread (reserved=4209797KB, committed=4209797KB)
(thread #32)
(stack: reserved=4209664KB, committed=4209664KB)
(malloc=96KB #165)
(arena=37KB #59)
- Code (reserved=249823KB, committed=2759KB)
(malloc=223KB #730)
(mmap: reserved=249600KB, committed=2536KB)
- GC (reserved=48700KB, committed=48700KB)
(malloc=10384KB #135)
(mmap: reserved=38316KB, committed=38316KB)
- Compiler (reserved=186KB, committed=186KB)
(malloc=56KB #105)
(arena=131KB #7)
- Internal (reserved=9693KB, committed=9693KB)
(malloc=9661KB #2585)
(mmap: reserved=32KB, committed=32KB)
- Symbol (reserved=2021KB, committed=2021KB)
(malloc=1182KB #334)
(arena=839KB #1)
- Native Memory Tracking (reserved=85KB, committed=85KB)
(malloc=5KB #53)
(tracking overhead=80KB)
- Arena Chunk (reserved=196KB, committed=196KB)
(malloc=196KB)
从新以 VM.native_memory detail 参数运行 jcmd:
jcmd 24404 VM.native_memory detail
能够看到,有 16 个可疑线程,每一个线程保留了 262144KB 内存,也就是 256MB(通过下图红框能够看到,应用关键字 262144KB for Thread Stack from 搜寻到了 16 个后果):
其实,ThreadStackSize 参数的单位是 KB,所以咱们如果要设置线程栈 256KB,那么应该设置 256 而不是 256k。从新设置正确的参数后,应用 jcmd 再次验证下:
除了用于查看 NMT 外,jcmd 还有许多性能。咱们能够通过 help,看到它的所有性能:
jcmd 24781 help
除了 jps、jinfo、jcmd、jstack、jstat、jconsole、jvisualvm 外,JDK 中还有一些工具,你能够通过官网文档查看残缺介绍。
官网文档:https://docs.oracle.com/javas…