JDK 27运行时更新详解:G1默认GC、紧凑对象头、JFR脱敏三大变更


G1成了Java 27所有环境里的默认垃圾回收器,连1核CPU、1.7GB内存的容器也一样,不写参数就是它!

小机器跑Java的兄弟注意:你的容器如果从来没配过GC参数,升级到JDK 27之后,垃圾回收器会悄悄换人!

JDK 27把九个JEP里的三个运行时变更全部设为默认行为,你什么都不用做,它们就生效了。G1替掉Serial、对象头瘦身33%、JFR录音自动打码敏感数据,而JVMCI被整个删掉。

默认垃圾回收器换人了小机器却成了新常态

JDK 9到JDK 26这十八个版本里,HotSpot JVM在启动时会干一件事:检测到单核CPU或者物理内存不到1792MB,就默默把Serial GC选为默认值。很多容器化部署的Java应用其实一直跑在Serial GC上,只是开发团队压根不知道,因为没人显式配过GC参数。

但是JDK 27把这条隐式规则彻底废了。JEP 523规定G1在所有环境下都是默认垃圾回收器。Serial GC没有从JDK里删掉,你手动写-XX:+UseSerialGC照样能用,只是JVM不会再替你自动选它了。

为什么Oracle敢这么干?因为G1在后续版本里持续改进,在受限环境下的表现已经追上了Serial。OpenJDK质量团队给出的对比是:G1的原生内存开销和Serial差不多,吞吐量略低,但最大延迟更低。翻译成人话就是,小机器上G1可能算得稍微慢一点,但卡顿时间更短。

问题在于你的容器可能对吞吐量比对延迟敏感得多。一个跑在1核512MB容器里的后台任务,Serial GC的单线程简单粗暴反而更划算。升级到JDK 27之后不测GC就上线,性能翻车了都不知道问题出在哪!

这就要看你到底跑的是什么负载。

对象头瘦身三成内存省钱有了实锤

很多人以为Java对象的内存开销大头在字段数据上,但真实情况是对象头这个“公摊面积”在小对象场景下能吃掉超过20%的堆空间。

HotSpot JVM在64位架构上,每个对象头占96个比特,也就是12个字节。JEP 534把它压到了64个比特、8个字节,省了整整三成。这不是理论推演,是有基准测试数据的:SPECjbb2015用少了22%的堆空间和8%的CPU时间跑完,GC次数减少15%,一个高并行JSON解析器快了10%。

但是在最理想情况下,Amazon在生产环境跑了数百个服务之后,最高看到CPU用量降了30%。这个数字当然有水分,取决于你的应用里小对象占比多少,但哪怕只省10%的堆,对内存账单也是实打实的减少。

注意,紧凑对象头在JDK 25就已经是正式特性了,只是需要你手动加-XX:+UseCompactObjectHeaders才能生效。JDK 27把它变成了默认行为,不用加任何参数。想关掉的话用-XX:-UseCompactObjectHeaders,但除非遇到兼容性问题,否则没有理由关。

这就怪了——这么大的内存节省,为什么两年前不默认开?因为对象头从96比特压到64比特意味着有些元数据位被裁掉了,JVM团队需要足够多的生产验证才敢把这个改动推到所有用户面前。Amazon和SAP的测试数据成了关键推手。

JFR录音默认打码密码再也不裸奔了

JDK Flight Recorder是HotSpot JVM内置的低开销诊断框架,它会把JVM运行时的信息写进录音文件里,供事后分析用。问题在于,这些录音文件里会原封不动地记录你启动JVM时的命令行参数和环境变量,包括密码、API密钥和访问令牌。

一个典型的启动命令长这样:

bash
$ export ACCESS_TOKEN=SECRET_TOKEN
$ java -XX:StartFlightRecording:filename=dump.jfr \
  -Xmx2G \
  -Djavax.net.ssl.keyStorePassword=SECRET_PASSWORD \
  -jar application.jar \
  --dbpassword ANOTHER_SECRET_PASSWORD

产生的dump.jfr文件里,这三个密钥全部明文可见:环境变量事件记录ACCESS_TOKEN,系统属性事件记录keyStorePassword,JVM信息事件记录--dbpassword。你把录音文件发给同事排查问题,或者上传到支持工单系统,密钥就泄露了!

JEP 536把这个问题堵住了:JFR默认会对敏感信息脱敏,匹配到的值在录音文件里显示为[REDACTED]。默认过滤器覆盖了密码、API密钥、凭证、令牌、私钥等常见敏感字段。

你想加自定义过滤规则也可以,比如屏蔽环境变量里的dburl:

bash
$ java -XX:FlightRecorderOptions:redact-key=dburl

过滤器多了可以写进单独的文件里引用:

bash
$ java -XX:FlightRecorderOptions:redact-arguments=@args.txt

想保留默认过滤器的同时追加自己的规则,在第一个过滤器前加个+号:

bash
$ java -XX:FlightRecorderOptions:redact-key=+dburl

彻底关掉默认过滤用none。过滤器用的是glob模式,*?作为通配符,匹配时大小写不敏感。

这个改动的核心逻辑是:脱敏发生在数据离开JVM进程之前。也就是说,即使你拿到的录音文件被别人截获了,里面的密码也是打码状态。

