Java21已经于2023年9月19日发布,按照Oracle的规则,这是一个LTS(长期支持版),本篇学习并记录下该版本的新特性。

虚拟线程(Virtual Threads)JEP 444

原理:传统线程是“重量级”的(1:1 映射内核线程),而虚拟线程是“轻量级”的(M:N 调度)。它让阻塞式代码(如 JDBC 查询、HTTP 请求)能以异步非阻塞的效率运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package cn.net.dev;

import java.time.Duration;
import java.util.concurrent.Executors;

public class VirtualThreadDemo {

public static void main(String[] args) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("开始任务" + taskId + " | 线程:" + Thread.currentThread());
try {
Thread.sleep(Duration.ofSeconds(3));

} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("任务完成 " + taskId);
});
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行后输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
开始任务8 | 线程:VirtualThread[#39]/runnable@ForkJoinPool-1-worker-9
开始任务9 | 线程:VirtualThread[#40]/runnable@ForkJoinPool-1-worker-10
开始任务1 | 线程:VirtualThread[#32]/runnable@ForkJoinPool-1-worker-2
开始任务2 | 线程:VirtualThread[#33]/runnable@ForkJoinPool-1-worker-3
开始任务3 | 线程:VirtualThread[#34]/runnable@ForkJoinPool-1-worker-4
开始任务7 | 线程:VirtualThread[#38]/runnable@ForkJoinPool-1-worker-8
开始任务0 | 线程:VirtualThread[#30]/runnable@ForkJoinPool-1-worker-1
开始任务6 | 线程:VirtualThread[#37]/runnable@ForkJoinPool-1-worker-7
开始任务4 | 线程:VirtualThread[#35]/runnable@ForkJoinPool-1-worker-5
开始任务5 | 线程:VirtualThread[#36]/runnable@ForkJoinPool-1-worker-6
任务完成 6
任务完成 2
任务完成 3
任务完成 8
任务完成 5
任务完成 0
任务完成 9
任务完成 1
任务完成 4
任务完成 7

其优缺点分别如下:

优点:

  • 轻量级,一个线程是创建成百上千的虚拟线程也不会导致过多线程创建和上下文切换。
  • 异步编程, 可以简化异步编程,使得更易维护和理解。
  • 减少资源开销,JVM可以更加高效的利用底层资源,虚拟线程上下文切换比平台线程更加轻量,更好的支持高并发场景。

缺点:

  • 不适合计算密集型任务,更适合I/O密集型任务,始终需要CPU资源作为支持。
  • 兼容性差,并不是所有的第三方库都支持。

序列集合(Sequenced Collections)JEP431

序列集合(Sequenced Collections)在 core-libs/java.util:collections 包中。

该 JEP 提议引入“一个新的接口族,用于表示集合的概念,这些集合的元素按照预定义的序列或顺序排列,它们是作为集合的结构属性。”这一提案的动机是由于集合框架中缺乏预定义的顺序和统一的操作集。例如,在Java21之前,获取LinkedHashSet的最后一个元素非常麻烦,现在可以通过这个接口,所有有序集合的操作都变得一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cn.net.dev;

import java.util.LinkedHashSet;
import java.util.List;

public class SequencedCollectionDemo {

public static void main(String[] args) {
LinkedHashSet<String> list = new LinkedHashSet<>(List.of("One", "Two", "Three", "Four"));

// 获取首尾
System.out.println("First :" + list.getFirst());
System.out.println("Last :" + list.getLast());

// 首位添加
list.addFirst("Zero");
list.addLast("Five");

// 反转
System.out.println(list.reversed());
}
}

Record模式(Record Patterns)JEP440

使用记录模式(Record Patterns)增强Java编程语言,以解构记录值。可以嵌套记录模式和类型模式,以实现功能强大、声明性和可组合形式的数据导航和处理。 例如:允许在 instanceofswitch 中直接拆解 Record 内部的字段,省去了手动调用 getter 的步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.net.dev;

record Point(int x, int y) {
}

record Window(Point location, String tittle) {
}

public class RecordPatternDemo {
public static void main(String[] args) {
Object obj = new Window(new Point(10, 20), "Idea Window");
// 新特性
if (obj instanceof Window(Point(int x, int y), String tittle)) {
System.out.println("Window Tittle " + tittle + ", Point: x=" + x + ", y=" + y);
}
}
}

Switch模式匹配(Pattern Matching For Switch)JEP441

现在,switch可以支持类型匹配和when条件(Guards),并强制进行穷举检查(Exhaustiveness Check)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.net.dev;

public class SwitchPatternDemo {
public static void main(String[] args) {
Object[] obj = {100, "Hello", 0.5, null, true};
for (Object o : obj) {
String result = switch (o) {
case Integer i when i > 50 -> "大于50的整数: " + i;
case Integer i -> "整数: " + i;
case String s -> "字符串长度" + s.length();
case null -> "空对象";
default -> " 其他类型" + obj.toString();
};
System.out.println(result);
}
}
}

分代ZGC(Generational ZGC) JEP439

分代 ZGC(Generational ZGC)在 hotspot/gc 包中。

通过扩展Z垃圾回收器(ZGC)来维护年轻对象和年老对象的独立生成,从而提高应用程序性能。这将使ZGC能够更频繁地收集年轻对象——这些对象往往英年早逝。

例如:

1
java -XX:+UseZGC -XX:+ZGenerational -Xmx8g -jar app.jar

核心机制:读屏障(Load Barriers)显色指针(Colored Pointers),ZGC使用指针中的几个位(bit)来标记对象状态,分代后这些位被用来区分对象所属的分代,以及是否已经移动。

  • 年轻代回收(Minor GC): 非常频繁,只清理新创建的对象,速度极快。
  • 老年代回收(Major GC):频率较低,负责清理长期存活的对象。

分代ZGC不分代ZGC的区别:

特性 ZGC(不分代) - Java 17 分代ZGC - Java21
扫描范围 每次回收都会扫描全堆 有限并频繁扫描年轻代
内存效率 需要预留更多堆内存防止分配失败 内存利用率显著提高
吞吐量 较低 显著提升
停顿时间 <1ms <1ms
CPU消耗 较高,需要频繁全堆扫描 较低,聚焦于活跃对象

准备禁止动态加载代理 (Prepare to Disallow the Dynamic Loading of Agents) JEP451

在 Java 21 之前,一个运行中的 JVM 可以被外部进程通过 VirtualMachine.attach().loadAgent() 动态注入一个 Agent (代理)。虽然这给 BTrace 或 Arthas 等诊断工具带来了便利,但也留下了巨大的安全隐患:任何有权限的进程都可以修改正在运行的应用字节码。

Java 21 的变化:

  • 默认警告:当你尝试动态加载 Agent 时,JVM 会在标准错误流输出警告。
  • 静默加载(推荐):通过命令行参数 -javaagent-agentlib 在启动时指定的代理不会触发警告。
  • 未来趋势:未来的 Java 版本将默认禁止动态加载。

例如一个简单的程序尝试自我注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package cn.net.dev;

import com.sun.tools.attach.VirtualMachine;

import java.lang.management.ManagementFactory;

public class AgentWarningDemo {
public static void main(String[] args) throws Exception {
// 获取当前 JVM 的 PID
String pid = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
System.out.println("当前 JVM PID: " + pid);

try {
// 尝试动态附加到自身(这在 Java 21 中会触发警告)
VirtualMachine jvm = VirtualMachine.attach(pid);
System.out.println("成功附加到 JVM,准备加载 Agent (需提供实际 jar 路径)...");
// jvm.loadAgent("/path/to/your/agent.jar");
jvm.detach();
} catch (Exception e) {
System.err.println("附加失败: " + e.getMessage());
}
}
}

运行结果: 你会在控制台看到类似 WARNING: A {Type} agent has been loaded dynamically 的红色警告信息。

密钥封装机制(KEM)API JEP452

工作中很少遇到,简单描述下即可。

KEM (Key Encapsulation Mechanism) 是一种现代加密技术。

  • 传统方式:用公钥直接加密对称密钥(如 AES Key)。如果公钥加密算法(如 RSA)被攻破(如量子计算),密钥就泄露了。
  • KEM 方式:不直接加密,而是通过公钥生成一对:**加密后的密钥(封装)*和*共享的对称密钥。接收方用私钥解封。这种方式更安全,也是 后量子加密 (Post-Quantum Cryptography) 的标准流程。

Java 21 引入了 KEM 类,目前默认支持 DQC-Shoup 等算法(具体取决于安全提供者)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package cn.net.dev;

import javax.crypto.DecapsulateException;
import javax.crypto.KEM;
import javax.crypto.SecretKey;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;

public class KEMDemo {
public static void main(String[] args) throws NoSuchAlgorithmException, DecapsulateException, InvalidKeyException {
// 1. 生成密钥对 (以 RSA 为例,虽然 KEM 常用于更现代的算法)
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();

// 2. 发送方:封装 (Encapsulator)
KEM kemSender = KEM.getInstance("RSA-KEM"); // 需确保 Security Provider 支持
var encapsulator = kemSender.newEncapsulator(kp.getPublic());
var encapsulated = encapsulator.encapsulate();

// 这就是生成的对称密钥 (用于 AES 加密数据)
SecretKey sharedKeySender = encapsulated.key();
// 这是需要传给接收方的“封装包”
byte[] encapsulation = encapsulated.encapsulation();

System.out.println("发送方生成的对称密钥: " + bytesToHex(sharedKeySender.getEncoded()));

// 3. 接收方:解封 (Decapsulator)
KEM kemReceiver = KEM.getInstance("RSA-KEM");
var decapsulator = kemReceiver.newDecapsulator(kp.getPrivate());
SecretKey sharedKeyReceiver = decapsulator.decapsulate(encapsulation);

System.out.println("接收方解封后的对称密钥: " + bytesToHex(sharedKeyReceiver.getEncoded()));

// 验证两者是否一致
System.out.println("密钥是否一致: " + sharedKeySender.equals(sharedKeyReceiver));
}

private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) sb.append(String.format("%02x", b));
return sb.toString();
}
}

还有一些预览的,例如JEP 430:字符串模板JEP 442:外部函数和内存 APIJEP 443:未命名模式和变量JEP 445:未命名类和实例主方法JEP 446:作用域值JEP 453:结构化并发,等下一个LTS吧。