JDK11 是继 JDK8 之后的 LTS,从 2018 至今经历了3周年考验,安全、性能问题已得到充分暴露,并且版本 9、10、11 包含了便利开发的特性,综合考虑,OPPO 健康云端从2021年中旬筹备从 JDK8 升级到 JDK11 事宜。升级先将从微服务 third-party 开始,然后在升级 admin(提供 Http 服务),观察2周没问题就逐步升级其余微服务。
本文将从升级过程中遇到的问题记录开始,最后介绍 JDK9~JDK11 的新特性,方便给团队宣讲。

问题记录

废弃的 GC 参数

-XX:+PrintGCDateStamps 已废弃,建议使用 -Xlog 统一输出 JVM 日志,避免垃圾回收器输出不同的日志格式。如果你裸眼分析过 gc 日志,你一定知道我在说什么。

Spring Core 0day 漏洞

微服务 admin 升级 JDK11后,公司风险预警扫描到我们使用了 SpringMVC,发出 0day 漏洞预警。对于 Spring core 在JDK9(及以上)版本中,远程攻击者可在满足特定条件的基础上,通过@InitBinder注解的参数绑定功能获取 AccessLogValve 对象并诸如恶意字段值,从而触发pipeline机制并写入任意路径下的文件。
我们解决的方案是在工程中下新建一个 ControllerAdvice 配置:

  1. import org.springframework.core.annotation.Order;
  2. import org.springframework.web.bind.WebDataBinder;
  3. import org.springframework.web.bind.annotation.ControllerAdvice;
  4. import org.springframework.web.bind.annotation.InitBinder;
  5. @ControllerAdvice
  6. @Order(10000)
  7. public class GlobalControllerAdvice {
  8. @InitBinder
  9. public void setAllowedFields(WebDataBinder dataBinder){
  10. String[]abd=new String[]{"class.*","Class.*","*.class.*","*.Class.*"};
  11. dataBinder.setDisallowedFields(abd);
  12. }
  13. }

系统包 sun.misc.* 不存在

sun.misc 是 java 还属于 Sun 公司的时代遗漏下来的工具包,在 JDK9 因为模组系统引入被裁剪了。
由于我们依赖了 Base64 相关的 API,因此我们的解决方案是使用 java.util.Base64.Encoder、java.util.Base64.Decoder 替代之。

Lombok 兼容性问题

首先,使用 JDK11 + Lombok 请先升级 Lombok 版本到 1.18.4。
Lombok 在 java8 时代为了支持函数式编程,引入了关键字 var 和 val,但到 JDK10 引入关键字 var,二者冲突了。
决解方案:全文搜索替换掉 import lombok.var;

JDK 9 特性

升级 SDK 是有工作量且伴随风险的昂贵动作,因此希望升级后开发伙伴能充分利用新特性,提速开发、提升技术。

200: The Modular JDK

模块化系统,一言难尽。你可能听说过 jigsaw,但切实用过吗?没用过它就过时了。
有别于嵌入式,服务端环境不计较存储、内存,JRE 太大确实不好,但运行时类加载失败更痛苦。

222: jshell: The Java Shell (Read-Eval-Print Loop)