JVMCI被删了Graal编译器的老路走不通了

JVM Compiler Interface是JDK里一个实验性功能,它允许用Java写的编译器(比如Graal)替换HotSpot默认的C++即时编译器。如果你的项目依赖GraalVM的JIT编译能力,JVMCI就是那条通路。

JDK 27把这条通路拆了。JVMCI相关的HotSpot代码、jdk.internal.vm.ci模块、jdk.graal.compiler模块、所有名字里带JVMCI的编译标志,以及-XX:+UseGraalJIT参数,全部移除。

TornadoVM这个高性能Java计算框架就撞上了这堵墙。它的旧版本硬依赖JVMCI,JDK 27一升级直接跑不了。TornadoVM 6.0的应对方式是砍掉对JVMCI的依赖,改用反射、ASM和Unsafe,让同一个构建产物能同时跑在JDK 21到JDK 27上。

但是对普通开发者来说,这件事的影响几乎为零。JVMCI从来都是实验性的,用它的项目屈指可数。如果你不知道JVMCI是什么,你大概率从来没碰过它。真正需要关注的是GraalVM自身的发布节奏,它已经开始转向月度发布,不再绑死在OpenJDK的版本周期上了。

jcmd工具升级安全属性终于能看了

jcmd是JDK自带的命令行诊断工具,你可以用它连到正在运行的JVM进程上查看各种运行时信息。以前它有一个VM.system_properties命令能打印系统属性,但安全属性一直没有对应的命令。

JDK 27补上了VM.security_properties。这个命令打印当前JVM进程的Java安全属性集合,和VM.system_properties一样,输出格式脚本友好,适合在自动化运维流程里做安全检查。

现在排查安全配置问题不用去翻代码了:

bash
$ jcmd VM.security_properties

输出直接给你看当前安全设置到底是什么。

同时,VM.info命令和HotSpot致命错误日志现在会报告当前进程打开的文件描述符数量。如果你的应用在高峰期因为文件描述符耗尽而崩溃,这个数字就是排障的第一手证据。

jcmd还加了Bash自动补全脚本,加载之后敲命令能自动补全:

bash
$ source /conf/bash-completion/jcmd

要全局生效的话,把脚本放到/usr/share/bash-completion/completions/目录下。

启动参数删了一批旧写法该改了

JDK 27删除了四个曾经被废弃的JVM参数:-noclassgc-noverify-verifyremote-Xverify:none

-noclassgc的功能被-Xnoclassgc替代,-verifyremote的功能被-Xverify:remote替代。-noverify-Xverify:none没有替代品,它们的功能彻底消失了。

如果你的启动脚本里还在用这些老参数,JDK 27升级上去会报错。去翻翻你项目里的Dockerfile、Kubernetes部署配置和启动脚本,搜一下这几个参数。

-XX:InitiatingHeapOccupancyPercent被改名为-XX:G1IHOP。旧名字还能用,但已经被标记为废弃,未来版本会删掉。趁现在把启动命令改过来。

-XX:+UseCompressedClassPointers和它的关闭选项-XX:-UseCompressedClassPointers现在彻底过时了。JVM现在永远使用压缩类指针,你加不加参数都一样,加了还会打印警告信息。

G1堆伸缩默认关闭System.gc()不再白干了

G1垃圾回收器在JDK 27里改了一个容易被忽视但影响不小的默认值:-XX:MinHeapFreeRatio变成了40和70,-XX:MaxHeapFreeRatio变成了0和100。

以前Full GC之后,G1会检查堆的空闲比例,如果空闲太多就缩小堆,空闲太少就扩大堆。有些应用在代码里频繁调System.gc(),每次Full GC之后堆都要伸缩一次,白白浪费CPU和内存管理开销。新默认值让G1的堆伸缩实际上被禁用了,Full GC之后再也不会触发自动扩缩容。

你自己显式设了-XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio的话,原来的行为不变。没设过的话,系统gc()调用的副作用少了一个。

JFR的一个事件也被关掉了:jdk.OldObjectSample在使用分代ZGC时不再产生数据,因为性能开销大到不可接受。如果你的JFR配置依赖这个事件做对象年龄分析,切到分代ZGC之后这块数据会缺失。

JSON线程转储格式变了解析器要跟着改

jcmd Thread.dump_to_fileHotSpotDiagnosticMXBean.dumpThreads生成的JSON线程转储,以前把线程ID、线程数量和进程ID都输出为字符串,比如{ "tid": "3", .. }。JDK 27把它们改成了数字,变成{ "tid": 3, .. }

如果你的监控系统或日志分析工具解析这些JSON字段时假设它们是字符串,升级JDK 27之后解析会出错。转储对象里还新增了一个formatVersion字段,当前值是2,以后线程转储格式再变的话这个数字会跟着更新。

现在G1在1核1.7GB的容器里成了默认垃圾回收器,如果你不主动加-XX:+UseSerialGC,升级之后小机器的GC行为就变了。Oracle的建议是:“小环境里不选GC的话,要么接受G1,要么显式配Serial。”而你的下一个容器可能比1核1.7GB还小——AWS Lambda的Java运行时给的内存上限远低于1792MB这个阈值,G1在那些环境里到底跑得怎么样,目前还没有公开的基准测试数据。