写在前面:
JDK已经如火如荼的更新到了JDK 19,集团内也在推AJDK 11的升级,升级已然是一个大趋势。本文主要是对体育营销场景升级JDK 11,进行了整理与总结,希望对大家有所帮助。
JDK版本概述
从1996年初JDK1.0发布到现在已经二十多年了, 最新一个版本已经到 JDK 19,JDK17是最新的一个LTS(Long-Term Support)版本。所谓LTS版本就是可以得到至少八年产品支持的版本。到目前为止,有四个LTS版本,JDK 7、JDK 8、JDK 11、JDK 17,下一个LTS版本是JDK 21,预计在2023年9月发布。Oracle 每隔6个月就会有一个短期维护版本(non-LTS)发布出来;然后每隔2年,就会发布一款得到8年长期支持维护的JDK版本,下图是oracle官方发布的roadmap, 可以更直观的看到JDK版本的更新, 这个发布节奏着实让人有点追不上啊。
Java 版本新特性
上图显示了自 Java 8 起到 Java 19 各版本新增特性的数量。从 Java 8 到 Java 19 总共引入了超过 250 个新特性,其中 Java 9 中包含了 91 个新特性,这是由于之前的发布周期较长所导致的,在应用新的发布模型后,各版本的新增特性数量都维持在 10 个左右的水平。下面主要针对各版本的主要特性进行介绍
JDK 9
模块化
模块化提供了比 package 更高级别的聚合,模块是一个包的容器。模块的代码被组织成多个包,每个包中包含Java类和接口;如果我们想让项目成为一个模块,则需要在该项目的java源码的根目录(如果是maven项目也就是src/main/java)添加一个特殊的java文件模块描述符文件 module-info.java,以jdk 里java.sql 模块为例:
/** * Defines the JDBC API. * * @uses java.sql.Driver * * @moduleGraph * @since 9 */ // 声明模块 module java.sql { // 声明依赖模块, transitive修饰符会导致依赖于当前模块的其他模块具有隐式依赖性。 requires transitive java.logging; requires transitive java.transaction.xa; requires transitive java.xml; // 声明哪些包是可以被其它模块访问 exports java.sql; exports javax.sql; // 使用语句(uses statement)和提供语句(provides statement)实现其服务 // 使用语句可以指定服务接口的名字,当前模块就会发现它,使用 java.util.ServiceLoader类进行加载 uses java.sql.Driver; }
接口支持私有方法
JDK 8 为我们带来了接口的默认方法。接口现在也可以包含行为,而不仅仅是方法签名。JDK 9支持了私有方法,可以解决接口中代码复用问题;该特性主要是为了Java 8中default方法和static方法服务的。
public interface TestInterface{ default void method(){ init(); } default void anotherMethod(){ init(); } private void init(){ System.out.println("Initializing");} }
新增Stream API & 集合工厂方法
JDK 9 为 Stream 新增了几个方法:dropWhile、takeWhile、ofNullable,为 iterate 方法新增了一个重载方法。
- takeWhile
takeWhile 方法使用一个断言作为参数,返回给定 Stream 的子集直到断言语句第一次返回 false。如果第一个值不满足断言条件,将返回一个空的 Stream。
// 输出 => abc Stream.of("a","b","c","","e","f").takeWhile(s->!s.isEmpty()) .forEach(System.out::print);
- dropWhile
dropWhile 方法和 takeWhile 作用相反的,使用一个断言作为参数,直到断言语句第一次返回 true 才返回给定 Stream 的子集。
// 输出 => ef Stream.of("a","b","c","","e","f").dropWhile(s-> !s.isEmpty()) .forEach(System.out::print);
- ofNullable
ofNullable 方法可以预防 NullPointerException 异常, 可以通过检查流来避免 null 值。
// 输出 => 0 System.out.println(Stream.ofNullable(null).count());
- 集合工厂方法
// 通过java.util.Set 创建 不可变 的集合实例 Set<String> set = Set.of("A", "B", "C"); // 通过java.util.List 创建 不可变 的集合实例 List<String> list = List.of("A", "B", "C"); // 通过k1,v1,k2,v2,...,形式创建 Map<String, String> map = Map.of("A","V1","B","v2","C","v3"); // 通过 Map.entry 形式创建 Map<Integer, String> map1 = Map.ofEntries ( Map.entry(1, "v1"), Map.entry(2, "v2"), Map.entry(3, "v3"));
改进版 Try-With Resources
try-with-resources 是 JDK 7 中一个新的异常处理机制,它能够很容易地关闭在 try-catch 语句块中使用的资源。try-with-resources 声明在 JDK 9 已得到改进。如果你已经有一个资源是 final 或等效于 final 变量,您可以在 try-with-resources 语句中使用该变量,而无需在 try-with-resources 语句中声明一个新变量。
static String readData(String message) throws IOException { Reader reader = new StringReader(message); BufferedReader br = new BufferedReader(reader); // 不需要重新声明变量 try (br) { return br.readLine(); } }
G1 成为默认垃圾收集器
在 Java 8 的时候,默认垃圾回收器是 Parallel Scavenge(新生代)+Parallel Old(老年代)。到了 Java 9, CMS 垃圾回收器被废弃了,G1(Garbage-First Garbage Collector) 成为了默认垃圾回收器。G1 是在 Java 7 中被引入的,经过两个版本优异的表现成为成为默认垃圾回收器。
JDK 10
APPCDS 应用程序类数据共享
CDS 的全称是 Class-Data Sharing, CDS 的作用是让类可以被预处理放到一个归档文件中,后续 Java 程序启动的时候可以直接带上这个归档文件,这样 JVM 可以直接将这个归档文件映射到内存中,以节约应用启动的时间。这个特性在 JDK 1.5 就开始引入, 但是 CDS 只能作用与 Boot Class Loader 加载的类,不能作用于 App Class Loader 或者自定义的 Class Loader 加载的类。在 JDK 10 中, CDS 扩展为 AppCDS,AppCDS 不止能够作用于 Boot Class Loader,App Class Loader 和自定义的 Class Loader 也都能够起作用,进一步提高了应用启动性能。
多线程并行 GC
在JDK9中G1被选定为默认的垃圾收集器,G1的设计目标是避免发生Full GC,由于Full GC较难产生所以在设计之初只有Young GC和Mixed GC是并行的,而Full GC是单线程使用标记-清理-合并算法进行垃圾回收。G1只是避免发生Full GC,在极端情况下,当G1的回收速度相对于产生垃圾的速度不是足够快时,就会发生Full GC。为了最大限度地减少 Full GC 造成的应用停顿的影响,从 JDK 10开始,G1 的 FullGC 改为并行的标记清除算法,同时会使用与年轻代回收和混合回收相同的并行工作线程数量,从而减少了 Full GC 的发生,以带来更好的性能提升、更大的吞吐量线程的数量可以由 -XX:ParallelGCThreads 选项来控制,这个参数也用来控制Young GC和Mixed GC的线程数。
局部变量类型推断
JDK10 可以使用var作为局部变量类型推断标识符,此符号仅适用于局部变量,增强for循环的索引,以及传统for循环的本地变量;它不能使用于方法形式参数,构造函数形式参数,方法返回类型,字段,catch形式参数或任何其他类型的变量声明。
var list = new ArrayList<String>(); list.add("hello,world!"); System.out.println(list);
反编译后是这样的
ArrayList<String> list = new ArrayList(); list.add("hello,world!"); System.out.println(list);
从示例中可以看出,var 其实是一种语法糖,旨在改善开发者体验
线程-局部管控
这是在 JVM 内部相当低级别的更改,现在将允许在不运行全局虚拟机安全点的情况下实现线程回调。这将使得停止单个线程变得可能和便宜,而不是只能启用或停止所有线程。
基于Java的实验性JIT编译器Graal
Graal 是一个以 Java 为主要编程语言,面向 Java bytecode 的编译器。与用 C++ 实现的 C1 及 C2 相比,它的模块化更加明显,也更加容易维护。Graal 既可以作为动态编译器,在运行时编译热点方法;亦可以作为静态编译器,实现 AOT 编译。在 JDK 10 中,Graal 作为试验性 JIT compiler 一同发布
JDK 11
String 增强
JDK 11 增加了一系列的字符串处理方法:
// 判断字符串是否为空 " ".isBlank(); // true // 去除字符串首尾空格 " JDK11 ".strip();// "JDK11" // 去除字符串首部空格 " JDK11 ".stripLeading(); // "JDK11 " // 去除字符串尾部空格 " JDK11 ".stripTrailing(); // " JDK11" // 重复字符串多少次 "JDK11 ".repeat(3); // "JDK11 JDK11 JDK11 " // 返回由行终止符分隔的字符串集合 "A\nB\nC".lines().count(); // 3
支持TLS 1.3 协议
实现TLS协议1.3版本, 替换了之前版本中包含的 TLS,包括 TLS 1.2,同时还改进了其他 TLS 功能, 在安全性和性能方面也做了很多提升
HTTP Client
在 JDK 11 中 Http Client API 得到了标准化的支持。且支持 HTTP/1.1 和 HTTP/2 ,也支持 websockets。使用起来也很简单,如下:
HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(uri)) .build(); // 异步 client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body) .thenAccept(System.out::println) .join(); // 同步 HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body());
Epsilon:低开销垃圾回收器
新增的垃圾回收器,一个完全消极的 GC 实现,分配有限的内存资源,最大限度的降低内存占用和内存吞吐延迟时间
飞行记录器:JFR
Java飞行记录器(Java Flight Recorder)已经变成JDK 11的一部分了,之前它是一个商业功能,但是伴随JDK 11发布,它从OracleJDK开源到了OpenJDK。
飞行记录器类似飞机上的黑盒子,是一种低开销的事件信息收集框架,主要用于对应用程序和 JVM 进行故障检查、分析。飞行记录器记录的主要数据源于应用程序、JVM 和 OS,这些事件信息保存在单独的事件记录文件中,故障发生后,能够从事件记录文件中提取出有用信息对故障进行分析
JDK 12 ~ 14
G1 收集器优化
JDK 12 为垃圾收集器 G1 带来了两项更新
- 可中止的混合收集集合 :为了达到用户提供的停顿时间目标,通过把要被回收的区域集(混合收集集合)拆分为强制和可选部分,使 G1 垃圾回收器能中止垃圾回收过程。G1 可以中止可选部分的回收以达到停顿时间目标。
- 及时返回未使用的已分配内存 :由于 G1 尽量避免完整的 GC,并且仅基于 Java 堆占用和分配活动来触发并发周期,因此在许多情况下,除非从外部强制执行,否则它不会返还 Java 堆内存。JDK 12增强了 G1 GC,可以在空闲时自动将 Java 堆内存返回给操作系统。
SocketAPI 重构
java.net.Socket 和 java.net.ServerSocket 类早在 Java 1.0 时就已经引入了,它们的实现的 Java 代码和 C 语言代码的混合,维护和调试都十分不易;而且这个实现还存在并发问题,有时候排查起来也很困难。JDK 13 将 Socket API 的底层进行了重写,并且在 JDK 13 中是默认使用新的 Socket 实现, 使其易于排查问题,同时也增加了可维护性。
动态 CDS 存档
JDK 13 中对 JDK 10 中引入的应用程序类数据共享(AppCDS)进行了进一步的简化、改进和扩展,即:允许在 Java 应用程序执行结束时动态进行类归档,具体能够被归档的类包括所有已被加载,但不属于默认基层 CDS 的应用程序类和引用类库中的类。这提高了应用程序类数据共享AppCDS的可用性。
增强 switch
增强版 switch 在 JDK 12 作为预览特性引入。在 JDK 14 之后,增强版 switch 语句块具备返回值
String result = switch (day) { case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" -> "工作日"; case "weekend" -> "周末"; default -> { // yield 关键字 yield的是JDK13后的一个新特性,它主要的作用是进行程序的局部返回 yield "unknown"; } }; System.out.println(result);
移除 CMS 垃圾收集器
移除了 CMS(Concurrent Mark Sweep) 垃圾收集器(功成而退)
JDK 15 ~ 16
文本块
JDK 15正式发布文本块功能, 用来解决多行文本的问题,文本块以三重双引号开头,并以同样的以三重双引号结尾终止示例:
String content = """ JDK 10 JDK 11 JDK 12 """; System.out.println(content);
输出:
JDK 10 JDK 11 JDK 12
Hidden Classes(隐藏类)
此功能可帮助需要在运行时生成类的框架。框架生成类需要动态扩展其行为,但是又希望限制对这些类的访问。隐藏类很有用,因为它们只能通过反射访问,而不能从普通字节码访问。此外,隐藏类可以独立于其他类加载,这可以减少框架的内存占用。
instanceof 模式匹配
之前使用instanceof 进行类型判断之后,需要进行对象类型转换后才能使用。而在 JDK 16 中,可以在判断类型时指定变量名称进行类型转换,方便了使用。
// JDK 16之前 if (obj instanceof String) { // 强制转换后使用 String str = (String)o; ... use str ... } // JDK 16 if (o instanceof String str) { // 直接使用str变量 ... use s ... }
记录类
record 是一种全新的类型,它本质上是一个 final 类,同时所有的属性都是 final 修饰,或者可看成是 Lombok 中 @Data 注解的一个 "低配" 替代。它会自动编译出 getXXX、toString、 hashcode 、equals等方法,减少了代码编写量。
// 定义记录类 public record Person(String name, Integer age) { } // ====== // 使用 Person person = new Person("张三", 16); System.out.println(person);
ZGC 并发线程堆栈处理
ZGC是JDK 11引入的新的垃圾收集器,JDK 15 正式发布成正式特性,ZGC是一个重新设计的并发的垃圾回收器,可以极大的提升GC的性能。支持任意堆大小而保持稳定的低延迟(10ms以内),性能非常可观。JDK 16将 ZGC 线程栈处理从安全点转移到一个并发阶段,甚至在大堆上也允许在毫秒内暂停 GC 安全点。消除 ZGC 垃圾收集器中最后一个延迟源可以极大地提高应用程序的性能和效率。
JDK 17
增强的伪随机数生成器
JDK 17 之前,我们可以借助 Random、ThreadLocalRandom和SplittableRandom来生成随机数。不过,这 3 个类都各有缺陷,且缺少常见的伪随机算法支持。JDK 17 为伪随机数生成器 (pseudorandom number generator,RPNG,又称为确定性随机位生成器)增加了新的接口类型和实现,使得开发者更容易在应用程序中互换使用各种 PRNG 算法。使用示例:
RandomGeneratorFactory<RandomGenerator> l128X256MixRandom = RandomGeneratorFactory.of("L128X256MixRandom"); // 使用时间戳作为随机数种子 RandomGenerator randomGenerator = l128X256MixRandom.create(System.currentTimeMillis()); // 生成随机数 randomGenerator.nextInt(10));
密封类
密封类可以是封闭类和或者封闭接口,用来增强 Java 编程语言,防止其他类或接口扩展或实现它们。这个特性由JDK 15的预览版本在JDK 17晋升为正式版本。密封类引入了sealedclass或interface,这些class或者interfaces只允许被指定的类或者interface进行扩展和实现。使用修饰符sealed,可以将一个类声明为密封类。密封的类使用关键字permits列出可以直接扩展它的类。子类可以是最终的,非密封的或密封的。
// 比如现在要限制 Person类 只能被这三个类继承,不能被其他类继承,可以这么做 // 添加sealed修饰符,permits后面跟上只能被继承的子类名称 public sealed class Person permits Teacher, Worker, Student{ } //人 // 子类可以被修饰为 final final class Teacher extends Person { }//教师 // 子类可以被修饰为 non-sealed,此时 Worker类就成了普通类,谁都可以继承它 non-sealed class Worker extends Person { } //工人 // 任何类都可以继承Worker class AnyClass extends Worker{}
移除实验性的 AOT 和 JIT 编译器
实验性的基于 Java 的提前 (AOT) 和即时 (JIT) 编译器是实验性功能,并未得到广泛采用。作为可选,它们已经从 JDK 16 中删除。这个 JEP 从 JDK 源代码中删除了这些组件。
删除 Applet API
Applet 是使用 Java 编写的可以嵌入到 HTML 中的小应用程序,嵌入方式是通过普通的 HTML 标记语法,由于早已过时,几乎没有场景在使用了。Applet API 在 JDK 9 时已经标记了废弃,现在 JDK 17 中彻底删除
AJDK11专有特性
AJDK是集团JVM 团队在 Java Standard Edition (SE) 规范下的 OpenJDK 基础上自主研发的 JDK 版本,开源版本是Alibaba Dragonwell
应用启动加速 EagerAppCDS
针对阿里的应用场景,对AppCDS进行优化, 缩短应用启动时间
代码预热技术JWarmup2
JWarmup是AJDK 8引入的一个功能,主要是解决线上应用代码预热的问题,JWarmup2在JWarmup1基础上开发,使用更便捷,性能更优。
AppAOT
AOT(ahead of time)是一项新的Java编译技术,不同于我们习惯的Jit(Just in time)编译,AOT在应用运行前就把一部分代码编译好,这样在启动的时候可以节省编译时间,减少CPU使用,加速启动。
QuickStart框架
QuickStart集成了AppCDS、EagerAppCDS、AOT等多种特性,旨在提升应用启动性能。
VectorAPI
Vector API是OpenJDK project Panama的一个重要组成部分,它的目标是让Java开发者更加自由的调用CPU强大的SIMD指令,让一条指令处理多条数据,从而获得成倍的性能提升。Vector API在大数据,AI计算和多媒体处理等。
为什么升级?
目前大多数应用用的还是JDK 8,不知道高版本的JDK能带来什么收益,所以没有动力去升级,我理解升级JDK版本带来的收益核心有三点:
- 性能收益,从已升级的应用的数据来看,性能收益还是不错的,后面详细介绍
- 新特性, 高版本有更多的语法和功能供我们使用
- 业界主流趋势 springboot 、netty、Kafka这些常用的框架在高版本里已不再支持低版本JDK(JDK8及以下)
如何选择版本?
首选 LTS版本, JDK 8以后LTS版本目前就两个,JDK 11和 JDK17, 对应集团内的版本就是AJDK 11和 AJDK 17, 直接从JDK 8 -> JDK 17, 风险较高,由于没有经过JDK 11过渡,可能会出现测试未覆盖代码在线上运行时直接异常退出情况。总的来看,目前选择JDK11是一个不错的选择,建议大家可以先升级JDK11, 如果有需求再升级JDK17, 如果有的同学对应用的代码和依赖完全可控, 可以直接升AJDK 17。另外附上New Relic 2022年3月发布的一份 Java 生态系统状况报告[1] 供参考,该报告基于从数百万个提供性能数据的匿名应用程序中收集的数据。报告显示,JDK 11 采用率已经超过 JDK 8,已经成为生产环境的最新标准。
AJDK11+G1升级收益
主要从自己实践的应用及集团内已升级的应用,升级前后的数据来看性能收益 。
体育营销中心
升级后进行压测,两个版本的压测数据如下:
GC平均次数 | GC平均耗时 | 接口平均RT | 接口最大RT | CPU利用率 | |
AJDK 8 + CMS | 28.63 | 1445.00ms | 44.19ms | 1787.42ms | 96.14% |
AJDK 11 + G1 | 14.75 | 160.25ms | 32.97ms | 1448.79ms | 92.72% |
总的来看,升级后GC次数下降48%, GC平均耗时下降88%,接口平均RT降低25%,CPU利用率下降3.5%从上面两张图可以看出,应对突发流量的情况JDK 11也优于JDK 8。
其它案例
SPECjbb2015 JDK8/11性能分析
SPECjbb2015是SPEC组织的一个用于评估服务器端Java应用性能的基准测试程序。
TPP
TPP升级后JDK版本后主要降低了P99指标。
店铺投放场景在应对突发流量的情况,升级JDK 11后超时Y率从6.58%降低至1.65%。
手猜工程场景,超时率从1.3%左右下降到了1.3‰左右,SLA从2个9变成了3个9,gc的次数跟gc耗时都有下降。
MTOP
MTOP 升级 JDK 11 + G1 GC 后,单机QPS 提升 11%, CPU 降低 2 pt, RT 降低 5%,YGC 平均次数 和 平均暂停时长均 降低 40-50% ,落到成本上可以缩减 MTOP 机器 10% 即 100 台左右 。
升级指南
常用软件升级
一些常用软件或框架的老版本不支持JDK11,需要进行升级。
常用的开发软件和插件的支持JDK11的最低版本:
- IntelliJ IDEA: 2018.2[2]
- Eclipse: Photon 4.9RC2 with Java 11 plugin[3]
- Maven: 3.5.0
- compiler plugin: 3.8.0
- surefire and failsafe: 2.22.0
- Gradle: 5.0[4]
常用框架支持JDK11的最低版本:
- ASM : 7.0
- Spring : 5.0
- Guice : 4.2
- guava : 19.0
本地环境升级
- 本地开发可以在这里[5]选择合适的版本进行下载 (AJDK11只支持Linux操作系统)
- 项目配置新版本JDK 11
Linux部署环境升级
- 修改 ${app}.release 文件 的 baseline.jdk
# 构建打包使用jdk版本, aone编译机器上未安装最新版本的ajdk, # 可以用已安装的ajdk版本进行编译,实际运行用最新版本的ajdk,最新版本的ajdk有较多新特性 baseline.jdk=ajdk11_11.0.14.13_fp2 # 升级maven版本,建议用amaven, amaven有代码编译加速的功能 build.tools.maven=amaven3.5.0
- dockerfile修改
# 安装ajdk11 最新版本的jdk可以在这里查看 http://yum.tbsite.net/taobao/7/x86_64/current/ajdk11/ rpm -ivh --nodeps "http://yum.tbsite.net/taobao/7/x86_64/current/ajdk11/ajdk11-11.0.16.15_fp1-20220929100209.alios7.x86_64.rpm" && \ # 把java目录软链到 /opt/taobao/java 目录,ajdk8自动连接,ajdk11 需要手动软链 rm -rf /opt/taobao/java && ln -s /opt/taobao/install/ajdk11-11.0.16.15_fp1 opt/taobao/java && \
- setenv.sh 脚本修改
# 去掉cms相关参数,删除下面两行 SERVICE_OPTS="${SERVICE_OPTS} -XX:+UseConcMarkSweepGC -XX:CMSMaxAbortablePrecleanTime=5000" SERVICE_OPTS="${SERVICE_OPTS} -XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly"
遇到的问题
aone编译机器上未安装相应的jdk
- aone发布编译报错
- 原因是目前ajdk 11 只在aone编译机器上安装了部分版本,未升级到最新版本
- 解决方案:
- 编译AJDK版本可以用aone编译机器上已安装的版本
- 申请请安装新版本的ajdk
应用启动报错 io.netty.util.internal.PlatformDependent0 - direct buffer constructor: unavailable
- 报错截图
- 解决方式:setenv.sh 脚本增加如下参数。
SERVICE_OPTS="${SERVICE_OPTS} -Dio.netty.tryReflectionSetAccessible=true --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED"
QuickStart框架集成
目前我只在预发环境测试了其加速效果,应用启动时间从 40s 左右 可以降低至25s左右。还未来得及在生产环境中实践,不过JVM团队大佬给出了几个在生产环境落地的方案,大家可以参考下,后续有实践后再更新 。
总结
本文主要讲了JDK高版本的一些新特性、升级JDK 11带来的收益以及升级步骤和升级常见问题。总体来说, JDK 11 升级带来的收益是大于升级成本的(特别老的应用另说),希望本文能够帮助大家对高版本的JDK有一个全面的了解,对于有升级需要的同学也能有所帮助。
参考链接:
[1]https://newrelic.com/resources/report/2022-state-of-java-ecosystem
[2]https://blog.jetbrains.com/idea/2018/06/java-11-in-intellij-idea-2018-2/
[3] https://blog.codefx.org/https://marketplace.eclipse.org/content/java-11-support-eclipse-photon-49
[4]https://docs.gradle.org/5.0/release-notes.html#java-11-runtime-support
[5]https://adoptium.net/zh-CN/marketplace/?version=11
作者 | 刘兵(多叶)
来源 | 阿里云开发者公众号