这个系列
「JVM 线上排查实战」共五篇,每篇都在真机上跑、JDK 8 和 JDK 17 两个版本都贴输出:
- (一)先把 JVM 看清楚:进程、参数、默认值
- (二)CPU 飙高:找到那个线程
- (三)线程卡住:死锁、BLOCKED、线程池打满
- (四)内存:OOM 了先干什么
- (五)GC 日志:从 JDK 8 升 17,老启动参数会让进程直接起不来 ------ 本篇(完结)
实测环境同前几篇:CentOS 7.9 · JDK 1.8.0_381(Oracle)装在 /opt/jdk8 · JDK 17.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:
vbnet
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 逐个试启动:
bash
#!/bin/bash
# 用法: check_opts.sh <java 路径> <启动参数...> 逐个参数试启动,报出会让 JVM 起不来的那几个
JAVA=$1; shift
for opt in "$@"; do
out=$("$JAVA" "$opt" -version 2>&1)
if [ $? -ne 0 ]; then echo "❌ $opt -> $(echo "$out" | head -1)"
elif echo "$out" | grep -qiE 'warning|deprecated|ignoring'; then echo "⚠️ $opt -> $(echo "$out" | grep -iE 'warning|deprecated|ignoring' | head -1)"
else echo "✅ $opt"; fi
done
拿一份典型的 JDK 8 启动参数试:
ruby
$ 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:+HeapDumpOnOutOfMemoryError
11 个参数,5 个会让 17 起不来。逐个试的好处是一次把所有问题列全。整串一起启动,JVM 只报第一个不认识的 ------ 实测 java -XX:MaxPermSize=256m -XX:+UseConcMarkSweepGC -XX:+PrintGCDateStamps -version 只报了 Unrecognized VM option 'MaxPermSize=256m',后两个一声不吭,改一个再撞下一个。
2. 坑:别用 IgnoreUnrecognizedVMOptions 糊过去 ✅
-XX:+IgnoreUnrecognizedVMOptions 能让 17 跳过所有不认识的参数:
ruby
$ 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 确认:
arduino
$ 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 实跑):
python
# 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 上实跑通过:
bash
-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():
java
import java.util.ArrayList;
import java.util.List;
public class GcDemo {
static final List<byte[]> OLD = new ArrayList<>();
static volatile Object sink;
public static void main(String[] args) throws Exception {
long end = System.currentTimeMillis() + Long.getLong("ms", 4000);
boolean called = false;
while (System.currentTimeMillis() < end) {
for (int i = 0; i < 1000; i++) sink = new byte[4 * 1024]; // 短命对象:触发 Young GC
if (OLD.size() < 300) OLD.add(new byte[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):
yaml
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):
scss
[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.176ms
GC(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 秒:
yaml
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 用装饰器:
bash
-Xlog:gc:file=gc17t.log:time,uptime,level,tags
scss
[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 标签、没有日期:
css
[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(不开轮转),同一个命令跑两次:
ini
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(进程号),每次启动一个新文件:
ruby
-Xloggc:gc8-%t-%p.log → gc8-2026-09-20_03-17-41-pid7877.log
JDK 17 同样跑两次 file=gc17r.log:
sql
-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 G1
17 把上一次的文件改名成了 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:
c
-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:
c
-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.2
17 正在写的就是 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() 在日志里是两行:
css
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:
ruby
$ 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.105
JDK 17.0.8:
objectivec
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
各列:
| 列 | 含义 |
|---|---|
S0 S1 E O M CCS |
两个 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 总耗时的:
bash
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列错位 ------ 本篇