:JDK 8 升 17 GC 日志参数怎么改?PrintGCDateStamps 起不来、-Xlog 对照表、jstat -gcutil 怎么读)
这个系列「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被悄悄忽略,实际跑的是 G1JDK 8 用-Xloggc:gc.log固定文件名,重启一次上次的 GC 日志就没了;17 会把旧文件留成gc.log.017 默认的日志行没有日期时间,只有进程启动后的秒数,要自己加time装饰器jstat -gcutil在 17 上多了CGC/CGCT两列,按列号取数的监控脚本会取错1. 老参数在 JDK 17 上的下场 ✅每个参数单独加在java 参数 -version上跑一遍,两个版本对比:参数JDK 8u381JDK 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:NumberOfGCLogFiles5✅❌ 起不来-XX:GCLogFileSize10M✅❌ 起不来-XX:UseConcMarkSweepGC✅❌ 起不来-XX:UseParNewGC⚠️Using the ParNew young collector with the Serial old collector is deprecated…❌ 起不来-XX:CMSInitiatingOccupancyFraction75✅❌ 起不来-XX:UseCMSInitiatingOccupancyOnly✅❌ 起不来-XX:CMSClassUnloadingEnabled✅❌ 起不来-XX:PermSize128m⚠️ignoring option PermSize128m; support was removed in 8.0❌ 起不来-XX:MaxPermSize256m⚠️ignoring option MaxPermSize256m; 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-version21)if[$?-ne0];thenecho❌$opt-$(echo$out|head-1)elifecho$out|grep-qiEwarning|deprecated|ignoring;thenecho⚠️$opt-$(echo$out|grep-iEwarning|deprecated|ignoring|head-1)elseecho✅$opt;fidone拿一份典型的 JDK 8 启动参数试:$ bash check_opts.sh /usr/java/bin/java -Xms2g -Xmx2g -XX:MetaspaceSize256m -XX:MaxPermSize256m -XX:UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction75 -XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/tmp/gc.log -XX:UseGCLogFileRotation -XX:HeapDumpOnOutOfMemoryError ✅ -Xms2g ✅ -Xmx2g ✅ -XX:MetaspaceSize256m ❌ -XX:MaxPermSize256m - Unrecognized VM option MaxPermSize256m ❌ -XX:UseConcMarkSweepGC - Unrecognized VM option UseConcMarkSweepGC ❌ -XX:CMSInitiatingOccupancyFraction75 - Unrecognized VM option CMSInitiatingOccupancyFraction75 ⚠️ -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:MaxPermSize256m -XX:UseConcMarkSweepGC -XX:PrintGCDateStamps -version只报了Unrecognized VM option MaxPermSize256m,后两个一声不吭,改一个再撞下一个。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 8JDK 1717 实跑的日志行-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:filegc.log——-XX:PrintGCDateStamps装饰器加time[2026-09-20T03:17:23.6390800][0.037s][info][gc] GC(0) Pause Young …-XX:PrintGCTimeStamps装饰器uptime(默认就有)[0.037s]-XX:UseGCLogFileRotation -XX:NumberOfGCLogFiles5 -XX:GCLogFileSize20M输出选项filecount5,filesize20m见第 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:gcagetrace[0.030s][debug][gc,age] GC(1) Desired survivor size 524288 bytes, new threshold 15 (max threshold 15)-XX:PrintHeapAtGC-Xlog:gcheapdebug[0.023s][debug][gc,heap] GC(0) Heap before GC invocations0 (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:filecount5,filesize20m(实测时路径换成了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{staticfinalListbyte[]OLDnewArrayList();staticvolatileObjectsink;publicstaticvoidmain(String[]args)throwsException{longendSystem.currentTimeMillis()Long.getLong(ms,4000);booleancalledfalse;while(System.currentTimeMillis()end){for(inti0;i1000;i)sinknewbyte[4*1024];// 短命对象:触发 Young GCif(OLD.size()300)OLD.add(newbyte[64*1024]);// 慢慢留下 ~19MB:晋升到老年代if(!calledSystem.currentTimeMillis()end-2000){System.gc();calledtrue;}Thread.sleep(5);}System.out.println(done, oldOLD.size());}}JDK 8u381,-Xmx64m -XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:gc8.log(默认 Parallel GC):2026-09-20T03:17:11.5460800: 0.047: [GC (Allocation Failure) [PSYoungGen: 15360K-512K(17920K)] 15360K-512K(58880K), 0.0010461 secs] [Times: user0.00 sys0.00, real0.00 secs] 2026-09-20T03:17:13.5330800: 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: user0.01 sys0.00, real0.01 secs]读法(第一行):2026-09-20T03:17:11.5460800是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*:filegc17.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:filegc17t.log:time,uptime,level,tags[2026-09-20T03:17:23.6390800][0.037s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 13M-1M(62M) 0.482ms [2026-09-20T03:17:23.6840800][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.7490800] 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: lines72 first_gc2026-09-20T03:18:19.5520800: CommandLine_count1 after run2: lines71 first_gc2026-09-20T03:18:23.1050800: CommandLine_count1第二次跑完,文件里第一条 GC 的时间变成了第二次运行的时间,行数没有翻倍,文件头(CommandLine flags:那一行)也只有一份 ——第一次的日志被整个覆盖了。线上最需要看 GC 日志的时候,往往就是进程刚崩溃、被自动拉起之后,而这时崩溃前的那份已经没了。JDK 8 的解法是文件名里带%t(启动时间)或%p(进程号),每次启动一个新文件:-Xloggc:gc8-%t-%p.log → gc8-2026-09-20_03-17-41-pid7877.logJDK 17 同样跑两次filegc17r.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.2830800] Using G1 gc17r.log.0 first[2026-09-20T03:17:36.7490800] Using G117 把上一次的文件改名成了gc17r.log.0,没有覆盖。%t、%p在 17 里也能用:filegc17-%t-%p.log生成gc17-2026-09-20_03-17-42-7893.log(注意 17 的%p是纯数字,8 是pid7877)。轮转之后,文件名也不一样JDK 8,-Xloggc:rot8.log -XX:UseGCLogFileRotation -XX:NumberOfGCLogFiles3 -XX:GCLogFileSize8k:-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*:filerot.log:time,uptime:filecount3,filesize2k:-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:DisableExplicitGCJDK 8u381(-XX:PrintGC)2 行(一行GC (System.gc())、一行Full GC (System.gc()))0JDK 17.0.8(-Xlog:gc)1 行(Pause Full (System.gc()))0JDK 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/YGCTYoung GC累计次数 / 累计耗时(秒)FGC/FGCTFull GC 累计次数 / 累计耗时CGC/CGCT17 才有:并发 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-gcutilpid|awkNR2{print $11}JDK 8 的第 11 列是GCT;JDK 17 的第 11 列是CGC,GCT挪到了第 13 列。脚本不报错,只是从「GC 总耗时」悄悄变成了「并发 GC 次数」。按表头的列名取值,别按列号。9. 本篇速查想做的事JDK 8JDK 17升级前查启动参数——用第 1 节的check_opts.sh逐个试基本 GC 日志-XX:PrintGCDetails -Xloggc:gc.log-Xlog:gc:filegc.log(细节用gc*)带日期-XX:PrintGCDateStamps装饰器:time,uptime,level,tags轮转-XX:UseGCLogFileRotation -XX:NumberOfGCLogFiles5 -XX:GCLogFileSize20M(当前文件带.current):filecount5,filesize20m(当前文件就是原名)重启不丢日志文件名带%t/%p默认会把旧文件留成.0;也可带%t/%p停顿时间-XX:PrintGCApplicationStoppedTime-Xlog:safepoint年龄分布-XX:PrintTenuringDistribution-Xlog:gcagetraceGC 前后堆详情-XX:PrintHeapAtGC-Xlog:gcheapdebug现场看频率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 飞行记录器,(七)工具连不上进程时怎么办。