一、前言
前一阵Oracle刚刚发布了Java21,由于这是最新的LTS版本,引起了大家的关注。我也第一时间在个人项目中进行了升级体验,一探究竟,和大家分享。
二、Java21更新内容介绍
JEP 431:序列集合
JEP 439:分代 ZGC
JEP 440:记录模式
JEP 441:switch 模式匹配
JEP 444:虚拟线程
JEP 449:弃用 Windows 32 位 x86 移植
JEP 451:准备禁止动态加载代理
JEP 452:密钥封装机制 API
JEP 430:字符串模板(预览)
JEP 442:外部函数和内存 API(第三次预览)
JEP 443:未命名模式和变量(预览)
JEP 445:未命名类和实例主方法(预览)
JEP 446:作用域值(预览)
JEP 453:结构化并发(预览)
JEP 448:Vector API(孵化器第六阶段)
其中大家比较关注的是分代 ZGC和虚拟线程。
三、开箱
下载地址:
OpenJDK 版本:https://jdk.java.net/21/
Oracle 版本:https://www.oracle.com/java/technologies/downloads/
对比17
边框由不锈钢升级为钛金属,目录结构一致:
模块数量比17少一个:
整体大小从289MB增加到了320MB
四、升级体验
运行报错:
java.lang.NoSuchFieldError: Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field 'com.sun.tools.javac.tree.JCTree qualid'
解决办法:升级lombok至1.18.30
原因:https://github.com/projectlombok/lombok/issues/3393
由于我的项目以前用的JDK17,本次升级兼容性良好,只发现了一处:
系统托盘中 使用了PopupMenu,出现了字符集问题:
五、分代ZGC体验
ZGC在之前的JDK版本中也有,这次的分代ZGC更是被大家看好,官方的介绍如下:
Applications running with Generational ZGC should enjoy:
Lower risks of allocations stalls,
Lower required heap memory overhead, and
Lower garbage collection CPU overhead.
Enable Generational ZGC with command line options -XX:+UseZGC -XX:+ZGenerational
旨在通过为年轻对象和旧对象维护独立的代来改善应用程序性能。年轻对象往往很快就会死亡;维护独立的代将允许ZGC更频繁地收集年轻对象。运行在分代ZGC上的应用程序应会看到以下好处:分配中断风险更低,需要的堆内存开销更低,垃圾收集CPU开销更低。这些好处应该可以在吞吐量不明显下降的情况下实现。
参考:原文链接:https://blog.csdn.net/qq_35030548/article/details/133047541
性能测试参考:https://inside.java/2023/09/03/roadto21-performance/
JVM参数:-XX:+UseZGC -XX:+ZGenerational
使用Java21,未使用ZGC
MooInfo内存占用查看
MooInfo内存占用查看
以上只是初步体验,关于ZGC的更多内容,如详细的分代回收情况后续进一步探索。
以上内存占用查看使用我之前做的一个工具,MooInfo:
https://github.com/rememberber/MooInfo
六、虚拟线程探索
Virtual threads are lightweight threads that reduce the effort of writing, maintaining, and debugging high-throughput concurrent applications.
虚拟线程是轻量级线程,可以减少编写、维护和调试高吞吐量并发应用程序的工作量。
Oracle介绍原文:https://docs.oracle.com/en/java/javase/20/core/virtual-threads.html#GUID-DC4306FC-D6C1-4BCC-AECE-48C32C1A8DAA
Oracle官方文档的机器翻译:
平台线程是作为操作系统(OS)线程的瘦包装器实现的。
平台线程在其底层操作系统线程上运行Java代码,平台线程在平台线程的整个生命周期内捕获其操作系统线程。
因此,可用平台线程的数量受限于操作系统线程的数量。
平台线程通常有一个大的线程堆栈和其他由操作系统维护的资源。
平台线程支持线程局部变量。
平台线程适合运行所有类型的任务,但可能是有限的资源。
虚拟线程
Oracle官方文档的机器翻译:
与平台线程一样,虚拟线程也是 java.lang.Thread 的一个实例。
但是,虚拟线程并不依赖于特定的操作系统线程。
虚拟线程仍然在操作系统线程上运行代码。
但是,当虚拟线程中运行的代码调用阻塞 I/O 操作时,Java 运行时会挂起虚拟线程,直到可以恢复为止。
与挂起的虚拟线程关联的操作系统线程现在可以自由地为其他虚拟线程执行操作。
实现原理
虚拟线程的实现方式与虚拟内存类似。
为了模拟大量内存,操作系统将较大的虚拟地址空间映射到有限的 RAM。
同样,为了模拟大量线程,Java运行时将大量虚拟线程映射到少量操作系统线程。
与平台线程不同,虚拟线程通常具有浅调用堆栈,只执行单个 HTTP 客户端调用或单个 JDBC 查询。
尽管虚拟线程支持线程局部变量,但您应该仔细考虑使用它们,因为单个 JVM 可能支持数百万个虚拟线程。
虚拟线程适合运行大部分时间处于阻塞状态、通常等待 I/O 操作完成的任务。但是,它们不适用于长时间运行的 CPU 密集型操作。
Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
thread.join();
或者
try {
Thread.Builder builder = Thread.ofVirtual().name("MyThread");
Runnable task = () -> {
System.out.println("Running thread");
};
Thread t = builder.start(task);
System.out.println("Thread t name: " + t.getName());
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
或者
public class CreateNamedThreadsWithBuilders {
public static void main(String[] args) {
try {
Thread.Builder builder =
Thread.ofVirtual().name("worker-", 0);
Runnable task = () -> {
System.out.println("Thread ID: " +
Thread.currentThread().threadId());
};
// name "worker-0"
Thread t1 = builder.start(task);
t1.join();
System.out.println(t1.getName() + " terminated");
// name "worker-1"
Thread t2 = builder.start(task);
t2.join();
System.out.println(t2.getName() + " terminated");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
或者
try (ExecutorService myExecutor =
Executors.newVirtualThreadPerTaskExecutor()) {
Future> future =
myExecutor.submit(() -> System.out.println("Running thread"));
future.get();
System.out.println("Task completed");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
以上是Java20文档的用法,实际使用时我发现还可以这样:
Thread.startVirtualThread(() -> {
// do something
});
为了测试对比,我建了一个项目
初步对比,和官网描述一致,计算密集型场景差别不大,IO密集型场景有明显改善:
虚拟线程100个,IO读文件
平台线程100个,IO读文件
虚拟线程100个,Get请求百度首页
平台线程100个,Get请求百度首页
但是由于是本地测试,且用例比较简陋,无法完全得出准确结论。
日后大家有实际IO密集性多线程场景可以实际感受下。
开发人员通常会将应用程序代码从基于线程池的传统 ExecutorService 迁移到虚拟线程每任务 ExecutorService。
线程池和所有资源池一样,旨在共享昂贵的资源,
但虚拟线程并不昂贵,而且永远不需要将它们池化。
七、一颗语法糖?
Java21 新特性:Record Patterns
一个例子感受一下新特性:Record Patterns
before:
static void printSum(Object obj) {
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x+y);
}
}
after:
static void printSum(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println(x+y);
}
}
参考资料:
[1] https://my.oschina.net/didispace/blog/10112428
-end-