这个系列
「JVM 线上排查实战」共七篇,每篇都在真机上跑、JDK 8 和 JDK 17 两个版本都贴输出:
- (一)先把 JVM 看清楚:进程、参数、默认值
- (二)CPU 飙高:找到那个线程
- (三)线程卡住:死锁、BLOCKED、线程池打满
- (四)内存:OOM 了先干什么
- (五)GC 日志:从 JDK 8 升 17,老启动参数会让进程直接起不来—— 本篇
- (六)JFR 飞行记录器:录一段现场下来慢慢看
- (七)工具连不上进程时怎么办
实测环境同前几篇:CentOS 7.9 · JDK1.8.0_381(Oracle)装在/opt/jdk8· JDK17.0.8(Oracle)装在/usr/java。
先说结论
- JDK 8 的 GC 日志参数,在 17 上绝大多数直接让 JVM 起不来(
Unrecognized VM option,退出码 1)。只有-XX:+PrintGC、-XX:+PrintGCDetails、-Xloggc三个会打一行警告、自动换成-Xlog - 同一份启动脚本不能两个版本通用:反过来,
-Xlog:gc在 JDK 8 上也起不来 - 别用
-XX:+IgnoreUnrecognizedVMOptions糊过去:进程是起来了,但-XX:+UseConcMarkSweepGC被悄悄忽略,实际跑的是 G1 - JDK 8 用
-Xloggc:gc.log固定文件名,重启一次上次的 GC 日志就没了;17 会把旧文件留成gc.log.0 - 17 默认的日志行没有日期时间,只有进程启动后的秒数,要自己加
time装饰器 jstat -gcutil在 17 上多了CGC/CGCT两列,按列号取数的监控脚本会取错
1. 老参数在 JDK 17 上的下场 ✅
每个参数单独加在java <参数> -version上跑一遍,两个版本对比:
| 参数 | JDK 8u381 | JDK 17.0.8 |
|---|---|---|
-XX:+PrintGC | ✅ | ⚠️-XX:+PrintGC is deprecated. Will use -Xlog:gc instead. |
-XX:+PrintGCDetails | ✅ | ⚠️-XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead. |
-Xloggc:/tmp/x.log | ✅ | ⚠️-Xloggc is deprecated. Will use -Xlog:gc:/tmp/x.log instead. |
-XX:+PrintGCTimeStamps | ✅ | ❌ 起不来 |
-XX:+PrintGCDateStamps | ✅ | ❌ 起不来 |
-XX:+PrintGCApplicationStoppedTime | ✅ | ❌ 起不来 |
-XX:+PrintGCApplicationConcurrentTime | ✅ | ❌ 起不来 |
-XX:+PrintHeapAtGC | ✅ | ❌ 起不来 |
-XX:+PrintTenuringDistribution | ✅ | ❌ 起不来 |
-XX:+PrintGCCause | ✅ | ❌ 起不来 |
-XX:+PrintAdaptiveSizePolicy | ✅ | ❌ 起不来(还会提示Did you mean '(+/-)UseAdaptiveSizePolicy'?) |
-XX:+PrintReferenceGC | ✅ | ❌ 起不来 |
-XX:+UseGCLogFileRotation | ✅ | ❌ 起不来 |
-XX:NumberOfGCLogFiles=5 | ✅ | ❌ 起不来 |
-XX:GCLogFileSize=10M | ✅ | ❌ 起不来 |
-XX:+UseConcMarkSweepGC | ✅ | ❌ 起不来 |
-XX:+UseParNewGC | ⚠️Using the ParNew young collector with the Serial old collector is deprecated… | ❌ 起不来 |
-XX:CMSInitiatingOccupancyFraction=75 | ✅ | ❌ 起不来 |
-XX:+UseCMSInitiatingOccupancyOnly | ✅ | ❌ 起不来 |
-XX:+CMSClassUnloadingEnabled | ✅ | ❌ 起不来 |
-XX:PermSize=128m | ⚠️ignoring option PermSize=128m; support was removed in 8.0 | ❌ 起不来 |
-XX:MaxPermSize=256m | ⚠️ignoring option MaxPermSize=256m; support was removed in 8.0 | ❌ 起不来 |
-XX:+AggressiveOpts | ✅ | ❌ 起不来 |
-XX:+UseBiasedLocking | ✅ | ⚠️Option UseBiasedLocking was deprecated in version 15.0… |
-XX:+DisableExplicitGC | ✅ | ✅ |
-XX:+HeapDumpOnOutOfMemoryError | ✅ | ✅ |
反方向-Xlog:gc | ❌Unrecognized option: -Xlog:gc | ✅ |
-XX:+UseZGC | ❌ 起不来 | ✅ |
-XX:+UseShenandoahGC | ❌ 起不来 | ❌Option -XX:+UseShenandoahGC not supported(本文用的 Oracle 发行版) |
「起不来」在 17 上的样子都是这三行,退出码 1:
Unrecognized VM option 'PrintGCDateStamps' Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit.最常见的组合-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log,前后两个只是警告,中间那个让进程起不来。只看到「PrintGCDetails 在 17 上会自动转换」就以为老参数都兼容,上线就挂在PrintGCDateStamps上。
PermSize/MaxPermSize在 JDK 8 上就已经不起作用了(8 用 Metaspace 取代了永久代,只打一行ignoring option),很多启动脚本里一直留着没人删,到 17 上变成了起不来的原因。
升级前先扫一遍启动参数
把线上的启动参数整串丢给下面这个脚本,用 17 逐个试启动:
#!/bin/bash# 用法: check_opts.sh <java 路径> <启动参数...> 逐个参数试启动,报出会让 JVM 起不来的那几个JAVA=$1;shiftforoptin"$@";doout=$("$JAVA" "$opt"-version2>&1)if[$?-ne0];thenecho"❌$opt->$(echo"$out"|head-1)"elifecho"$out"|grep-qiE'warning|deprecated|ignoring';thenecho"⚠️$opt->$(echo"$out"|grep-iE'warning|deprecated|ignoring'|head-1)"elseecho"✅$opt";fidone拿一份典型的 JDK 8 启动参数试:
$ bash check_opts.sh /usr/java/bin/java -Xms2g -Xmx2g -XX:MetaspaceSize=256m -XX:MaxPermSize=256m -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/tmp/gc.log -XX:+UseGCLogFileRotation -XX:+HeapDumpOnOutOfMemoryError ✅ -Xms2g ✅ -Xmx2g ✅ -XX:MetaspaceSize=256m ❌ -XX:MaxPermSize=256m -> Unrecognized VM option 'MaxPermSize=256m' ❌ -XX:+UseConcMarkSweepGC -> Unrecognized VM option 'UseConcMarkSweepGC' ❌ -XX:CMSInitiatingOccupancyFraction=75 -> Unrecognized VM option 'CMSInitiatingOccupancyFraction=75' ⚠️ -XX:+PrintGCDetails -> [0.000s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead. ❌ -XX:+PrintGCDateStamps -> Unrecognized VM option 'PrintGCDateStamps' ⚠️ -Xloggc:/tmp/gc.log -> [0.000s][warning][gc] -Xloggc is deprecated. Will use -Xlog:gc:/tmp/gc.log instead. ❌ -XX:+UseGCLogFileRotation -> Unrecognized VM option 'UseGCLogFileRotation' ✅ -XX:+HeapDumpOnOutOfMemoryError11 个参数,5 个会让 17 起不来。逐个试的好处是一次把所有问题列全。整串一起启动,JVM 只报第一个不认识的 —— 实测java -XX:MaxPermSize=256m -XX:+UseConcMarkSweepGC -XX:+PrintGCDateStamps -version只报了Unrecognized VM option 'MaxPermSize=256m',后两个一声不吭,改一个再撞下一个。
2. 坑:别用 IgnoreUnrecognizedVMOptions 糊过去 ✅
-XX:+IgnoreUnrecognizedVMOptions能让 17 跳过所有不认识的参数:
$ java -XX:+IgnoreUnrecognizedVMOptions -XX:+PrintGCDateStamps -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -version [0.000s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead. [0.003s][info ][gc] Using G1 ... java version "17.0.8" 2023-07-18 LTS进程起来了。但注意第二行Using G1—— 启动参数里写的是 CMS。再用PrintFlagsFinal确认:
$ java -XX:+IgnoreUnrecognizedVMOptions -XX:+UseConcMarkSweepGC -XX:+PrintFlagsFinal -version | grep -E ' Use(G1|ConcMarkSweep|Parallel|Serial)GC ' bool UseG1GC = true {product} {ergonomic} bool UseParallelGC = false {product} {default} bool UseSerialGC = false {product} {default}UseConcMarkSweepGC这个开关在 17 里已经不存在了,实际用的是默认的 G1。加这个参数等于把「GC 换了」这件事藏了起来,PrintGCDateStamps之类也是默默不生效。该删的删、该换的换,别靠它启动。
3. JDK 8 → 17 GC 日志参数对照 ✅
17 的日志统一走-Xlog,格式是-Xlog:<标签>=<级别>:<输出>:<装饰器>:<输出选项>。每一行都在 17.0.8 上实跑过,右边是真实日志行:
| JDK 8 | JDK 17 | 17 实跑的日志行 |
|---|---|---|
-XX:+PrintGC | -Xlog:gc | [0.037s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 13M->1M(62M) 0.622ms |
-XX:+PrintGCDetails | -Xlog:gc* | [0.036s][info][gc,start ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)等多行 |
-Xloggc:gc.log | -Xlog:gc:file=gc.log | —— |
-XX:+PrintGCDateStamps | 装饰器加time | [2026-09-20T03:17:23.639+0800][0.037s][info][gc] GC(0) Pause Young … |
-XX:+PrintGCTimeStamps | 装饰器uptime(默认就有) | [0.037s] |
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M | 输出选项filecount=5,filesize=20m | 见第 6 节 |
-XX:+PrintGCApplicationStoppedTime | -Xlog:safepoint | [0.027s][info][safepoint] Safepoint "G1CollectForAllocation", Time since last: 5654673 ns, Reaching safepoint: 18497 ns, At safepoint: 635803 ns, Total: 654300 ns |
-XX:+PrintTenuringDistribution | -Xlog:gc+age=trace | [0.030s][debug][gc,age] GC(1) Desired survivor size 524288 bytes, new threshold 15 (max threshold 15) |
-XX:+PrintHeapAtGC | -Xlog:gc+heap=debug | [0.023s][debug][gc,heap] GC(0) Heap before GC invocations=0 (full 0): |
对照 JDK 8 那几个老参数自己的输出(8u381 实跑):
# PrintGCApplicationStoppedTime 0.036: Total time for which application threads were stopped: 0.0032329 seconds, Stopping threads took: 0.0000567 seconds # PrintTenuringDistribution Desired survivor size 2621440 bytes, new threshold 7 (max 15)把常用的几项合成一条,17 上实跑通过:
-Xlog:gc*,safepoint:file=/data/logs/gc-%t-%p.log:time,uptime,level,tags:filecount=5,filesize=20m(实测时路径换成了logs/,生成的文件名是gc-2026-09-20_03-19-47-8351.log,里面 GC 和 safepoint 两类行都有。)
4. 同一个程序,两个版本的日志怎么读 ✅
demo:不停创建短命对象触发 Young GC,同时慢慢留下约 19MB 让它晋升到老年代,中途调用一次System.gc():
importjava.util.ArrayList;importjava.util.List;publicclassGcDemo{staticfinalList<byte[]>OLD=newArrayList<>();staticvolatileObjectsink;publicstaticvoidmain(String[]args)throwsException{longend=System.currentTimeMillis()+Long.getLong("ms",4000);booleancalled=false;while(System.currentTimeMillis()<end){for(inti=0;i<1000;i++)sink=newbyte[4*1024];// 短命对象:触发 Young GCif(OLD.size()<300)OLD.add(newbyte[64*1024]);// 慢慢留下 ~19MB:晋升到老年代if(!called&&System.currentTimeMillis()>end-2000){System.gc();called=true;}Thread.sleep(5);}System.out.println("done, old="+OLD.size());}}JDK 8u381,-Xmx64m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc8.log(默认 Parallel GC):
2026-09-20T03:17:11.546+0800: 0.047: [GC (Allocation Failure) [PSYoungGen: 15360K->512K(17920K)] 15360K->512K(58880K), 0.0010461 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 2026-09-20T03:17:13.533+0800: 2.035: [Full GC (System.gc()) [PSYoungGen: 32K->0K(19968K)] [ParOldGen: 19668K->19463K(40960K)] 19700K->19463K(60928K), [Metaspace: 2494K->2494K(1056768K)], 0.0055001 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]读法(第一行):
2026-09-20T03:17:11.546+0800是PrintGCDateStamps加的日期,0.047是启动后的秒数GC (Allocation Failure):Young GC,原因是新生代分配不下了PSYoungGen: 15360K->512K(17920K):新生代回收前 → 回收后(新生代总容量)15360K->512K(58880K):整个堆回收前 → 回收后(堆总容量)0.0010461 secs:这次停顿约 1 毫秒
第二行Full GC (System.gc()):原因是代码里调了System.gc(),ParOldGen: 19668K->19463K说明老年代那 19MB 都是活的,基本没收掉。
JDK 17.0.8,-Xmx64m -Xlog:gc*:file=gc17.log(默认 G1):
[0.036s][info][gc,start ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) [0.037s][info][gc ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 13M->1M(62M) 0.653ms [2.022s][info][gc,start ] GC(45) Pause Full (System.gc()) [2.024s][info][gc ] GC(45) Pause Full (System.gc()) 26M->19M(62M) 2.176msGC(0)、GC(45)是 GC 的编号,同一次 GC 的多行日志编号相同,grep 'GC(45)'就能把一次 GC 的所有细节拎出来Pause Young (Normal) (G1 Evacuation Pause):G1 的 Young GC;Pause Full (System.gc()):Full GC 及原因13M->1M(62M) 0.653ms:堆回收前 → 回收后(堆容量),停顿时长- 标签
[gc,start]是开始,[gc]是结束那一行,带结果
坑:-Xlog:gc*的量比想象中大
同一个 demo、同样跑 4 秒:
93 gc17s.log # -Xlog:gc 1407 gc17.log # -Xlog:gc*gc*是「所有以 gc 开头的标签」,包括每次 GC 各阶段的耗时、各区域的变化,多出十几倍。日常只看频率和停顿,-Xlog:gc就够;要细查时再开gc*,并且一定配上文件轮转。
5. 坑:17 默认的日志行没有日期 ✅
上面 17 的日志行开头是[0.037s],那是进程启动后的秒数。进程跑了三天,出问题时业务日志说「14:05 接口超时」,你得拿进程启动时间去换算,才知道对应哪条 GC。
JDK 8 靠PrintGCDateStamps解决这个问题,17 用装饰器:
-Xlog:gc:file=gc17t.log:time,uptime,level,tags[2026-09-20T03:17:23.639+0800][0.037s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 13M->1M(62M) 0.482ms [2026-09-20T03:17:23.684+0800][0.082s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 36M->1M(62M) 0.677ms注意:装饰器一写就是全量替换,默认的uptime,level,tags要自己写回去。第 6 节的实验只写了:time,日志行就只剩日期:[2026-09-20T03:17:36.749+0800] Using G1,连级别和标签都没了。
还有一处:如果还在用老写法-Xloggc:gc17old.log,17 转换成的是-Xlog:gc:gc17old.log,日志内容只有gc标签、没有日期:
[0.000s][warning][gc] -Xloggc is deprecated. Will use -Xlog:gc:gc17old.log instead.文件里是[0.006s][info][gc] Using G1这种格式。老参数能跑,不代表日志还是原来的样子。
6. 坑:JDK 8 重启一次,上次的 GC 日志就没了 ✅
JDK 8 用固定文件名-Xloggc:gc8r.log(不开轮转),同一个命令跑两次:
after run1: lines=72 first_gc=2026-09-20T03:18:19.552+0800: CommandLine_count=1 after run2: lines=71 first_gc=2026-09-20T03:18:23.105+0800: CommandLine_count=1第二次跑完,文件里第一条 GC 的时间变成了第二次运行的时间,行数没有翻倍,文件头(CommandLine flags:那一行)也只有一份 ——第一次的日志被整个覆盖了。线上最需要看 GC 日志的时候,往往就是进程刚崩溃、被自动拉起之后,而这时崩溃前的那份已经没了。
JDK 8 的解法是文件名里带%t(启动时间)或%p(进程号),每次启动一个新文件:
-Xloggc:gc8-%t-%p.log → gc8-2026-09-20_03-17-41-pid7877.logJDK 17 同样跑两次file=gc17r.log:
-rw-r--r--. 1 root root 9949 03:17:41 gc17r.log -rw-r--r--. 1 root root 9899 03:17:38 gc17r.log.0 gc17r.log first=[2026-09-20T03:17:40.283+0800] Using G1 gc17r.log.0 first=[2026-09-20T03:17:36.749+0800] Using G117 把上一次的文件改名成了gc17r.log.0,没有覆盖。%t、%p在 17 里也能用:file=gc17-%t-%p.log生成gc17-2026-09-20_03-17-42-7893.log(注意 17 的%p是纯数字,8 是pid7877)。
轮转之后,文件名也不一样
JDK 8,-Xloggc:rot8.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=3 -XX:GCLogFileSize=8k:
-rw-r--r--. 1 root root 700 03:17:50 rot8.log.0.current -rw-r--r--. 1 root root 8406 03:17:49 rot8.log.1 -rw-r--r--. 1 root root 8600 03:17:50 rot8.log.2开了轮转后根本没有rot8.log这个文件,正在写的是带.current后缀的那个。tail -f rot8.log会报文件不存在。
JDK 17,-Xlog:gc*:file=rot.log:time,uptime:filecount=3,filesize=2k:
-rw-r--r--. 1 root root 794 03:17:46 rot.log -rw-r--r--. 1 root root 2114 03:17:46 rot.log.0 -rw-r--r--. 1 root root 2116 03:17:46 rot.log.1 -rw-r--r--. 1 root root 2074 03:17:46 rot.log.217 正在写的就是rot.log,旧的编号往后排。日志采集(filebeat 之类)的路径配置,升级时要跟着改。
7.System.gc()和 DisableExplicitGC ✅
代码里(或第三方库里)调了System.gc(),两个版本的日志里都会出现原因System.gc()的 Full GC(见第 4 节)。加-XX:+DisableExplicitGC后,同一个 demo 数日志里含System.gc的行:
| 不加 | 加-XX:+DisableExplicitGC | |
|---|---|---|
JDK 8u381(-XX:+PrintGC) | 2 行(一行GC (System.gc())、一行Full GC (System.gc())) | 0 |
JDK 17.0.8(-Xlog:gc) | 1 行(Pause Full (System.gc())) | 0 |
JDK 8 不带Details时,一次System.gc()在日志里是两行:
0.030: [GC (System.gc()) 4660K->392K(58880K), 0.0008697 secs] 0.031: [Full GC (System.gc()) 392K->322K(58880K), 0.0016187 secs]这个参数两个版本都认(第 1 节表里都是 ✅)。
8.jstat -gcutil怎么读 ✅
不开 GC 日志的进程,jstat是最快的现场工具。每秒一次、采 6 次:
JDK 8u381:
$ jstat -gcutil <pid> 1000 6 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 34.38 0.00 0.00 29.26 51.29 51.74 42 0.024 0 0.000 0.024 6.25 0.00 0.00 48.66 51.29 51.74 78 0.044 0 0.000 0.044 0.00 6.25 0.00 48.66 51.29 51.74 113 0.059 0 0.000 0.059 6.25 0.00 0.00 48.66 51.29 51.74 148 0.072 0 0.000 0.072 6.25 0.00 0.00 48.66 51.29 51.74 184 0.090 0 0.000 0.090 0.00 6.25 0.00 48.66 51.29 51.74 219 0.105 0 0.000 0.105JDK 17.0.8:
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT 0.00 89.59 80.00 44.19 26.87 2.59 23 0.014 0 0.000 0 0.000 0.014 0.00 70.83 81.25 69.84 26.87 2.59 46 0.026 0 0.000 0 0.000 0.026 0.00 0.39 90.91 75.16 26.87 2.59 69 0.033 0 0.000 0 0.000 0.033 0.00 0.39 21.21 75.16 26.87 2.59 93 0.040 0 0.000 0 0.000 0.040 0.00 0.39 42.42 75.16 26.87 2.59 116 0.047 0 0.000 0 0.000 0.047 0.00 0.39 54.55 75.16 26.87 2.59 139 0.062 0 0.000 0 0.000 0.062各列:
| 列 | 含义 |
|---|---|
S0S1EOMCCS | 两个 Survivor、Eden、老年代、Metaspace、压缩类空间的使用百分比(此刻的快照) |
YGC/YGCT | Young GC累计次数 / 累计耗时(秒) |
FGC/FGCT | Full GC 累计次数 / 累计耗时 |
CGC/CGCT | 17 才有:并发 GC(G1 的并发标记等)累计次数 / 耗时 |
GCT | 全部 GC 累计耗时 |
这些计数都是累计值,单看一行没有意义,要看相邻两行的差。算一下本文 demo:
- JDK 8:6 秒内
YGC从 42 到 219,约每秒 35 次Young GC;YGCT增加 0.081 秒,平均每次约 0.46 毫秒 - JDK 17:
YGC从 23 到 139,约每秒 23 次;YGCT增加 0.048 秒,平均每次约 0.41 毫秒 - 两个版本
FGC都一直是 0(这次 demo 跑 15 秒,System.gc()在第 13 秒,采样时还没到);老年代O在 8 上停在 48.66%、在 17 上停在 75.16%,之后不再涨(demo 只留 300 个 64KB 数组)
判断「GC 有没有问题」看的就是这几个差值:Young GC 每秒几次、每次多久;FGC有没有在涨、涨得多快;O是不是每次 GC 后都比上次高(那是老年代在泄漏)。
坑:17 多了两列,按列号取数的脚本会错
很多监控脚本是这么取 GC 总耗时的:
jstat-gcutil<pid>|awk'NR==2{print $11}'JDK 8 的第 11 列是GCT;JDK 17 的第 11 列是CGC,GCT挪到了第 13 列。脚本不报错,只是从「GC 总耗时」悄悄变成了「并发 GC 次数」。按表头的列名取值,别按列号。
9. 本篇速查
| 想做的事 | JDK 8 | JDK 17 |
|---|---|---|
| 升级前查启动参数 | —— | 用第 1 节的check_opts.sh逐个试 |
| 基本 GC 日志 | -XX:+PrintGCDetails -Xloggc:gc.log | -Xlog:gc:file=gc.log(细节用gc*) |
| 带日期 | -XX:+PrintGCDateStamps | 装饰器:time,uptime,level,tags |
| 轮转 | -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M(当前文件带.current) | :filecount=5,filesize=20m(当前文件就是原名) |
| 重启不丢日志 | 文件名带%t/%p | 默认会把旧文件留成.0;也可带%t/%p |
| 停顿时间 | -XX:+PrintGCApplicationStoppedTime | -Xlog:safepoint |
| 年龄分布 | -XX:+PrintTenuringDistribution | -Xlog:gc+age=trace |
| GC 前后堆详情 | -XX:+PrintHeapAtGC | -Xlog:gc+heap=debug |
| 现场看频率 | jstat -gcutil <pid> 1000 | 同左,多CGC/CGCT两列 |
前五篇回顾
前五篇下来,每篇都是同一个做法:在一台 CentOS 7 上,JDK 8 和 17 同时跑,把真实输出贴出来,再挑出两个版本不一样、或者和「常识」不一样的地方。
- (一)先把 JVM 看清楚:
jps看不到进程、参数到底生效没有、两个版本的默认值差在哪 - (二)CPU 飙高:
top -Hp找线程,线程名截断、ps的 %CPU 是平均值、17 的cpu=字段 - (三)线程卡住:ReentrantLock 死锁里没有 BLOCKED、不加
-l看不到锁在谁手里、线程池自己等自己不报死锁 - (四)内存:OOM 后进程还活着、G1 下连出错行都没打印、
jcmd默认就触发 Full GC、直接内存 OOM 没有转储 - (五)GC 日志:老参数在 17 上起不来、日志被覆盖、
jstat列错位 —— 本篇
后面还有两篇:(六)JFR 飞行记录器,(七)工具连不上进程时怎么办。