概要
本文以团队内部网关类应用(以下简称应用) 从 JDK 8 升级到 JDK 11 + G1 GC 的实践出发,梳理 JDK 11 升级(踩坑)指南、升级注意事项 以及 Java 9 - 19 的重要新特性:
- 升级收益:介绍应用升级获得的性能提升及成本收益,供大家参考。
- 升级指南:为大家实际动手升级时提供操作指南,避免重复踩坑。升级的主要流程和常见二方、三方依赖的升级 文章里基本都有提到。
- 新特性:简单介绍下 Java 9 - 19 的改进点和新增 API,主要介绍一些个人认为比较有用的新特性,看能否应用到实际开发。
- 为什么要升级▐ 性能提升
通过运行 SPECJbb2015 对比分析性能,整体而言 JDK11 优于 JDK8,G1 优于 CMS。在两个 JDK 版本默认状态下(JDK11 + G1 V.S JDK8 + CMS),JDK11 max-jOPS(纯吞吐量) 分数提升 17%,critical-jOPS(限制响应时间下的吞吐量) 分数提升 105%。
注:以上数据源自内部测试,非权威数据,仅供参考。
附:本应用升级效果及成本收益
应用升级 JDK 11 + G1 GC 后,单机性能在 极限 QPS、CPU、RT、GC 表现上均有提升,落到成本上可以进一步缩减机器上百台,每年可节省数十万成本。其中:
- 单机极限 QPS 提升 11% (1.8K -> 2.0K)
- CPU 降低 2 pt(55% -> 53%)
- RT 降低 5% (41ms -> 39ms)
- 日常水位 和 极限 QPS 下,GC 表现均有所提升
- YGC 平均次数 和 平均暂停时长均降低 40-50%
- 极限 QPS 下 吞吐量提升4.6pt:93.99% -> 98.65%
▐ 持续跟进 Java 新版本
目前,官方已经停止 Java 8 的公共更新。作为 Java 8 后的第一个 LTS,升级 Java 11 不仅能够避免因升级跨度过大带来的稳定性风险(比如 JDK 17 中无法使用 CMS、反射依赖 JDK 内部字段或方法的代码也可能存在不兼容),而且也能降低后续升级 Java 新版本的成本。
升级指南
▐ 升级准备
常用的开发软件支持 JDK11 的最低版本:
- IntelliJ IDEA: 2018.2(地址:https://blog.jetbrains.com/idea/2018/06/java-11-in-intellij-idea-2018-2/)
- Eclipse: Photon 4.9RC2 with Java 11 plugin(地址:https://nipafx.dev/)
- Maven: 3.5.0
-
compiler plugin: 3.8.0
surefire and failsafe: 2.22.0
▐ 基础环境升级
- JDK 升级,可以根据自己应用的部署模式升级
- https://openjdk.org/install/
- https://github.com/docker-library/openjdk/issues/272
- Tomcat 升级
- https://tomcat.apache.org/whichversion.html
▐ 依赖升级
- 依赖检查
在 JDK8 中 JavaSE 和 JavaEE 有很多共享代码,但是 JDK11 中这两部分独立了,JavaEE 相关模块被移除,无法编译,因此需要添加包括这些包的第三方依赖。如:编译时 @Resource/@PostConstruct等注解找不到,需要显式引入 javax.annotation
<dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>1.3.2</version> </dependency>
有一些类被移除或者变更,需要检查二方、三方依赖内是否有引用。比如 sun.misc.Cleaner 被移除。更多被移除模块参考:
- JDK11删除功能和选项-阿里云开发者社区(地址:https://developer.aliyun.com/article/652709)
- Java 9: Removed APIs, Features, and Options(地址:https://www.oracle.com/java/technologies/javase/9-removed-features.html)
- Maven 升级
- 升级 maven 至推荐版本 3.5.0 (release)
- 升级 maven-compiler-plugin 到 3.8.0 以上,同时指定编译的目标文件和源文件的编译版本
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>11</source> <target>11</target> </configuration></plugin>
- Spring 升级
由于 Spring 4.x 最多只支持到 JDK 8,因此若要升级 JDK 11,建议同时升级 Spring 至 5.x 版本。
附 Spring 各版本支持的 JDK 版本范围
Spring Framework 6.0.x: JDK 17-21 (expected)
Spring Framework 5.3.x: JDK 8-19 (expected)
Spring Framework 5.2.x: JDK 8-15
Spring Framework 5.1.x: JDK 8-12
Spring Framework 5.0.x: JDK 8-10
Spring Framework 4.3.x: JDK 6-8
升级到 Spring 5.x 的注意点:Upgrading-to-Spring-Framework-5.x(地址:https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-5.x)
废弃 ref local 标签
spring-beans-4.0.xsd ref/idref 标签不再支持 local 属性,需要替换为 bean 或指定低版本的 xsd(不推荐)
The local attribute on the ref/idref element is no longer supported in the 4.0 beans XSD, since it does not provide value over a regular bean reference any more. Change your existing ref local references to ref bean when upgrading to the 4.0 schema.
参考: 官方文档(地址:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-ref-element)
Spring 5.2.0 无法扫描非 Runtime 的注解
问题:线上压测时发现 某个二方库的本地缓存失效,导致下游依赖 QPS 大幅上涨。
原因:Spring 5.2.x 只能找到 @Retention(RetentionPolicy.RUNTIME) 的自定义注解,应用依赖的二方库中有非 RUNTIME 的注解,因此与 5.2.x 及以上版本不兼容。导致依赖注解扫描加载的富客户端本地缓存代理类 无法被加载,缓存失效,才会导致大量请求打到远端服务。
官方描述如下:
Spring's annotation retrieval algorithms have been completely revised for efficiency and consistency, as well as for potential optimizations through annotation presence hints (e.g. from a compile-time index). This may have side effects -- for example, finding annotations in places where they haven't been found before or not finding annotations anymore where they have previously been found accidentally. While we don't expect common Spring applications to be affected, annotation declaration accidents in application code may get uncovered when you upgrade to 5.2. For example, all annotations must now be annotated with @Retention(RetentionPolicy.RUNTIME) in order for Spring to find them.See gh-23901, gh-22886, and gh-22766.
解决:
- 推进二方包升级
- 若 1 短期内无法升级,可临时将 Spring 回退到 5.1.0,兼容二方库中非 Runtime 的自定义注解。
- Log4j 兼容/升级
Spring 4.2.1 起 废弃 Log4jConfigListener,支持Apache Log4j 2,官方表述如下:
Deprecated. as of Spring 4.2.1, in favor of Apache Log4j 2 (following Apache's EOL declaration for log4j 1.x) @Deprecatedpublic class Log4jConfigListenerextends java.lang.Objectimplements ServletContextListener
两种解决方案:
- 升级 Log4j2(推荐)
- 手动使 Spring 5.x 兼容 Log4j 1.x,建议仅在依赖二方包无法平滑迁移 Log4j 2 情况下使用(不推荐)
升级 Log4j 2.x
- Log4j 官方提供了 bridge 包进行平滑迁移,无需代码改动。但无法兼容编程配置方式、也无法支持访问 log4j 内部实现。
- slf4j 版本 和 log4j-slf4j-impl 版本需要同步
-
log4j-slf4j-impl should be used with SLF4J 1.7.x releases or older.
log4j-slf4j18-impl should be used with SLF4J 1.8.x releases or newer. ref
- log4j 2 不支持 Servlet 2.4 ref , 需要升级到 Servlet 3.0。
具体参考参考文档:官方升级文档(地址:https://logging.apache.org/log4j/2.x/manual/migration.html)
兼容 Log4j 1.x
若依赖的二方库使用 Log4j 1.x 导致应用无法通过官方适配包平滑迁移到 Log4j2,可以自行实现 Log4jConfigListener 初始化 log4j,使 Spring 5.x 兼容 Log4j1.x
验证日志是否正确输出时需要注意,相比于 Spring 4.x,Spring 5.x 部分日志级别有所调整。
Log4jConfigListener 实现参考:(见文末附录)
- 其他三方依赖
- ASM : 7.0
- Guice : 4.2
- guava : 19.0
- Lombok:1.18.x
- Netty:需要升级到 4.1.33.Final或之后的版本,否则会引起堆外内存增长。
- Apache Commons Lang3:3.9
注意:
- 任何操作字节码的依赖都需要关注是否需要升级,比如 ASM (7.0), Byte Buddy (1.9.0), cglib (3.2.8), or Javassist (3.23.1-GA)。
- 从 Java 9 开始,字节码级别每六个月增加一次,因此需要定期更新相关依赖,例如:任何使用在字节码上运行的东西,如 Spring (5.1)、Hibernate (unknown)、Mockito (2.20.0) 和许多其他项
▐ GC 升级
JDK11 在 GC 上有一些值得注意的变化,包括:
- 默认 GC 由 CMS 换成 G1
- 废弃了多种 GC 组合 和 GC 参数。
- 所有 GC 策略 GC log 打印出的文本格式发生了变化,和 JDK8 不兼容。
- GC 参数
GC log 参数改变JDK11中打印 GC log 的参数有所变化
- -Xloggc:<logfile> 改为-Xlog:gc:<logfile>
- JDK11 中不再支持 -XX:+PrintGCDetails,PrintGCDateStamps
-
若使用了 -XX:+PrintGCDetails,将-Xlog:gc:<logfile>改成-Xlog:gc*:<logfile>.
若使用 -XX:+PrintGCDateStamps,在-Xlog:gc:<logfile>后面添加:time,即修改为-Xlog:gc:<logfile>:time.
废弃 GC 参数具体废弃参数可参考:https://www.oracle.com/java/technologies/javase/9-removed-features.html
- GC 算法升级
G1 和 ZGC 的特点及适用场景见后文新特性中关于 GC 的介绍
配置调优参数
G1 配置和调优参数可参考:https://www.oracle.com/technical-resources/articles/java/g1gc.html
ZGC 配置参数
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
具体调优参数参考:https://docs.oracle.com/en/java/javase/11/gctuning/z-garbage-collector1.html#GUID-A5A42691-095E-47BA-B6DC-FB4E5FAA43D0
应用升级后单机 GC 性能压测表现
日常水位 GC 统计数据
JDK 8 + CMS |
JDK 11 + G1 |
|
YGC 平均次数 |
7 次/min |
4 次/min(↓43%) |
YGC 平均时长 |
48ms |
25ms (↓48%) |
吞吐率 |
99.44% |
99.83%(↑0.40%) |
FGC |
0 |
0 |
极限 QPS (1.8K) 统计数据
JDK 8 + CMS |
JDK 11 + G1 |
|
YGC 平均次数 |
79 次/min |
36 次/min(↓54%) |
YGC 平均时长 |
45.3 ms |
22.3 ms(↓51%) |
吞吐率 |
93.99% |
98.65%(↑5%) |
FGC 总次数 (压测过程中) |
2 |
0 |
JDK 11 + ZGC 压测时,在 400 QPS 时 RT 和 CPU 开始不正常飙高,经分析 ZGC 由于不分代,适合老年代对象较少的场景,而本应用有大量常驻内存的对象,所以不适合使用 ZGC。
▐ 升级注意事项
- 注意观察下游依赖监控指标。
- 注意观察日志,包括但不限于:中间件、Spring 启动日志;业务日志;GC 日志(格式及内容);如果有升级 日志框架,还需要看文件名格式、内容时间格式、日志滚动等是否一致。
- 升级 G1 GC 后,JVM 进程大小会有所增加(Remembered Sets、Collection Sets 占用),需要注意应用内存变化。
新特性
▐ Java 9
- 模块化
模块化提供了比 package 更高级别的聚合,将 Java 长期依赖 All-in-one/Environment 的结构,转变为以 Module 为基础的组件,帮助开发人员在构建、维护和发展软件系统时提高生产力。
- 更清晰的依赖关系和系统结构。
- 按需使用,提高性能和降低使用复杂度。
- 模块系统的一个关键动机是强封装(模块默认非公开),提高性能和安全性。
与 Maven 依赖管理的区别:
- Maven 访问是单向的,而 Java 9 的模块化特性是双向协议。模块默认非公开,因此需要显式 export 以被其他模块使用。使用某个模块时,也需要明确声明依赖该模块。
- Maven 管理的是整个 Jar 的依赖,关注整体。而模块化管理的是 Jar 中的模块需要对外暴露的内容和对外依赖的模块,关注细节。Maven 的依赖是将整个 Jar 都给你了,哪怕你仅仅只需要其中的一个类。模块化的依赖则是更细粒度的管理,你只能使用你依赖的模块下被暴露出来的 package。
- G1 成为默认垃圾收集器
Java 9 起,废弃 CMS 垃圾收集器,并在 JDK 17 中删除。G1 成为 默认垃圾收集器。
Java 语言提供了若干种 GC 来适应不同的需求。这些 GC 在吞吐率和响应时间两方面的表现有不同的特点:
- Parallel GC:吞吐率高,GC 暂停时间长;
- G1 GC,CMS GC:吞吐率和 GC 暂停时间都比较好,G1 GC 是 Java11 的默认 GC(目标 GC 暂停时间为 200ms),而 CMS GC 在 Java11 中已经不推荐使用,并且会在 Java14 中正式废弃。
- ZGC,Shenandaoh GC:GC 暂停时间短,吞吐率一般。
G1
G1是一个分代的,增量的,并行与并发的标记-复制垃圾回收器,为运行需要大堆和有限 GC 延迟的应用程序的用户提供一个解决方案。它以高概率满足垃圾收集 (GC) 暂停时间目标,同时实现高吞吐量。
特点G1 回收器和 CMS 比起来,有以下不同:
- 开创面向局部收集、化整为零的设计思路和基于 Region 的内存布局形式。G1 将 Region 作为单次回收的最小单元,优先处理回收价值收益最大的 Region ,有计划地避免在整个 Java 堆中进行垃圾收集。
- 提供可预测的停顿时间模型,即软实时(soft real-time)。能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。
- compacting 收集:由于都是以相等大小的分区为单位进行操作,因此 G1 天然适合压缩收集(局部压缩),通过将存活对象复制到另一个空闲分区中,消除潜在的碎片问题。适合短命对象多的场景。
- JVM 进程变大:比起 CMS,G1 为了完成 GC 而额外占用的空间有所增加,主要包括:
-
G1 用了两个 bitmap 用于标记存活对象,大小为 2 * heap size / 64。
Remembered Sets、Collection Sets、pointer queue 等数据结构的占用
除此之外,由于 old 区不连续的缘故,G1 看起来会比 CMS 占用更多物理内存,但随着时间推移,该部分差异会逐渐消除。
适用场景
- 大堆应用,实时对象占用了超过半数的堆空间;
- 期望消除耗时较长的GC或停顿(超过 0.5 - 1 s)
ZGC
ZGC 即 Z Garbage Collector(垃圾收集器或垃圾回收器),这应该是 Java 11 中最为瞩目的特性,没有之一。ZGC 是一个可伸缩的、低延迟的垃圾收集器。
特点
- 不分代,因此不适合常驻内存对象(老年代对象)较多的应用,本次升级的应用就属于此类情况。
- 主要通过并发转移降低暂停时间。并发转移通过 着色指针和读屏障 来保证转移过程中对象被应用线程访问的问题。
- ZGC 的 GC 触发机制有很大不同。ZGC 的核心特点是并发,GC 过程中一直有新的对象产生。如何保证在 GC 完成之前,新产生的对象不会将堆占满,是ZGC参数调优的第一大目标,也是最大可能瓶颈。
适用场景
- 对长尾请求 RT P99/P999 等指标有高要求的 Java 业务:这些业务通常要求
- 实时响应,对最慢的 1% 或 0.1% 的请求非常敏感;
- 长寿对象相对较少:Java11 的 ZGC 尚未分代,无法高效地处理此类对象。
- 机器的内存与 CPU 资源丰富:丰富的计算资源可以开启更大的堆和更多的并发 GC 线程;
- 可以容忍吞吐率降低:业务经过权衡后,认为 RT P99/P999 的指标比 QPS 指标更重要;
此外,Java 业务如果仍然运行在 Java 8 上,那么还需要考虑到切换到 Java 11 的代价。
参考资料:
- Getting Started with the G1 Garbage Collector(地址:https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html)
- 丝般顺滑!全新垃圾回收器 ZGC 初体验 | 龙蜥技术
- 不可变集合:List.of(), Set.of() And Map.of()
JDK 8 中,创建不可变集合方式有以下几种:
Set<String> set = new HashSet<>();set.add("a");set.add("b");set.add("c");set = Collections.unmodifiableSet(set); // 需要通过复制构造 HashSetList<String> stringList = Arrays.asList("a", "b", "c");Set<String> set = Collections.unmodifiableSet(new HashSet<String>(stringList)); // 创建匿名内部类实例,可能导致内存泄漏Set<String> set = Collections.unmodifiableSet(new HashSet<String>() {{ add("a"); add("b"); add("c");}}); // StreamSet<String> set = Collections.unmodifiableSet(Stream.of("a", "b", "c").collect(toSet())); // GuavaList<String> strings = ImmutableList.of("a", "b", "c");
JDK 9 提供了不可变集合的工厂方法:
Set<String> set = Set.of("a", "b", "c"); // 任意数量的元素List.of(E… elements)Set.of(E… elements) // 支持最多 10 对 k-v,超过 10 可以使用 ofEntries Map.of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10) // 支持任意数量 entry(Guava 最多支持创建 5 个键值对的 Map) Map.ofEntries(entry(k1, v1), entry(k2, v2), entry(k3, v3), ..., entry(kn, vn));
Java 9 「不可变集合」和由 Collections.unmodifiableXXX() 包装器方法返回的「不可修改集合」是不同的。
- 「不可修改集合」 只是原始集合的只读视图。我们仍旧可以对原始集合执行修改操作,这些修改将反映返回的不可修改集合中。
- Java 9 静态工厂方法返回的「不可变集合」是 100% 不可变的。
Java 9 不可变集合的特点:
- 不允许修改
- 不允许空元素
- 线程安全
- 非常节省空间,比可变集合消耗更少的内存。
使用场景:
集合中有很多已知数据,且在整个执行过程中永远不会更改并且这些值被频繁检索,那么可以考虑使用 Java 9 不可变集合。在这种情况下,不可变集合比可变集合提供更好的性能。
Java 11 中也新增创建不可变或不可修改集合的方法:
方法 |
解释 |
List.copyOf() | |
Set.copyOf() | |
Map.copyOf() | |
Collectors.toUnmodifiableList() | |
Collectors.toUnmodifiableSet() | |
Collectors.toUnmodifiableMap() |
- String 存储结构变更
Java 9 中 String 类通过 byte[] 存储字符串内容(之前是 char[]),原因是大多数 String 对象只包含 LATIN1 字符,此类字符只需要 1 字节的存储空间。
附 本应用升级前后 堆内存数据对比:
(GC 刚发生后的统计值) |
JDK 8 + CMS |
JDK 11 + G1 |
堆大小 |
4.08G |
3.83G(-256M, -6.13%) |
对象数量 |
106.6m |
106.8m |
String 数量 |
15,073,418 |
15,132,185 |
String Shallow Size |
345M |
346M |
String Retained Size |
>=1.26G |
>=961M(约 -329M) |
byte[] / char[] 数量 |
15,077,296 |
15,152,173 |
byte[] / char[] Shallow Size |
960M |
668M(约 -292M) |
byte[] / char[] Retained Size |
>=960M |
>=668M |
- Try-Recourse
Java 9 之前,try with resources 的资源需要在 try 块的 () 内声明,否则需要将外部声明的资源的引用分配给 try 块的局部变量,它不识别在其体外声明的资源。
public static void main(String[] args) throws FileNotFoundException { FileOutputStream fos = new FileOutputStream("Resource.txt"); //OR try(FileOutputStream fos = new FileOutputStream("Resource.txt")) try (FileOutputStream localFos = fos) { //Using the resources } catch (IOException e) { } //No need to close the resources explicitly. //Resources are implicitly closed. }
从 Java 9 开始,可以将外部声明的资源的引用直接传递给 try with resources 块。无需在 try 块中本地声明资源。但声明的资源引用必须是事实不可变。
public static void main(String[] args) throws FileNotFoundException { FileOutputStream fos = new FileOutputStream("Resource.txt"); // No need to declare resources locally // Variable used as a try-with-resources resource should be final or effectively final try (fos) { //Using the resources } catch (IOException e) { } //No need to close the resources explicitly. //Resources are implicitly closed }
- Stream API
Java 9 新增了一些 Stream API,如下:
方法 |
解释 |
takeWhile(Predicate<? super T> predicate) |
从流中的头开始取元素,直到不满足 Predicate 为止。 |
dropWhile(Predicate<? super T> predicate) |
从头开始删除满足条件的数据,直到遇见第一个不满足 Predicate 的位置,并保留剩余元素。 |
ofNullable(T t) |
ofNullable 允许创建 Stream 流时,只放入一个 null,返回的是一个空 Stream |
iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next) |
增加额外 Predicate 类型参数 hasNext 来决定何时终止操作。 |
Stream.iterate(1, i -> i <= 100000, i -> i*10).forEach(System.out::println);
- 接口支持私有方法
Java 8 中,接口支持 静态方法 和 default 方法。
Java 9 开始,接口支持 私有方法 和 私有静态方法,但仍旧不支持私有成员变量。
ref地址:https://javaconceptoftheday.com/java-9-interface-private-methods/
- <> 支持匿名内部类
Java 8 中,匿名内部类中的 <>必须要和前面的声明保持一致。
Java 9 中,可以省略不写。
// Before Java 9:'<>' cannot be used with anonymous classesAddition<Integer> integerAddition = new Addition<Integer>() { @Override void add(Integer t1, Integer t2) { System.out.println(t1+t2); }}; // Java 9Addition<Integer> integerAddition = new Addition<>() { @Override void add(Integer t1, Integer t2) { System.out.println(t1+t2); }};
- Optional 改进
Optional 类新增 API 如下:
方法 | 解释 | Java 版本 |
ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) | 可以分别指定 值存在和不存在 对应的操作 | Java 9 |
or(Supplier<? extends Optional<? extends T>> supplier) | 如果 Optional value 为空,使用 supplier 函数生成值。 | Java 9 |
stream() | 用于将 Optional 转换成流 | Java 9 |
orElseThrow() | 如果 value 为空,抛出默认异常 NoSuchElementException | Java 10 |
isEmpty() | 判断 Optional value 是否为空。 | Java 11 |
- 其他
提升内置锁竞争性能
提高竞争 Java Monitor 的性能。
- Field reordering and cache line alignment
- Speed up PlatformEvent::unpark()
- Fast Java monitor enter operations
- Fast Java monitor exit operations
- Fast Java monitor notify/notifyAll operations
HttpClient
HttpClient 在 Java 9 引入,Java 11 正式使用。替代仅适用于 bolocking 模式的 HTTPUrlConnection,并提供对 WebSocket 和 HTTP2 的支持。
▐ Java 10
局部变量类型推断
在 Java 10 可以使用 var 声明局部变量,省略变量具体类型,编译器可以根据变量初始化进行类型推断。
- var 仅在显式初始化变量时有效。
- var 仅适用于局部变量,不适用于全局变量。
- var 不能用作方法参数和返回类型。
- var 不适用于 lambda 表达式。
- var 声明的变量必须始终为同一类型。
- var 不能用于 null 。
在 Java 11 中,在 lambda 可以使用 var 类型参数,支持注解修饰,比如:
// Java 10,参数不可以用注解修饰。(m, n) -> m * n; // Java 11,参数可以用注解修饰。(@Nullable var m, @NonNull var n) -> m * n;
▐ Java 11
String 新增方法
方法 | 解释 |
isBlank() | 检查给定的字符串是否为空。 |
lines() | 返回从该字符串中提取的行 Stream,由换行符分隔。 |
repeat(int) | 返回一个字符串,其值为该字符串重复计数次数的串联。 |
String strip() | 返回 删除所有首尾空格 的字符串。 |
String stripLeading() | 返回 删除所有前导空格 的字符串。 |
String stripTrailing() | 返回 删除所有尾随空格 的字符串。 |
- String.trim() 判断是否是空格的标准是 小于或等于 U+0020。
- String.strip() 系列方法判断是否空格基于 Character.isWhiteSpace()。换句话说,它关注的是 Unicode whitespace 字符。
▐ After Java 11
- Java 15:Text Block
是 Java 在编译器层面实现的修改,Text Block 能够提高 Java 程序的可读性和可写性。本质上是一个二维的文本块,而不是一维的字符序列。
- 支持跨行字符串
- 避免常见转义的使用,如 "。
- 可自动格式化,支持自定义字符串格式、参数值替换等。
使用方式
- 可直接使用 ",不需要转义 \"
- 可直接换行,不需要 \n
- 可以使用 String::format 、 String::replace 、 String::formatted 进行参数值替换
- 其他方法:
-
String::stripIndent():用于从文本块内容中去除偶然空白
String::translateEscapes():用于翻译转义序列String::formatted(Object...args):简化文本块中的值替换
// before: 使用“一维”字符串String s = "function hello() {\n" + " print('\"Hello, world\"');\n" + "}\n" + "\n" + "hello();\n"; // after: 使用“二维”文本块String s = """ function hello() { print('"Hello, world"'); } hello(); """; // 3. 参数值替换示例String code = """ public void print($type o) { System.out.println(Objects.toString(o)); } """.replace("$type", type); String code = String.format(""" public void print(%s o) { System.out.println(Objects.toString(o)); } """, type); String source = """ public void print(%s object) { System.out.println(Objects.toString(object)); } """.formatted(type);
编译处理文本块的内容由 Java 编译器分三个不同的步骤处理,处理后仍是 String 实例。
- 转换行终止符:Text Block 内容中的行终止符被统一转换为 LF (\u000A)。目的是在跨平台移动 Java 源代码时遵循最小意外原则。
- 重新缩进:Text Block 为匹配 Java 源代码的缩进而引入空白符将会被删除。
- 转义处理:解释 Text Block 内容中的转义序列。这意味着开发人员可以编写转义序列,例如 \n,而不会被前续步骤修改或删除。
重新缩进算法( re-indentation algorithm):
- 删除所有行的公共空白前缀,开发人员可以通过结束"""的缩进来控制编译器的重新缩进。
- 删除所有行尾的空白符。
String html = """........ <html>........... <body>........... <p>Hello, world</p>........... </body>........... </html>..........."""; // 重新缩进后 | <html>|| <body>|| <p>Hello, world</p>|| </body>|| </html>| String html = """..............<html>................. <body>................. <p>Hello, world</p>................. </body>.................</html>................. """; // 重新缩进后 |<html>|| <body>|| <p>Hello, world</p>|| </body>||</html>|
转义处理
- Text Block 支持字符串中支持的所有转义序列,包括 \n、\t、\'、\" 和 \\。可以访问通过 String::translateEscapes 转义处理,这是一个新的实例方法。
- 可以直接使用 " 、 "",但使用连续三个 """ 需要对其中一个转义,即 ""\""\"",以区分 Text Block 结束符。
- 新增两个转义字符: \ 表示禁止编译器插入换行符\s 转为 单个空格 (\u0020),可以在行尾充当栅栏以防止编译器删除行尾空格。
// beforeString literal = "Lorem ipsum dolor sit amet, consectetur adipiscing " + "elit, sed do eiusmod tempor incididunt ut labore " + "et dolore magna aliqua."; // With the \<line-terminator> escape sequence this could be expressed as: String text = """ Lorem ipsum dolor sit amet, consectetur adipiscing \ elit, sed do eiusmod tempor incididunt ut labore \ et dolore magna aliqua.\ """; // Using \s at the end of each line guarantees each line is exactly six characters longString colors = """ red \s green\s blue \s """;
JEP 378: Text Blocks 地址:https://openjdk.org/jeps/378
- Java 16:Record 定义值对象类型
类似于 lombok 的 @Value,减少 getter/setter、equals、hashcode、默认构造器等样板代码
record Point(int x, int y) { } // 被编译为record Point(int x, int y) { // Implicitly declared fields private final int x; private final int y; // Other implicit declarations elided ... // Implicitly declared canonical constructor Point(int x, int y) { this.x = x; this.y = y; }}
JEP 395: Records地址:https://openjdk.org/jeps/395
- Java 16:Pattern matching for instanceof
instanceof 运算符支持模式匹配
A pattern is a combination of (1) a predicate, or test, that can be applied to a target, and (2) a set of local variables, known as pattern variables, that are extracted from the target only if the predicate successfully applies to it.
// beforeif (obj instanceof String) { String s = (String) obj; // grr... ...} // afterif (obj instanceof String s) { // Let pattern matching do the work! ...} // complex conditionif (obj instanceof String s && s.length() > 5) { flag = s.contains("jdk");}
JEP 394: Pattern Matching for instanceof地址:https://openjdk.org/jeps/394
- Java 17 - 20:Pattern Matching for switch
switch 表达式的模式匹配在 Java 17 中作为预览功能被提出,Java 20 中第四次改进。
- 支持模式匹配
- case 支持 when 子句
- case 支持匹配 null 和 常量
SwitchLabel: case CaseConstant { , CaseConstant } case null [, default] case Pattern default
record Point(int i, int j) {}enum Color { RED, GREEN, BLUE; } static void typeTester(Object obj) { switch (obj) { // null 匹配 case null -> System.out.println("null"); // when 子句 case String s when s.length() > 5 -> System.out.println("Long String"); // 模式匹配 case String s -> System.out.println("Short String"); case Color c -> System.out.println("Color: " + c.toString()); case Point p -> System.out.println("Record class: " + p.toString()); case int[] ia -> System.out.println("Array of ints of length" + ia.length); default -> System.out.println("Something else"); }}
JEP 433: Pattern Matching for switch (Fourth Preview)地址:https://openjdk.org/jeps/433
- Java 19:Virtual Thread
虚拟线程在 Java 19 中作为预览 API 被发布。旨在减少编写、维护和观测分析高吞吐量并发应用程序的工作量。
为什么需要虚拟线程?
Little Law 排队理论告诉我们:系统中的长期平均客户数量 L = 客户有效到达率 λ * 客户在系统中花费的时机 W,即 L = λW。因此,要想提高系统吞吐量:要么提高并发数,要么缩短处理时延。
Thread-per-request 传统风格
- 传统的 Java 线程与操作系统线程 1:1,而操作系统线程昂贵且有限,将应用程序的吞吐量限制在远低于硬件所能支持的水平。
- 即使将线程池化,也只能避免启动新线程的高成本,但不会增加线程总数。
反应式编程
开发者想要提高吞吐,就不得不实现更细粒度的线程共享。于是有了反应式编程。
- 必须采用新的编码风格,编写和理解成本高。(即将其请求处理逻辑分解为小阶段,通常编写为lambda 表达式)
- 异步程序在不同的线程中执行,难以追踪和调试。
虚拟线程
虚拟线程有助于在相同的硬件配置下实现与反应式编程相当的高可扩展性和吞吐量,而不会增加语法的复杂性。
- 保留 thread-per-request 风格,通过将大量虚拟线程映射到少量操作系统线程上,最佳化硬件利用率,实现高并发、高吞吐量、高可伸缩性。
- 使用 java.lang.Thread API 的现有代码能够以最小的更改采用虚拟线程,开发人员使用成本低。
使用虚拟线程 vs 平台线程
虚拟线程可以在以下场景下显着提高应用程序吞吐量
- 任务并发数高(几千以上):虚拟线程作为普通 Java 对象由 JVM 管理,因此我们可以在一个应用程序中创建数百万的虚拟线程,不受平台线程数量的限制。
- 工作负载不受 CPU 限制(非 CPU 密集型):虚拟线程只在 CPU 计算时占用平台线程,在等待或睡眠时不会阻塞操作系统线程。
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10_000).forEach(i -> { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return i; }); });} // executor.close() is called implicitly, and waits
比较平台线程和虚拟线程的性能为了比较两者性能,我们简单通过 sleep 模拟任务阻塞执行的过程,并分别使用虚拟线程和平台线程来执行大量任务(示例中是 10000 个),通过控制 任务执行时间 和 平台线程个数 两个变量来对比两者性能。代码如下:
Instant start = Instant.now();//使用虚拟线程的代码:try (var executor = Executors.newVirtualThreadPerTaskExecutor())try (var executor = Executors.newFixedThreadPool(100)) { IntStream.range(0, 10_000).forEach(i -> { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(sleepSeconds)); return i; }); });}Instant finish = Instant.now();long timeElapsed = Duration.between(start, finish).toMillis();System.out.println("Total elapsed time : " + timeElapsed);
执行耗时如下表格,可以看到:
- 使用平台线程执行任务,程序可扩展性有限,无法通过提高并发数(线程数)来无限扩大吞吐量。
- 而虚拟线程几乎能够同时执行 10000 个任务。
(本机简单模拟测试,非权威数据) |
|||
模拟任务执行时间(Sleep Seconds) | 虚拟线程执行时间(毫秒) | 平台线程个数 | 平台线程执行时间(毫秒) |
100 |
100531 |
||
1000 |
10497 |
||
2000 |
6035 |
||
3000 |
6338 |
||
4000 |
7324 |
||
5000 |
OOM |
||
100 |
1000555 |
||
1000 |
100502 |
||
2000 |
51018 |
||
3000 |
41127 |
||
4000 |
31172 |
||
5000 |
OOM |
在任务时长 1s 的情况下,使用虚拟线程执行任务,并继续扩大任务数量,耗时情况如下表格。
(本机简单模拟测试,非权威数据) |
|
任务数量 |
虚拟线程执行时间(毫秒) |
10_000 |
1255 |
20_000 |
1319 |
40_000 |
1433 |
80_000 |
(2000, 4000) |
100_000 |
(3000, 6000) |
可以看到在同时开启 4 万个虚拟线程的情况下,耗时还是较为稳定的。由此看来,虚拟线程的性能还是很值得我们期待的。
最佳实践
- 不要池化虚拟线程,虚拟线程并不是昂贵资源,应该为每个应用程序任务创建一个新的虚拟线程。
- 尽量避免在虚拟线程中使用 ThreadLocal。由于虚拟线程可以创建数百万个,所以使用 ThreadLocal 要慎重。
- 使用 ReentrantLock 而不是同步块。执行同步块或本地方法时,虚拟线程会阻塞平台线程。
JEP 425: Virtual Threads (Preview)地址:https://openjdk.org/jeps/425
附录
Log4jConfigListener 实现参考:
import java.io.FileNotFoundException;import java.net.URL; import javax.servlet.ServletContext;import javax.servlet.ServletContextEvent;import javax.servlet.ServletContextListener; import org.apache.log4j.LogManager;import org.apache.log4j.PropertyConfigurator;import org.apache.log4j.xml.DOMConfigurator;import org.springframework.util.ResourceUtils;import org.springframework.util.SystemPropertyUtils;import org.springframework.web.util.WebUtils; /** * copy from Spring 2.0.7, reserve necessary method and args. * * Bootstrap listener for custom Log4J initialization in a web environment. * Delegates to Log4jWebConfigurer (see its javadoc for configuration details). * * <b>WARNING: Assumes an expanded WAR file</b>, both for loading the configuration * file and for writing the log files. If you want to keep your WAR unexpanded or * don't need application-specific log files within the WAR directory, don't use * Log4J setup within the application (thus, don't use Log4jConfigListener or * Log4jConfigServlet). Instead, use a global, VM-wide Log4J setup (for example, * in JBoss) or JDK 1.4's <code>java.util.logging</code> (which is global too). * * <p>This listener should be registered before ContextLoaderListener in web.xml, * when using custom Log4J initialization. * * <p>For Servlet 2.2 containers and Servlet 2.3 ones that do not * initalize listeners before servlets, use Log4jConfigServlet. * See the ContextLoaderServlet javadoc for details. * * @author Juergen Hoeller * @since 13.03.2003 */public class Log4jConfigListener implements ServletContextListener { /** * Parameter specifying the location of the Log4J config file */ public static final String CONFIG_LOCATION_PARAM = "log4jConfigLocation"; /** * Extension that indicates a Log4J XML config file: ".xml" */ public static final String XML_FILE_EXTENSION = ".xml"; public void contextInitialized(ServletContextEvent event) { initLogging(event.getServletContext()); } public void contextDestroyed(ServletContextEvent event) { shutdownLogging(event.getServletContext()); } /** * Initialize Log4J, including setting the web app root system property. * * @param servletContext the current ServletContext * @see WebUtils#setWebAppRootSystemProperty */ public static void initLogging(ServletContext servletContext) { // Only perform custom Log4J initialization in case of a config file. String location = servletContext.getInitParameter(CONFIG_LOCATION_PARAM); if (location != null) { // Perform actual Log4J initialization; else rely on Log4J's default initialization. try { // Return a URL (e.g. "classpath:" or "file:") as-is; // consider a plain file path as relative to the web application root directory. if (!ResourceUtils.isUrl(location)) { // Resolve system property placeholders before resolving real path. location = SystemPropertyUtils.resolvePlaceholders(location); location = WebUtils.getRealPath(servletContext, location); } // Write log message to server log. servletContext.log("Initializing Log4J from [" + location + "]"); // Initialize without refresh check, i.e. without Log4J's watchdog thread. initLogging(location); } catch (FileNotFoundException ex) { throw new IllegalArgumentException("Invalid 'log4jConfigLocation' parameter: " + ex.getMessage()); } } } /** * Shut down Log4J, properly releasing all file locks * and resetting the web app root system property. * * @param servletContext the current ServletContext */ public static void shutdownLogging(ServletContext servletContext) { servletContext.log("Shutting down Log4J"); LogManager.shutdown(); } /** * Initialize Log4J from the given file location, with no config file refreshing. * Assumes an XML file in case of a ".xml" file extension, and a properties file else. * * @param location the location of the config file: either a "classpath:" location * (e.g. "classpath:myLog4j.properties"), an absolute file URL * (e.g. "file:C:/log4j.properties), or a plain absolute path in the file system * (e.g. "C:/log4j.properties") * @throws FileNotFoundException if the location specifies an invalid file path */ public static void initLogging(String location) throws FileNotFoundException { String resolvedLocation = SystemPropertyUtils.resolvePlaceholders(location); URL url = ResourceUtils.getURL(resolvedLocation); if (resolvedLocation.toLowerCase().endsWith(XML_FILE_EXTENSION)) { DOMConfigurator.configure(url); } else { PropertyConfigurator.configure(url); } } }