官方提供的 REPL 工具,Read Eval Print Loop 交互式解释器。对于 java 推广意义重大,但老手基本不用这个,因为老手少了 IDE 代码提示就不会编程了。(逃

248: Make G1 the Default Garbage Collector

JDK8 之前默认 Parallel GC 垃圾收集器,到了 JDK9 G1 上位。
G1 虽然原理复杂,但可调参数少,CMS 可调参数多,但被标记为废弃了。
面试 JVM 这块卷不起来了,但这才是常态,毕竟 C# 和 Go 都需要 VM,但几乎不用调参。

250: Store Interned Strings in CDS Archives

String 的实现底层由char[] 改为byte[],API 维持不变。提升序列化效率。

264: Platform Logging API and Service

Java 日志平台,旨在管理 JDK 产生的日志,包含系统调用、JVM、监控日志等。
SLF4J 提供了 slf4j-jdk-platform-logging.jar,可以通过日志平台,使 JDK 日志按 SLF4J 格式输出。

269: Convenience Factory Methods for Collections

List、Set、Map 等集合可以使用静态工场方法 of 轻松构建出一个不可变集合。

未列入 JEP 的特性

接口私有方法

JDK8 为了支持函数式编程引入 default 修饰符,可以理解。但接口支持 private 我没领会精神。

增强 try-with-resources

JDK8 要求实现 Closeable 的类在 try 语句中实例化,到了 JDK9 就没有这个限制,如果希望 try 帮忙调用 close,只需要把 Closeable 的引用放入 try 即可。

加强 Stream API

增加 takeWhile、dropWhile 等方法,允许遍历到中途就退出。

反应式流 Reactive Streams

java.util.concurrent.Flow 类中新增了反应式流规范的核心接口。响应式编程已经说了好多年了,至今未推广开来。

JDK 10 特性

286: Local-Variable Type Inference

var 可以在编译阶段推断出类型,代价是必须初始化为一个值。这是大趋势,毕竟变量声明出来是要用的,使用前必须初始化。类型推断对于函数式编程来说很重要,相信 JDK8 引入的函数接口已经充分教育 java 程序员了。

307: Parallel Full GC for G1

JDK9 的 G1 FullGC 依然使用单线程完成 mark-sweep,从 JDK10 开始使用并行的标记清除算法。了解下就行。

JDK 11 特性

318: Epsilon: A No-Op Garbage Collector

Epsilon 是一个不会回收垃圾的垃圾回收器,一旦可用堆内存用完,JVM退出。
这意味着 System.gc() 调用不会有效果,Epsilon 根本不会回收,一旦启动,要么主进程结束,要么爆堆而亡。
看起来很鸡肋,但它依然有适用的场景:

  • 性能测试。Epsilon 可以帮助过滤掉GC引起的性能损失;
  • 量化交易。一些阿尔法策略甚至不容忍 10ms 的延迟,那么干脆禁止 GC,等收盘后结束进程;
  • 内存压力测试。测试用例配置 -Xmx1g -XX:+UseEpsilonGC,如果违反该约束,则 heap dump并崩溃;
  • 一次性任务。例如 Jshell 求一个算术平均,使用GC清理堆都是浪费空间、时间;

    321: HTTP Client (Standard)

    官方实现,支持 HTTP/2 和 WebSocket 的HTTP客户端。
    pros:不必依赖 Apache HttpClient 和 OkHttp
    cons:接口的设计不够 fluent,请求入参也需要通过 String 构造

    328: Flight Recorder

    简称 JFR,是一个低开销的数据收集框架,用于在运行时分析Java进程、JVM运行状况及性能问题。
    市面上已经有同类产品,比如阿里巴巴开源的 Aarthas,虽然开销不如 JFR,但胜在方便(解读 jfr 快照还需要自行编译 JMC)。
    此外从安规角度考虑,企业也会也会提供自己的 profiler,所以 JFR 更多是聊胜于无吧。

    330: Launch Single-File Source-Code Programs

    可以使用 java 命令识别到 **.java 文件会自动补一个 javac,省地用户亲自编译。

    332: Transport Layer Security (TLS) 1.3

    如果客户端支持,使用 TLS1.3 可以实现首次握手 1-RTT,会话重用 0-RTT。
    TLS1.2 兼容历史浏览器,在第一个RTT 需要协商算法版本等信息, 在第二个RTT 才能完成密钥协商。
    TLS1.3 禁用历史密钥算法,强制使用 PSK,在第一个RTT 就能完成密钥协商。

    333: ZGC: A Scalable Low-Latency Garbage Collector

    ZGC 在 JDK11 还是实验特性,但到 JDK14 已经孵化为 production-ready,可谓进步神速。它有以下特性:
    可扩展:最大支持4TB堆内存,无论内存多大,均可以维持 O(1) 停顿时间
    低延迟:设计最大停顿时间10ms,在 SPEC JBB 2005 基准测试中,TP99 小于 1ms,MAX 1.68ms
    兼容性:支持 numa 架构,此外除了 Parallel Scavenger,CMS、G1 均不支持 numa
    无分代:未利用上对象朝生夕死的统计学结论,导致对象分配速度拉跨,但官方后期会选择技术路线解决该问题
    为什么可以做到无视堆大小的低延迟收集,而且 TP99 还低于 1ms,并且 throught 只牺牲了 10%?

    参考文献

  • JDK9 Features

  • JDK10 Features
  • JDK11 Features
  • ZGC_ The Next Generation Low-Latency Garbage Collector
  • Java Flight Recorder初探
  • 听R大论JDK ZGC
  • Linux 内核 101:NUMA架构