JVM 线上排查实战(五):升 JDK 17 后进程起不来,十几个老 GC 参数挨个实测(完结)

这个系列

「JVM 线上排查实战」共五篇,每篇都在真机上跑、JDK 8 和 JDK 17 两个版本都贴输出:

  1. (一)先把 JVM 看清楚:进程、参数、默认值
  2. (二)CPU 飙高:找到那个线程
  3. (三)线程卡住:死锁、BLOCKED、线程池打满
  4. (四)内存:OOM 了先干什么
  5. (五)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 同时跑,把真实输出贴出来,再挑出两个版本不一样、或者和「常识」不一样的地方。

  1. (一)先把 JVM 看清楚:jps 看不到进程、参数到底生效没有、两个版本的默认值差在哪
  2. (二)CPU 飙高:top -Hp 找线程,线程名截断、ps 的 %CPU 是平均值、17 的 cpu= 字段
  3. (三)线程卡住:ReentrantLock 死锁里没有 BLOCKED、不加 -l 看不到锁在谁手里、线程池自己等自己不报死锁
  4. (四)内存:OOM 后进程还活着、G1 下连出错行都没打印、jcmd 默认就触发 Full GC、直接内存 OOM 没有转储
  5. (五)GC 日志:老参数在 17 上起不来、日志被覆盖、jstat 列错位 ------ 本篇
相关推荐
Flynt39 分钟前
Java 27 悄悄改了 3 个默认值,我在 1 核小机器上逐个验证了一遍
java·jvm·性能优化
天天被压力39 分钟前
【Python 量化取数指南 #09】Python 拿港股通数据:港股通成交与港股财报实测
java·人工智能·python
吃饱了得干活39 分钟前
数据放哪儿:从 HashMap 到一致性哈希
java·后端
SL_staff41 分钟前
四维穿透式齐套判断:JVS-APS在制造计划系统中的技术实现
java·spring boot·python
杨杨杨大侠41 分钟前
多 Agent 协作到底靠什么?从 Codex 内部机制到 A2A 企业服务
java·人工智能·agent
AI深栈43 分钟前
第 17 章 · 编排 AI 流程:StateGraph 节点、条件边与意图路由
java·人工智能
吃饱了得干活44 分钟前
Hash 全景:从 HashMap 到一致性哈希,一文吃透哈希核心
java·后端
SL_staff1 小时前
城商行营销翻车复盘:JVS-Rules 如何用工程化机制保障规则变更的可溯性与稳定性
java·开源·全栈
vipxieliang1 小时前
ValidX 多租户系统的数据验证方案
java·spring boot