1. 升级前的准备:别急着换,先摸清家底
说起JDK升级这事儿,我一开始也没当回事。公司里有个老服务,从JDK 8一路升到11,跑得好好的,日常没人动它。后来因为有新需求要用到虚拟线程和Record模式,我在本地试了试,确实香,就琢磨着把它升到21。结果从动手到全量发布,前后折腾了将近一周,踩了一堆坑,心态从“这不就改个版本号的事”变成“我到底在升什么”。
先说个结论:JDK 11到JDK 21,可不是小版本平滑升级,中间隔着12到20这好几个大版本的变化。Oracle从Java 17之后改成每两年一个大版本,21是个LTS版本,所以该升还得升,但不能头铁直接一把梭。磨刀不误砍柴工,升级前必须做几件事,这里按优先级来。
1.1 先从“依赖现状”入手,别指望一把梭
很多人在升级前最爱问的一句话是:“我把Dockerfile里的镜像版本换成21能不能跑起来?”表面上看能,实际上你根本不知道项目里塞了多少藏在角落里的老依赖。我踩的第一个坑就是:以为项目纯净得很,结果一跑就报NoSuchMethodError。
所以我建议的第一步,是把你项目里的所有第三方依赖拉个清单。用Maven的dependency:tree,或者Gradle的dependencies命令,把完整依赖树导出来细细过一遍。重点盯这么几类:
- 字节码操作类库:CGLIB、ASM、Javassist,这类库跟JDK内部结构和字节码版本强绑定,稍老一点就没法在21上跑。
- 序列化/反序列化库:Kryo、FST、Protostuff,这些都是拿反射和Unsafe实现的,在更高版本的JDK上最容易出事。
- 动态代理/字节码增强框架:Spring AOP、MyBatis、Hibernate这类带运行时代理机制的框架,要么升到新版本,要么等官方支持。
- 日志桥接库:Log4j、SLF4J、Logback之间层层叠叠,老版本在不同JDK上表现差别极大。
怎么更精准地判断哪些库有兼容问题呢?我推荐你用JDK自带的jdeps工具,对打好的jar包做一次静态分析,看它依赖了哪些JDK内部API。这个工具在老版本上快被遗忘了,但在升级场景里是真的好用。
jdeps --multi-release 21 -s your-app.jar如果看到类似jdk.internal.misc.Unsafe或者sun.misc.Unsafe这样的输出,那就得格外小心了,这些内部API在21里被收得更紧,能用新的方式替代就尽早换。
1.2 列一张“我正在用哪些JDK特性”的清单
自己的代码比第三方依赖更可控,但也更需要过一遍。我在升级前把项目里的Java代码翻了一遍,专门排查以下几类API的使用情况:
sun.misc.Unsafe:这个是重灾区,很多老一代的工具类和框架都拿它做内存操作。在JDK 21里,Unsafe虽然在,但很多路径加了强限制,一不小心就报ExceptionInInitializerError。finalize():这个方法和System.runFinalization()系列在JDK 18被标记为废弃,21里虽然还能用,但强烈不建议再依赖它做资源释放。我们代码库里居然有一个老工具类还在用finalize做Socket清理,我直接改成try-with-resources了。Thread.stop()、Thread.destroy()这些早已废弃的线程方法:在21里遇到就直接报UnsupportedOperationException,有些老代码在“优雅停机”逻辑里会嵌这个,很隐蔽。- Java EE相关包:比如
javax.annotation.PostConstruct、javax.annotation.PreDestroy这种——这是从Jakarta EE 9开始出现的经典改动,javax.*改成jakarta.*,Spring Boot 3.x、Tomcat 10.x全受影响,这个我会在后面单独展开。
顺便说一句,如果你用了不少内部API且一时改不完,JDK 21还保留了--add-exports和--add-opens这类命令行参数,可以在小范围内做兼容。但注意,这只是过渡手段,千万别长期焊死在启动脚本里,不然新版本升级的坑永远躲不掉。
1.3 提前想好“回滚方案”
这不是说丧气话,而是真实教训。我第一次在一台预发机上跑新编译的包,跑了半小时一切正常,正当我以为大功告成时,一个沉睡很久的定时任务突然被唤醒,直接把一堆内存操作打爆了。预发环境的流量还好,生产环境要是发生这种事,没有快速回滚预案的后果不敢想。
所以,在你决定升级前,先确认几件事:
- 旧版本的镜像/构建产物是否还保留着?至少保留两版。
- 发布系统能不能一键回滚?如果不能,请先把旧包上传到一个可直接拉取的目录。
- 数据库、Redis这类外部依赖有没有版本适配问题?JDK本身容易查,但内存、GC参数换了,外部资源的使用方式也得跟着调整。
如果你用的是Kubernetes那套,建议先升级一个副本,观察一段时间,再逐步扩展,不要一口气把Deployment里的副本全换新。这个流程我们在后面“上线顺序”里再细说。
2. 核心改动:build文件和代码适配,一处处抠
升级前的工作做扎实了,下一步就是动代码和构建脚本了。这里有一说一,绝大多数“升级失败”的报错,最终都能追溯到构建配置或依赖版本不匹配上,真正要改自家业务代码逻辑的反而不多。
2.1 Maven和Gradle配置的变化点
我的项目用的Maven,所以先从Maven说起。
第一件事,切maven.compiler相关配置。很多老项目的pom.xml长这样:
<properties> <java.version>11</java.version> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties>改成21是最基础的,但千万别只改版本号就完事。我建议顺手把maven-compiler-plugin升到3.11.0以上,因为老版本插件对JDK 17+的编译支持不完整,可能会出现“非法目标发行版”或者注解处理器不生效的问题。
第二件事,升级你用的Spring Boot版本。这是JDK 11升级到21时最容易卡住的大头。Spring Boot 2.x是基于javax命名空间的,而且官方对Java 17+的适配只在小版本里做过测试,真要稳妥地跑在JDK 21上,建议直接跳到Spring Boot 3.2以上版本。这里要说明的是:Spring Boot 3.x本身要求Java 17起步,我们升到21完全兼容。
Spring Boot从一个主版本跳到另一个主版本,不只是版本号变化,还牵扯很多配置项和自动配置类的改动。比如spring.factories改成META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,自定义starter的写法也变了。这块我建议你看看官方迁移文档,但别指望文档全对,还是以实际运行报错为准。
第三件事,Lombok。很多人忽视了它。早期的Lombok版本对高版本JDK支持很差,因为Lombok是靠编译期注解处理器深入JDK内部API的。JDK 16开始强封装了JDK内部API,老Lombok直接编译报错。我最初用1.18.24在本地编译都能过,CI里换了台新机器直接Fail,就是这个问题。升到1.18.30以上基本能稳。
Gradle用户也一样,除了看项目的build.gradle里sourceCompatibility和targetCompatibility,还得检查你用的Gradle版本本身支持不支持Java 21。Gradle 7.5以下官方都不支持Java 18的特性,建议升到8.5以上。
2.2 javax到jakarta,这段迁移最琐碎
这个坑我在升级前是预料到了,但没想到这么琐碎。以前写代码习惯性import javax.annotation.PostConstruct;,Spring Boot 3.x一下全改成jakarta.annotation.PostConstruct了。
不只是注解,还有这些:
javax.servlet.*→jakarta.servlet.*javax.persistence.*→jakarta.persistence.*javax.validation.*→jakarta.validation.*javax.transaction.*→jakarta.transaction.*javax.xml.bind.*→jakarta.xml.bind.*
如果你代码里直接用了这些包,搜索引擎搜出来的老博客里全是旧写法,一个不留神就复制粘贴错了。更麻烦的是,有些库可能在传递依赖里把新旧两个命名空间都带进来了,编译不报错,运行时启动直接报Bean冲突——这类问题我后面会列在“隐蔽坑”里。
那有没有快速定位的方法?有,全局搜索代码和配置文件里的javax.前缀,一处一处改。注意,改完编译后开发工具里的缓存最好清一下,否则残留的编译缓存会迷惑你,让你以为没改干净。
我试过用IDEA自带的重构功能批量替换包名,但因为Spring Boot Starter里的很多依赖自带javax和jakarta两套包,全局替换容易把第三方的关系搞乱。我的做法是:只替换自己源码里的javax.*,第三方框架的包路径靠升级依赖版本解决,不要自己动手改第三方包。
2.3 内置API和废弃API的清理
JDK 11升到21,这里有个常见的“暗坑”:很多你从没用过但框架底层在用API变了。比如SecurityManager在JDK 17被标记为废弃,21里还在,但未来大概率移除;ThreadGroup虽然不是废了,但很多方法已经不可靠。如果你的项目有安全管理器之类的东西,基本可以删掉了。
另一个高频改动是java.util.logging和其他日志框架的桥接问题,这个不是JDK本身的问题,而是某些老库对LoggerFactory的绑定方式不兼容。常见的报错是LoggerFactory is not a Logback LoggerContext but Logback is on the classpath。这个多半是slf4j-api版本太老导致的,升级到SLF4J 2.x就能解决。
还有一点,是我自己项目里遇到的:JDK 17开始,java.lang.reflect.Proxy对于非public接口的动态代理默认不允许跨模块访问了。换句话说,如果你的代码里给包级私有接口做了动态代理,运行时会直接报IllegalAccessError。解决办法有两个:一是把接口改成public,二是在启动参数里加--add-opens。前者是治本,后者是应急。
JDK 21还默认启用了Dynamic Agent Loading的告警,你在启动日志里可能会看到一行WARNING: A terminally deprecated method in java.lang.System has been called之类的字眼,别慌,它只是提醒你某些老方法快不行了。建议把启动日志里所有WARNING和deprecated提示搜集起来,逐个处理,别放着不管,不然积累到下一个LTS版本升级时又是一笔大债。
2.4 代码层面更值得关注的几个改动
除了javax这波大迁徙,还有几个相对容易忽略的API变动,整理成一张表:
| 变更点 | JDK 11写法 | JDK 21推荐写法 | 影响范围 |
|---|---|---|---|
java.util.Date和Calendar相关 | 老代码里到处是 | 换用java.time(LocalDate/LocalDateTime) | 日期时间处理、定时任务 |
String构造函数 | new String(bytes) | 明确指定字符集,或直接用new String(bytes, StandardCharsets.UTF_8) | 文本处理、编码相关 |
BigDecimal构造方法 | new BigDecimal(double) | 用BigDecimal.valueOf(double)或字符串构造器 | 金额计算,精度问题 |
File相关API | File#delete()判断布尔值 | 用Files.delete(),它抛异常更明确 | 文件操作 |
| Java EE包 | javax.* | jakarta.* | Web应用、ORM、Bean Validation |
别小看这些改动。看起来都不难,但藏在大规模代码里的时候,逐个排查特别耗时。我个人的建议是:别想着一次性改完,按照“编译能过 → 单元测试能过 → 集成测试能过 → 生产验证”的顺序来,每走一步都提交一次。
这里多说一句,Java 21的虚拟线程(Virtual Threads)确实是个大亮点,但别天真地以为“代码里加个参数就自动用上了”。要把线程池那些改成虚拟线程,需要认真评估锁竞争、ThreadLocal使用、native方法对接等等。我这次升级没有大面积切虚拟线程,只在新建的一个异步任务模块里用了,求稳。
3. 构建编译和运行时环境:改镜像、调参数
代码层面改完,接着就到构建和运行环境了。这个环节看似简单,实际是最容易出问题的地方,尤其是你在CI/CD里用的构建工具和基础镜像版本不匹配时,报错能绕晕你。
3.1 基础镜像怎么选,必须是“多阶段构造”的思路
之前项目用的Dockerfile是FROM openjdk:11-jre-slim这种老写法。但注意,OpenJDK官方镜像从JDK 17之后就不再更新带-jre标签了,很多老镜像里的底层系统还是Ubuntu 20.04甚至更老,安全性也跟不上。
建议改用Eclipse Temurin或者Amazon Corretto的镜像,比如:
FROM eclipse-temurin:21-jre-alpine我踩过一个坑:alpine镜像很瘦小,但带的是musl libc,某些用到了JNI的原生库在alpine下没法跑。如果你项目里有JNA、OpenCV或者其他本地库依赖,请选择基于Ubuntu或UBI的发行版镜像,比如eclipse-temurin:21-jre-jammy。
如果你不想在项目里直接改基础镜像,也可以在发布系统里通过参数覆盖镜像版本,但这样做最大的问题是版本漂移,开发本地用的还是旧版本。一句话总结:基础镜像在Dockerfile里固定死,不要靠外部参数注入。
另外,如果你们公司有统一的私服镜像源,记得同步拉取JDK 21对应镜像到私服。别到了发布当天才发现私服上没有,临时去外网拉,网络一波动整个发布流程全卡住。
3.2 JVM参数从11到21的变化
很多人升级完JDK,启动脚本还是老一套参数,这里就要出问题了。JDK 21的垃圾回收器和默认参数有过不少调整,老参数有些已经没用了,有些则不再推荐。
举几个最实际的例子:
-XX:+UseConcMarkSweepGC:CMS垃圾收集器在JDK 14被移除,直接不能用了。项目如果还用这个参数,启动就直接失败。-XX:+UseParNewGC:同样被移除。-Xmn、-XX:PermSize、-XX:MaxPermSize:这些参数在JDK 8时代就被元空间替代,在21里会被忽略,但如果你不小心还把PermSize挂在脚本里,有可能被JVM警告。-XX:+PrintGCDetails、-XX:+PrintGCDateStamps:JDK 9开始统一改成-Xlog:gc*,旧参数虽然在后续版本中还能用,但官方建议换用统一日志风格。
你这会儿可能要问了,那到底该怎么设?没有万能参数,但可以参考一个比较稳妥的起步配置:
java -Xms4g -Xmx4g \ -XX:MaxMetaspaceSize=512m \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=100m \ -jar your-app.jarG1GC从JDK 9开始就是默认收集器,在21里也是主流选择。如果你的堆内存特别大,比如超过32G,可以试试ZGC,它的停顿时间更短,但需要额外确认操作系统版本和容器内存限制之间的配合,不然会看到奇怪的效果。
容器环境里还要注意-XX:+UseContainerSupport,JDK 10以后默认开启,不用你手动加。但如果你还习惯在启动脚本里写-XX:MaxRAMPercentage=75.0这类百分比参数,那就别再加固定-Xmx了,二者混用容易导致内存分配不符合预期。
3.3 模块化“坑王”:module-info与classpath的碰撞
JDK 9之后引入了模块系统,如果你有同学在项目里加了module-info.java,那升级21时特别容易踩到“包冲突”的坑。更多时候,项目没主动加模块化,但某些第三方库的模块描述符里写了强依赖,你把它和其他库放在同一个classpath下,就可能导致Module java.base does not export ...这样的启动错误。
遇到这类报错,最直接的解法就是用--add-opens或者--add-exports,把它们加进启动脚本里。比如:
java --add-opens java.base/java.lang=ALL-UNNAMED \ --add-opens java.base/java.util=ALL-UNNAMED \ -jar your-app.jar不过我得提醒一句:这是一种“缓解方案”,并不是“修复方案”。有一堆add-opens就说明你的依赖没彻底升级干净。长期来看,还是得把那些依赖模块内部机制的老库替换掉。
重要提示:如果你一个模块化应用用到了Spring或Spring Boot,要格外注意。Spring框架在Java 9+的模块化环境里一直强调“非模块化运行”,千万别轻易给项目加module-info,否则一堆反射操作会撞墙。
3.4 构建工具的隐藏雷区
说实话,我这次折腾最久的不是业务代码,而是CI里的构建工具版本。
- Maven用户,请确认Maven本身在3.9以上,否则可能不支持Java 21编译产物的处理。
- Gradle用户,Gradle 8.5以上稳妥,我项目里曾从7.6直接升8.4,发现有插件报兼容问题,升到8.6才好。
- 如果你在Jenkins pipeline里用了老旧的JDK工具链配置,也要把JDK 21的安装路径和版本号补进工具配置里。
再有就是,如果项目里用了maven-shade-plugin打胖包,建议升到3.5.x;maven-assembly-plugin升到3.6.0;spring-boot-maven-plugin随Spring Boot版本一起升。这些插件的旧版本在处理高版本class文件时多多少少有些脑血栓问题。
4. 实际升级实录:一次完整可参考的路径
这里我把自己这次升级的过程完整梳理一遍,给大家一个可以直接照抄的路径。当然,每个项目情况不同,但这套顺序是通用的,踩坑概率会小很多。
4.1 我的升级步骤总览
我把整个过程拆成六步:
- 准备一个独立的Git分支,专门做升级,别跟业务需求混在一起。
- 在本地把JDK切到21,用IDE打开项目,先让代码能编译过。
- 把构建脚本里的Java版本、插件版本、依赖版本逐项更新。
- 跑单元测试,修所有编译期和运行期错误。
- 在预发环境部署,用典型接口做回归测试。
- 灰度发布,观察JVM指标和生产日志。
你可能觉得第一步是废话,但很多团队真就是直接在主干上改,改到一半发现浪费了好几天,想回滚都困难。独立分支很重要。
4.2 编译期报错清单(这些都在预料中)
在实际编译期间,我遇到了这么几类报错:
第一类,无法访问javax.servlet.*。这个就是包名迁移问题,因为Spring Boot 3.x内置的Tomcat 10+已经强制使用jakarta命名空间,项目代码里的老import必须改。
第二类,程序包com.sun.nio.file不存在。有个老工具类用到了com.sun.nio.file.SensitivityWatchEventModifier,这是我们自己代码的问题,改掉这个调用就行。这类非官方API在JDK 17之后都被模块封装得死死的,不再向外部暴露。
第三类,lombok相关报错,比如java.lang.NoClassDefFoundError: lombok/launch/AnnotationProcessorHider$AnnotationProcessor。真的是措手不及,因为我在本地编译都好好的,跑到CI就炸了。后来发现是CI里的Maven进程用了老JDK去启动Lombok,这个不只是版本号的问题,而是整个链路的JDK都要对齐。
4.3 运行期问题实录(最折磨人的阶段)
编译过了、测试过了,不代表就万事大吉。运行期遇到的问题更多,也更隐蔽。
第一个经典报错是java.lang.reflect.InaccessibleObjectException: Unable to make field ... accessible。面对这种问题,先不要急着在启动脚本里--add-opens,先去网上搜一下这个类是从哪个第三方库来的,看看新版本有没有修复。比如有些CGLIB老版本动态生成子类时,会尝试反射访问目标类的私有字段,在JDK 17+必然失败,升级CGLIB版本就能解决。
第二个经典报错是NoSuchMethodError。这十有八九是依赖中的方法签名跟运行时实际加载的类不一致。比如两个库分别传递依赖了不同版本的ASM或ByteBuddy,构建时是A版本,运行时被B版本覆盖了。处理方式就是利用Maven的dependencyManagement或Gradle的resolutionStrategy统一指定一份最新的版本,别指望系统自动帮你解决。
第三个是启动时候的ClassNotFoundException或NoClassDefFoundError,尤其集中在JAXB、JAX-WS这些模块上。在JDK 11时代,你可能已经习惯单独引javax.xml.bind:jaxb-api这类的依赖了,到了JDK 17+,这类模块直接从JDK中移除了,必须显式引入对应的独立依赖。但如果你已经升级到Spring Boot 3.x,注意要用jakarta.xml.bind:jakarta.xml.bind-api,不要再引javax的旧包。
第四个是隐藏的ThreadLocal内存泄漏。Java 21里虚拟线程的ThreadLocal是虚拟线程私有的,虽然数量多时更吃内存,但真正让人头疼的是老代码里大量使用ThreadLocal存一些大型对象,换成高并发、高线程数场景会明显放大内存压力。我在一个异步处理模块里就遇到过这个问题,后来把大对象从ThreadLocal里挪出来,改用方法参数传递。
4.4 用两个工具帮你少走弯路
如果你觉得上面这些排查太靠经验,我推荐两个工具:
- OpenRewrite:这是一个批量重构框架,有现成的迁移规则集,可以自动把Spring Boot 2.x升级到3.x、把javax改成jakarta、把JUnit 4改成JUnit 5。虽然不是银弹,但能解决大部分机械性替换。
- jdeps:前面已提过一次,少不了的静态分析工具,建议在预发验证前对最终产物再跑一次,它能提示有哪些类还依赖JDK内部API。
用法示例:
jdeps --jdk-internals --multi-release 21 your-app.jar看到输出里列出JDK Internal API的行,就一条条核对。这些内部API在后续版本中可能直接消失,早改早安心。
5. 上线部署与常见问题排查:稳中带皮
代码改完、本地跑通,最后一步是上线。上线阶段的常见问题和前几个阶段又不太一样,更偏向运维和运行环境视角。
5.1 容器内存与JVM内存如何匹配
这是个大坑,很多团队升级后被OOMKilled打得很惨。原因在于,你没给JDK 21的元空间、线程栈、直接内存留足空间。在容器里,如果你设置了-Xmx4g,但容器的memory limit只有4g,那JVM在一开始会好好的,一旦GC和直接内存占用上去,直接被内核干掉。
我目前的习惯是,在Docker Compose或K8s里设置容器的内存limit为JVM堆内存的1.5倍左右。举个例子,如果应用堆需要4g,那么容器内存限制至少给6g,再多留一点给线程栈和Metaspace。别迷信“极限压缩内存”那套,稳定性优先。
启动参数里也建议配合使用-XX:MaxRAMPercentage=70.0这种方式,而不是写死-Xmx。百分比方式的优势是:当容器内存调整时,JVM堆能自动跟着变,不用我手动改参数。
5.2 灰度发布和回滚策略
升级JDK这种基础运行时的事故影响面往往比较大。我的建议是:先找一台机器改tag重启,观察5到10分钟JVM指标,特别是GC频率、Full GC次数、老年代占用率。如果一切正常,再按10%、30%、50%、100%的节奏逐渐放量。
放量过程中重点盯四类数据:
- 接口响应时间P99,尤其看有没有明显劣化。
- GC日志,看有没有频繁Full GC或者大对象分配。
- 错误日志,重点搜
Exception、Error、InaccessibleObjectException。 - 业务指标,比如下单量、支付成功率这种跟代码直接相关的数据。
如果发现异常,回滚优先级是:先切流量到旧版本,再排查问题原因,不要在现场做代码修复。旧版本的镜像产物请一定提前保留。
5.3 高频报错速查表
我把这次升级过程中遇到的高频问题整理成速查表,建议收藏:
| 报错现象 | 常见原因 | 解决路径 |
|---|---|---|
Unable to make field accessible | JDK模块化强封装,反射目标类所在模块不允许访问 | 升级相关库版本,必要时加--add-opens |
NoSuchMethodError | 依赖版本冲突或方法签名变更 | 统一依赖版本,清理无用的老版本 |
ClassNotFoundException: jakarta.* | 项目还在用javax,但框架已切到jakarta | 全局替换import,升级依赖版本 |
Module java.base does not export | 使用了JDK内部API | 改用标准API,或用--add-exports过渡 |
Unsupported class file major version 65 | 构建工具/插件版本太老,无法识别Java 21的class文件 | 升级Maven/Gradle/编译插件版本 |
GC overhead limit exceeded | 堆内存不足或者永假死循环 | 检查堆参数,排查代码里大对象分配 |
System property 'java.specification.version'相关异常 | 某些库在启动时校验JDK版本,老版本库不认21 | 升级库版本 |
Could not initialize class sun.misc.Unsafe | 使用了Unsafe内部API | 用标准API替代,或者找新版本库 |
5.4 我最后的避坑心得
最后,再分享几个我这次实操下来觉得特别有价值的小经验。
- 升级前先把所有不再维护的旧依赖查一遍,看看它最后发布的版本支不支持Java 21。如果项目停更在两三年前,别抱幻想了,尽早找替代品。
- 在做全量回归测试时,最好安排一个长时间运行的冒烟任务,比如持续跑十分钟以上的并发压测。很多JDK升级产生的问题,要等高并发或定时任务触发才会暴露,光靠几个接口冒烟根本测不出来。
- 我强烈建议把启动参数里加的
--add-opens集中放到一个配置文件里,边上注释写明“为什么加、谁需要它、何时可以移除”。这样后面的人接手时,才知道哪些参数是临时补丁,而不是当成金科玉律焊死在那里。
升级JDK这事,说难不难,说简单也真不简单。只要不头铁,按照“先摸清依赖 → 改构建和代码 → 本地验证 → 预发回归 → 灰度发布”的顺序来,每一步稳扎稳打,其实比想象中顺利。我在第一次跑通全流程的时候,看到旧服务用上虚拟线程后在高并发下的毛刺明显减少,还是觉得这一周的折腾挺值。