mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-07-28 12:22:17 +08:00
Compare commits
16 Commits
d243984c51
...
74170ea66d
Author | SHA1 | Date | |
---|---|---|---|
|
74170ea66d | ||
|
79bc813937 | ||
|
88aa050fa8 | ||
|
1c404b8340 | ||
|
b0cfa505a3 | ||
|
3cd90b7c5b | ||
|
534fde27f7 | ||
|
7cf404ba59 | ||
|
74fc413bcc | ||
|
2c6ed751db | ||
|
b6d1a8cb2e | ||
|
b5469fc505 | ||
|
afcf129b93 | ||
|
4e431621a5 | ||
|
d011ff6161 | ||
|
0d308e7eb2 |
@ -125,6 +125,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
|
||||
- [Java 19 新特性概览](./docs/java/new-features/java19.md)
|
||||
- [Java 20 新特性概览](./docs/java/new-features/java20.md)
|
||||
- [Java 21 新特性概览](./docs/java/new-features/java21.md)
|
||||
- [Java 22 & 23 新特性概览](./docs/java/new-features/java22-23.md)
|
||||
|
||||
## 计算机基础
|
||||
|
||||
|
@ -168,6 +168,7 @@ export default sidebar({
|
||||
"java19",
|
||||
"java20",
|
||||
"java21",
|
||||
"java22-23",
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -215,7 +215,7 @@ HTTP/2.0 多路复用效果图(图源: [HTTP/2 For Web Developers](https://b
|
||||
- **队头阻塞**:HTTP/2.0 多请求复用一个 TCP 连接,一旦发生丢包,就会阻塞住所有的 HTTP 请求。由于 QUIC 协议的特性,HTTP/3.0 在一定程度上解决了队头阻塞(Head-of-Line blocking, 简写:HOL blocking)问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。
|
||||
- **连接迁移**:HTTP/3.0 支持连接迁移,因为 QUIC 使用 64 位 ID 标识连接,只要 ID 不变就不会中断,网络环境改变时(如从 Wi-Fi 切换到移动数据)也能保持连接。而 TCP 连接是由(源 IP,源端口,目的 IP,目的端口)组成,这个四元组中一旦有一项值发生改变,这个连接也就不能用了。
|
||||
- **错误恢复**:HTTP/3.0 具有更好的错误恢复机制,当出现丢包、延迟等网络问题时,可以更快地进行恢复和重传。而 HTTP/2.0 则需要依赖于 TCP 的错误恢复机制。
|
||||
- **安全性**:在 HTTP/2.0 中,TLS 用于加密和认证整个 HTTP 会话,包括所有的 HTTP 头部和数据负载。TLS的工作是在 TCP 层之上,它加密的是在 TCP 连接中传输的应用层的数据,并不会对 TCP 头部以及 TLS 记录层头部进行加密,所以在传输的过程中 TCP 头部可能会被攻击者篡改来干扰通信。而 HTTP/3.0 的 QUIC 对整个数据包(包括报文头和报文体)进行了加密与认证处理,保障安全性。
|
||||
- **安全性**:在 HTTP/2.0 中,TLS 用于加密和认证整个 HTTP 会话,包括所有的 HTTP 头部和数据负载。TLS 的工作是在 TCP 层之上,它加密的是在 TCP 连接中传输的应用层的数据,并不会对 TCP 头部以及 TLS 记录层头部进行加密,所以在传输的过程中 TCP 头部可能会被攻击者篡改来干扰通信。而 HTTP/3.0 的 QUIC 对整个数据包(包括报文头和报文体)进行了加密与认证处理,保障安全性。
|
||||
|
||||
HTTP/1.0、HTTP/2.0 和 HTTP/3.0 的协议栈比较:
|
||||
|
||||
|
@ -508,7 +508,7 @@ MySQL 可以简单分为 Server 层和存储引擎层这两层。Server 层处
|
||||
|
||||
MySQL 5.7 可以通过查询 `sys` 库的 `schema_unused_indexes` 视图来查询哪些索引从未被使用。
|
||||
|
||||
### 知道如何分析语句是否走索引查询
|
||||
### 知道如何分析 SQL 语句是否走索引查询
|
||||
|
||||
我们可以使用 `EXPLAIN` 命令来分析 SQL 的 **执行计划** ,这样就知道语句是否命中索引了。执行计划是指一条 SQL 语句在经过 MySQL 查询优化器的优化会后,具体的执行方式。
|
||||
|
||||
|
@ -780,10 +780,15 @@ dynamic-hz yes
|
||||
|
||||
### 大量 key 集中过期怎么办?
|
||||
|
||||
如果存在大量 key 集中过期的问题,可能会使 Redis 的请求延迟变高。可以采用下面的可选方案来应对:
|
||||
当 Redis 中存在大量 key 在同一时间点集中过期时,可能会导致以下问题:
|
||||
|
||||
1. 尽量避免 key 集中过期,在设置键的过期时间时尽量随机一点。
|
||||
2. 对过期的 key 开启 lazyfree 机制(修改 `redis.conf` 中的 `lazyfree-lazy-expire`参数即可),这样会在后台异步删除过期的 key,不会阻塞主线程的运行。
|
||||
- **请求延迟增加:** Redis 在处理过期 key 时需要消耗 CPU 资源,如果过期 key 数量庞大,会导致 Redis 实例的 CPU 占用率升高,进而影响其他请求的处理速度,造成延迟增加。
|
||||
- **内存占用过高:** 过期的 key 虽然已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间。如果过期 key 没有及时清理,可能会导致内存占用过高,甚至引发内存溢出。
|
||||
|
||||
为了避免这些问题,可以采取以下方案:
|
||||
|
||||
1. **尽量避免 key 集中过期**: 在设置键的过期时间时尽量随机一点。
|
||||
2. **开启 lazy free 机制**: 修改 `redis.conf` 配置文件,将 `lazyfree-lazy-expire` 参数设置为 `yes`,即可开启 lazy free 机制。开启 lazy free 机制后,Redis 会在后台异步删除过期的 key,不会阻塞主线程的运行,从而降低对 Redis 性能的影响。
|
||||
|
||||
### Redis 内存淘汰策略了解么?
|
||||
|
||||
|
@ -109,6 +109,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
|
||||
- [Java 19 新特性概览](./java/new-features/java19.md)
|
||||
- [Java 20 新特性概览](./java/new-features/java20.md)
|
||||
- [Java 21 新特性概览](./java/new-features/java21.md)
|
||||
- [Java 22 & 23 新特性概览](./java/new-features/java22-23.md)
|
||||
|
||||
## 计算机基础
|
||||
|
||||
|
@ -694,21 +694,26 @@ System.out.println(s);
|
||||
**字符串常量池** 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
|
||||
|
||||
```java
|
||||
// 在堆中创建字符串对象”ab“
|
||||
// 将字符串对象”ab“的引用保存在字符串常量池中
|
||||
// 在字符串常量池中创建字符串对象 ”ab“
|
||||
// 将字符串对象 ”ab“ 的引用赋值给 aa
|
||||
String aa = "ab";
|
||||
// 直接返回字符串常量池中字符串对象”ab“的引用
|
||||
// 直接返回字符串常量池中字符串对象 ”ab“,赋值给引用 bb
|
||||
String bb = "ab";
|
||||
System.out.println(aa==bb);// true
|
||||
System.out.println(aa==bb); // true
|
||||
```
|
||||
|
||||
更多关于字符串常量池的介绍可以看一下 [Java 内存区域详解](https://javaguide.cn/java/jvm/memory-area.html) 这篇文章。
|
||||
|
||||
### String s1 = new String("abc");这句话创建了几个字符串对象?
|
||||
|
||||
会创建 1 或 2 个字符串对象。
|
||||
先说答案:会创建 1 或 2 个字符串对象。
|
||||
|
||||
1、如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。
|
||||
1. 字符串常量池中不存在 "abc":会创建 2 个 字符串对象。一个在字符串常量池中,由 `ldc` 指令触发创建。一个在堆中,由 `new String()` 创建,并使用常量池中的 "abc" 进行初始化。
|
||||
2. 字符串常量池中已存在 "abc":会创建 1 个 字符串对象。该对象在堆中,由 `new String()` 创建,并使用常量池中的 "abc" 进行初始化。
|
||||
|
||||
下面开始详细分析。
|
||||
|
||||
1、如果字符串常量池中不存在字符串对象 “abc”,那么它首先会在字符串常量池中创建字符串对象 "abc",然后在堆内存中再创建其中一个字符串对象 "abc"。
|
||||
|
||||
示例代码(JDK 1.8):
|
||||
|
||||
@ -718,16 +723,40 @@ String s1 = new String("abc");
|
||||
|
||||
对应的字节码:
|
||||
|
||||

|
||||
```java
|
||||
// 在堆内存中分配一个尚未初始化的 String 对象。
|
||||
// #2 是常量池中的一个符号引用,指向 java/lang/String 类。
|
||||
// 在类加载的解析阶段,这个符号引用会被解析成直接引用,即指向实际的 java/lang/String 类。
|
||||
0 new #2 <java/lang/String>
|
||||
// 复制栈顶的 String 对象引用,为后续的构造函数调用做准备。
|
||||
// 此时操作数栈中有两个相同的对象引用:一个用于传递给构造函数,另一个用于保持对新对象的引用,后续将其存储到局部变量表。
|
||||
3 dup
|
||||
// JVM 先检查字符串常量池中是否存在 "abc"。
|
||||
// 如果常量池中已存在 "abc",则直接返回该字符串的引用;
|
||||
// 如果常量池中不存在 "abc",则 JVM 会在常量池中创建该字符串字面量并返回它的引用。
|
||||
// 这个引用被压入操作数栈,用作构造函数的参数。
|
||||
4 ldc #3 <abc>
|
||||
// 调用构造方法,使用从常量池中加载的 "abc" 初始化堆中的 String 对象
|
||||
// 新的 String 对象将包含与常量池中的 "abc" 相同的内容,但它是一个独立的对象,存储于堆中。
|
||||
6 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
|
||||
// 将堆中的 String 对象引用存储到局部变量表
|
||||
9 astore_1
|
||||
// 返回,结束方法
|
||||
10 return
|
||||
```
|
||||
|
||||
`ldc` 命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。
|
||||
`ldc (load constant)` 指令的确是从常量池中加载各种类型的常量,包括字符串常量、整数常量、浮点数常量,甚至类引用等。对于字符串常量,`ldc` 指令的行为如下:
|
||||
|
||||
2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。
|
||||
1. **从常量池加载字符串**:`ldc` 首先检查字符串常量池中是否已经有内容相同的字符串对象。
|
||||
2. **复用已有字符串对象**:如果字符串常量池中已经存在内容相同的字符串对象,`ldc` 会将该对象的引用加载到操作数栈上。
|
||||
3. **没有则创建新对象并加入常量池**:如果字符串常量池中没有相同内容的字符串对象,JVM 会在常量池中创建一个新的字符串对象,并将其引用加载到操作数栈中。
|
||||
|
||||
2、如果字符串常量池中已存在字符串对象“abc”,则只会在堆中创建 1 个字符串对象“abc”。
|
||||
|
||||
示例代码(JDK 1.8):
|
||||
|
||||
```java
|
||||
// 字符串常量池中已存在字符串对象“abc”的引用
|
||||
// 字符串常量池中已存在字符串对象“abc”
|
||||
String s1 = "abc";
|
||||
// 下面这段代码只会在堆中创建 1 个字符串对象“abc”
|
||||
String s2 = new String("abc");
|
||||
@ -735,35 +764,48 @@ String s2 = new String("abc");
|
||||
|
||||
对应的字节码:
|
||||
|
||||

|
||||
```java
|
||||
0 ldc #2 <abc>
|
||||
2 astore_1
|
||||
3 new #3 <java/lang/String>
|
||||
6 dup
|
||||
7 ldc #2 <abc>
|
||||
9 invokespecial #4 <java/lang/String.<init> : (Ljava/lang/String;)V>
|
||||
12 astore_2
|
||||
13 return
|
||||
```
|
||||
|
||||
这里就不对上面的字节码进行详细注释了,7 这个位置的 `ldc` 命令不会在堆中创建新的字符串对象“abc”,这是因为 0 这个位置已经执行了一次 `ldc` 命令,已经在堆中创建过一次字符串对象“abc”了。7 这个位置执行 `ldc` 命令会直接返回字符串常量池中字符串对象“abc”对应的引用。
|
||||
|
||||
### String#intern 方法有什么作用?
|
||||
|
||||
`String.intern()` 是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中,可以简单分为两种情况:
|
||||
`String.intern()` 是一个 `native` (本地) 方法,用来处理字符串常量池中的字符串对象引用。它的工作流程可以概括为以下两种情况:
|
||||
|
||||
- 如果字符串常量池中保存了对应的字符串对象的引用,就直接返回该引用。
|
||||
- 如果字符串常量池中没有保存了对应的字符串对象的引用,那就在常量池中创建一个指向该字符串对象的引用并返回。
|
||||
1. **常量池中已有相同内容的字符串对象**:如果字符串常量池中已经有一个与调用 `intern()` 方法的字符串内容相同的 `String` 对象,`intern()` 方法会直接返回常量池中该对象的引用。
|
||||
2. **常量池中没有相同内容的字符串对象**:如果字符串常量池中还没有一个与调用 `intern()` 方法的字符串内容相同的对象,`intern()` 方法会将当前字符串对象的引用添加到字符串常量池中,并返回该引用。
|
||||
|
||||
总结:
|
||||
|
||||
- `intern()` 方法的主要作用是确保字符串引用在常量池中的唯一性。
|
||||
- 当调用 `intern()` 时,如果常量池中已经存在相同内容的字符串,则返回常量池中已有对象的引用;否则,将该字符串添加到常量池并返回其引用。
|
||||
|
||||
示例代码(JDK 1.8) :
|
||||
|
||||
```java
|
||||
// 在堆中创建字符串对象”Java“
|
||||
// 将字符串对象”Java“的引用保存在字符串常量池中
|
||||
// s1 指向字符串常量池中的 "Java" 对象
|
||||
String s1 = "Java";
|
||||
// 直接返回字符串常量池中字符串对象”Java“对应的引用
|
||||
// s2 也指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象
|
||||
String s2 = s1.intern();
|
||||
// 会在堆中在单独创建一个字符串对象
|
||||
// 在堆中创建一个新的 "Java" 对象,s3 指向它
|
||||
String s3 = new String("Java");
|
||||
// 直接返回字符串常量池中字符串对象”Java“对应的引用
|
||||
// s4 指向字符串常量池中的 "Java" 对象,和 s1 是同一个对象
|
||||
String s4 = s3.intern();
|
||||
// s1 和 s2 指向的是堆中的同一个对象
|
||||
// s1 和 s2 指向的是同一个常量池中的对象
|
||||
System.out.println(s1 == s2); // true
|
||||
// s3 和 s4 指向的是堆中不同的对象
|
||||
// s3 指向堆中的对象,s4 指向常量池中的对象,所以不同
|
||||
System.out.println(s3 == s4); // false
|
||||
// s1 和 s4 指向的是堆中的同一个对象
|
||||
System.out.println(s1 == s4); //true
|
||||
// s1 和 s4 都指向常量池中的同一个对象
|
||||
System.out.println(s1 == s4); // true
|
||||
```
|
||||
|
||||
### String 类型的变量和常量做“+”运算时发生了什么?
|
||||
@ -844,6 +886,7 @@ public static String getStr() {
|
||||
## 参考
|
||||
|
||||
- 深入解析 String#intern:<https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html>
|
||||
- Java String 源码解读:<http://keaper.cn/2020/09/08/java-string-mian-mian-guan/>
|
||||
- R 大(RednaxelaFX)关于常量折叠的回答:<https://www.zhihu.com/question/55976094/answer/147302764>
|
||||
|
||||
<!-- @include: @article-footer.snippet.md -->
|
||||
|
@ -55,8 +55,8 @@ head:
|
||||
|
||||
### Throwable 类常用方法有哪些?
|
||||
|
||||
- `String getMessage()`: 返回异常发生时的简要描述
|
||||
- `String toString()`: 返回异常发生时的详细信息
|
||||
- `String getMessage()`: 返回异常发生时的详细信息
|
||||
- `String toString()`: 返回异常发生时的简要描述
|
||||
- `String getLocalizedMessage()`: 返回异常对象的本地化信息。使用 `Throwable` 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 `getMessage()`返回的结果相同
|
||||
- `void printStackTrace()`: 在控制台上打印 `Throwable` 对象封装的异常信息
|
||||
|
||||
@ -451,8 +451,8 @@ SPI 将服务接口和具体的服务实现分离开来,将服务调用方和
|
||||
|
||||
简单来说:
|
||||
|
||||
- **序列化**:将数据结构或对象转换成二进制字节流的过程
|
||||
- **反序列化**:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
|
||||
- **序列化**:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
|
||||
- **反序列化**:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程
|
||||
|
||||
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
|
||||
|
||||
|
@ -11,8 +11,8 @@ tag:
|
||||
|
||||
简单来说:
|
||||
|
||||
- **序列化**:将数据结构或对象转换成二进制字节流的过程
|
||||
- **反序列化**:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
|
||||
- **序列化**:将数据结构或对象转换成可以存储或传输的形式,通常是二进制字节流,也可以是 JSON, XML 等文本格式
|
||||
- **反序列化**:将在序列化过程中所生成的数据转换为原始数据结构或者对象的过程
|
||||
|
||||
对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。
|
||||
|
||||
|
@ -454,7 +454,7 @@ void afterNodeInsertion(boolean evict) { // possibly remove eldest
|
||||
|
||||
## LinkedHashMap 和 HashMap 遍历性能比较
|
||||
|
||||
`LinkedHashMap` 维护了一个双向链表来记录数据插入的顺序,因此在迭代遍历生成的迭代器的时候,是按照双向链表的路径进行遍历的。这一点相比于 `HashMap` 那种遍历整个 bucket 的方式来说,高效需多。
|
||||
`LinkedHashMap` 维护了一个双向链表来记录数据插入的顺序,因此在迭代遍历生成的迭代器的时候,是按照双向链表的路径进行遍历的。这一点相比于 `HashMap` 那种遍历整个 bucket 的方式来说,高效许多。
|
||||
|
||||
这一点我们可以从两者的迭代器中得以印证,先来看看 `HashMap` 的迭代器,可以看到 `HashMap` 迭代键值对时会用到一个 `nextNode` 方法,该方法会返回 next 指向的下一个元素,并会从 next 开始遍历 bucket 找到下一个 bucket 中不为空的元素 Node。
|
||||
|
||||
@ -484,7 +484,7 @@ void afterNodeInsertion(boolean evict) { // possibly remove eldest
|
||||
}
|
||||
```
|
||||
|
||||
相比之下 `LinkedHashMap` 的迭代器则是直接使用通过 `after` 指针快速定位到当前节点的后继节点,简洁高效需多。
|
||||
相比之下 `LinkedHashMap` 的迭代器则是直接使用通过 `after` 指针快速定位到当前节点的后继节点,简洁高效许多。
|
||||
|
||||
```java
|
||||
final class LinkedEntryIterator extends LinkedHashIterator
|
||||
@ -550,7 +550,7 @@ System.out.println("linkedHashMap get time: " + (end - start));
|
||||
System.out.println(num);
|
||||
```
|
||||
|
||||
从输出结果来看,因为 `LinkedHashMap` 需要维护双向链表的缘故,插入元素相较于 `HashMap` 会更耗时,但是有了双向链表明确的前后节点关系,迭代效率相对于前者高效了需多。不过,总体来说却别不大,毕竟数据量这么庞大。
|
||||
从输出结果来看,因为 `LinkedHashMap` 需要维护双向链表的缘故,插入元素相较于 `HashMap` 会更耗时,但是有了双向链表明确的前后节点关系,迭代效率相对于前者高效了许多。不过,总体来说却别不大,毕竟数据量这么庞大。
|
||||
|
||||
```bash
|
||||
map time putVal: 5880
|
||||
|
@ -653,7 +653,15 @@ abc
|
||||
|
||||
我们上面的代码示例中,为了方便,都没有选择自定义线程池。实际项目中,这是不可取的。
|
||||
|
||||
`CompletableFuture` 默认使用`ForkJoinPool.commonPool()` 作为执行器,这个线程池是全局共享的,可能会被其他任务占用,导致性能下降或者饥饿。因此,建议使用自定义的线程池来执行 `CompletableFuture` 的异步任务,可以提高并发度和灵活性。
|
||||
`CompletableFuture` 默认使用全局共享的 `ForkJoinPool.commonPool()` 作为执行器,所有未指定执行器的异步任务都会使用该线程池。这意味着应用程序、多个库或框架(如 Spring、第三方库)若都依赖 `CompletableFuture`,默认情况下它们都会共享同一个线程池。
|
||||
|
||||
虽然 `ForkJoinPool` 效率很高,但当同时提交大量任务时,可能会导致资源竞争和线程饥饿,进而影响系统性能。
|
||||
|
||||
为避免这些问题,建议为 `CompletableFuture` 提供自定义线程池,带来以下优势:
|
||||
|
||||
- **隔离性**:为不同任务分配独立的线程池,避免全局线程池资源争夺。
|
||||
- **资源控制**:根据任务特性调整线程池大小和队列类型,优化性能表现。
|
||||
- **异常处理**:通过自定义 `ThreadFactory` 更好地处理线程中的异常情况。
|
||||
|
||||
```java
|
||||
private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
|
||||
|
@ -549,7 +549,7 @@ public class SynchronizedDemo2 {
|
||||
|
||||

|
||||
|
||||
`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
|
||||
`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取而代之的是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
|
||||
|
||||
如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。
|
||||
|
||||
@ -557,7 +557,7 @@ public class SynchronizedDemo2 {
|
||||
|
||||
`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。
|
||||
|
||||
`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。
|
||||
`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取而代之的是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。
|
||||
|
||||
**不过两者的本质都是对对象监视器 monitor 的获取。**
|
||||
|
||||
|
@ -93,7 +93,7 @@ Thread Name= 9 formatter = yy-M-d ah:mm
|
||||
|
||||
从输出中可以看出,虽然 `Thread-0` 已经改变了 `formatter` 的值,但 `Thread-1` 默认格式化值与初始化值相同,其他线程也一样。
|
||||
|
||||
上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA 会提示你转换为 Java8 的格式(IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法`withInitial()`,将 Supplier 功能接口作为参数。
|
||||
上面用于创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA 会提示你转换为 Java8 的格式(IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法`withInitial()`,将 Supplier 功能接口作为参数。
|
||||
|
||||
```java
|
||||
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
|
||||
|
@ -353,7 +353,7 @@ JDK1.2 以后,Java 对引用的概念进行了扩充,将引用分为强引
|
||||
|
||||
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
|
||||
|
||||
比如在新生代中,每次收集都会有大量对象死去,所以可以选择“标记-复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
|
||||
比如在新生代中,每次收集都会有大量对象死去,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
|
||||
|
||||
**延伸面试问题:** HotSpot 为什么要分为新生代和老年代?
|
||||
|
||||
|
@ -242,12 +242,12 @@ Class 文件中除了有类的版本、字段、方法、接口等描述信息
|
||||
**字符串常量池** 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
|
||||
|
||||
```java
|
||||
// 在堆中创建字符串对象”ab“
|
||||
// 将字符串对象”ab“的引用保存在字符串常量池中
|
||||
// 在字符串常量池中创建字符串对象 ”ab“
|
||||
// 将字符串对象 ”ab“ 的引用赋值给给 aa
|
||||
String aa = "ab";
|
||||
// 直接返回字符串常量池中字符串对象”ab“的引用
|
||||
// 直接返回字符串常量池中字符串对象 ”ab“,赋值给引用 bb
|
||||
String bb = "ab";
|
||||
System.out.println(aa==bb);// true
|
||||
System.out.println(aa==bb); // true
|
||||
```
|
||||
|
||||
HotSpot 虚拟机中字符串常量池的实现是 `src/hotspot/share/classfile/stringTable.cpp` ,`StringTable` 可以简单理解为一个固定大小的`HashTable` ,容量为 `StringTableSize`(可以通过 `-XX:StringTableSize` 参数来设置),保存的是字符串(key)和 字符串对象的引用(value)的映射关系,字符串对象的引用指向堆中的字符串对象。
|
||||
|
@ -36,7 +36,7 @@ Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行
|
||||
|
||||
Foreign Function & Memory API (FFM API) 定义了类和接口:
|
||||
|
||||
- 分配外部内存:`MemorySegment`、、`MemoryAddress`和`SegmentAllocator`);
|
||||
- 分配外部内存:`MemorySegment`、`MemoryAddress`和`SegmentAllocator`;
|
||||
- 操作和访问结构化的外部内存:`MemoryLayout`, `VarHandle`;
|
||||
- 控制外部内存的分配和释放:`MemorySession`;
|
||||
- 调用外部函数:`Linker`、`FunctionDescriptor`和`SymbolLookup`。
|
||||
|
@ -297,7 +297,7 @@ static String formatterPatternSwitch(Object obj) {
|
||||
}
|
||||
```
|
||||
|
||||
## JEP 442: 外部函数和内存 API(第三次预览)
|
||||
## JEP 442:外部函数和内存 API(第三次预览)
|
||||
|
||||
Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。
|
||||
|
||||
|
427
docs/java/new-features/java22-23.md
Normal file
427
docs/java/new-features/java22-23.md
Normal file
@ -0,0 +1,427 @@
|
||||
---
|
||||
title: Java 22 & 23 新特性概览
|
||||
category: Java
|
||||
tag:
|
||||
- Java新特性
|
||||
---
|
||||
|
||||
JDK 23 和 JDK 22 一样,这也是一个非 LTS(长期支持)版本,Oracle 仅提供六个月的支持。下一个长期支持版是 JDK 25,预计明年 9 月份发布。
|
||||
|
||||

|
||||
|
||||
由于 JDK 22 和 JDK 23 重合的新特性较多,这里主要以 JDK 23 为主介绍,会补充 JDK 22 独有的一些特性。
|
||||
|
||||
JDK 23 一共有 12 个新特性:
|
||||
|
||||
- [JEP 455: 模式中的原始类型、instanceof 和 switch(预览)](https://openjdk.org/jeps/455)
|
||||
- [JEP 456: 类文件 API(第二次预览)](https://openjdk.org/jeps/466)
|
||||
- [JEP 467:Markdown 文档注释](https://openjdk.org/jeps/467)
|
||||
- [JEP 469:向量 API(第八次孵化)](https://openjdk.org/jeps/469)
|
||||
- [JEP 473:流收集器(第二次预览)](https://openjdk.org/jeps/473)
|
||||
- [JEP 471:弃用 sun.misc.Unsafe 中的内存访问方法](https://openjdk.org/jeps/471)
|
||||
- [JEP 474:ZGC:默认的分代模式](https://openjdk.org/jeps/474)
|
||||
- [JEP 476:模块导入声明 (预览)](https://openjdk.org/jeps/476)
|
||||
- [JEP 477:未命名类和实例 main 方法 (第三次预览)](https://openjdk.org/jeps/477)
|
||||
- [JEP 480:结构化并发 (第三次预览)](https://openjdk.org/jeps/480)
|
||||
- [JEP 481: 作用域值 (第三次预览)](https://openjdk.org/jeps/481)
|
||||
- [JEP 482:灵活的构造函数体(第二次预览)](https://openjdk.org/jeps/482)
|
||||
|
||||
JDK 22 的新特性如下:
|
||||
|
||||

|
||||
|
||||
其中,下面这 3 条新特性我会单独拎出来详细介绍一下:
|
||||
|
||||
- [JEP 423:G1 垃圾收集器区域固定](https://openjdk.org/jeps/423)
|
||||
- [JEP 454:外部函数与内存 API](https://openjdk.org/jeps/454)
|
||||
- [JEP 456:未命名模式和变量](https://openjdk.org/jeps/456)
|
||||
- [JEP 458:启动多文件源代码程序](https://openjdk.org/jeps/458)
|
||||
|
||||
## JDK 23
|
||||
|
||||
### JEP 455: 模式中的原始类型、instanceof 和 switch(预览)
|
||||
|
||||
在 JEP 455 之前, `instanceof` 只支持引用类型,`switch` 表达式和语句的 `case` 标签只能使用整数字面量、枚举常量和字符串字面量。
|
||||
|
||||
JEP 455 的预览特性中,`instanceof` 和 `switch` 全面支持所有原始类型,包括 `byte`, `short`, `char`, `int`, `long`, `float`, `double`, `boolean`。
|
||||
|
||||
```java
|
||||
// 传统写法
|
||||
if (i >= -128 && i <= 127) {
|
||||
byte b = (byte)i;
|
||||
... b ...
|
||||
}
|
||||
|
||||
// 使用 instanceof 改进
|
||||
if (i instanceof byte b) {
|
||||
... b ...
|
||||
}
|
||||
|
||||
long v = ...;
|
||||
// 传统写法
|
||||
if (v == 1L) {
|
||||
// ...
|
||||
} else if (v == 2L) {
|
||||
// ...
|
||||
} else if (v == 10_000_000_000L) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 使用 long 类型的 case 标签
|
||||
switch (v) {
|
||||
case 1L:
|
||||
// ...
|
||||
break;
|
||||
case 2L:
|
||||
// ...
|
||||
break;
|
||||
case 10_000_000_000L:
|
||||
// ...
|
||||
break;
|
||||
default:
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### JEP 456: 类文件 API(第二次预览)
|
||||
|
||||
类文件 API 在 JDK 22 进行了第一次预览,由 [JEP 457](https://openjdk.org/jeps/457) 提出。
|
||||
|
||||
类文件 API 的目标是提供一套标准化的 API,用于解析、生成和转换 Java 类文件,取代过去对第三方库(如 ASM)在类文件处理上的依赖。
|
||||
|
||||
```java
|
||||
// 创建一个 ClassFile 对象,这是操作类文件的入口。
|
||||
ClassFile cf = ClassFile.of();
|
||||
// 解析字节数组为 ClassModel
|
||||
ClassModel classModel = cf.parse(bytes);
|
||||
|
||||
// 构建新的类文件,移除以 "debug" 开头的所有方法
|
||||
byte[] newBytes = cf.build(classModel.thisClass().asSymbol(),
|
||||
classBuilder -> {
|
||||
// 遍历所有类元素
|
||||
for (ClassElement ce : classModel) {
|
||||
// 判断是否为方法 且 方法名以 "debug" 开头
|
||||
if (!(ce instanceof MethodModel mm
|
||||
&& mm.methodName().stringValue().startsWith("debug"))) {
|
||||
// 添加到新的类文件中
|
||||
classBuilder.with(ce);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### JEP 467:Markdown 文档注释
|
||||
|
||||
在 JavaDoc 文档注释中可以使用 Markdown 语法,取代原本只能使用 HTML 和 JavaDoc 标签的方式。
|
||||
|
||||
Markdown 更简洁易读,减少了手动编写 HTML 的繁琐,同时保留了对 HTML 元素和 JavaDoc 标签的支持。这个增强旨在让 API 文档注释的编写和阅读变得更加轻松,同时不会影响现有注释的解释。Markdown 提供了对常见文档元素(如段落、列表、链接等)的简化表达方式,提升了文档注释的可维护性和开发者体验。
|
||||
|
||||

|
||||
|
||||
### JEP 469:向量 API(第八次孵化)
|
||||
|
||||
向量计算由对向量的一系列操作组成。向量 API 用来表达向量计算,该计算可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。
|
||||
|
||||
向量 API 的目标是为用户提供简洁易用且与平台无关的表达范围广泛的向量计算。
|
||||
|
||||
这是对数组元素的简单标量计算:
|
||||
|
||||
```java
|
||||
void scalarComputation(float[] a, float[] b, float[] c) {
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这是使用 Vector API 进行的等效向量计算:
|
||||
|
||||
```java
|
||||
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;
|
||||
|
||||
void vectorComputation(float[] a, float[] b, float[] c) {
|
||||
int i = 0;
|
||||
int upperBound = SPECIES.loopBound(a.length);
|
||||
for (; i < upperBound; i += SPECIES.length()) {
|
||||
// FloatVector va, vb, vc;
|
||||
var va = FloatVector.fromArray(SPECIES, a, i);
|
||||
var vb = FloatVector.fromArray(SPECIES, b, i);
|
||||
var vc = va.mul(va)
|
||||
.add(vb.mul(vb))
|
||||
.neg();
|
||||
vc.intoArray(c, i);
|
||||
}
|
||||
for (; i < a.length; i++) {
|
||||
c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JEP 473:流收集器(第二次预览)
|
||||
|
||||
流收集器在 JDK 22 进行了第一次预览,由 [JEP 461](https://openjdk.org/jeps/457) 提出。
|
||||
|
||||
这个改进使得 Stream API 可以支持自定义中间操作。
|
||||
|
||||
```java
|
||||
source.gather(a).gather(b).gather(c).collect(...)
|
||||
```
|
||||
|
||||
### JEP 471:弃用 sun.misc.Unsafe 中的内存访问方法
|
||||
|
||||
JEP 471 提议弃用 `sun.misc.Unsafe` 中的内存访问方法,这些方法将来的版本中会被移除。
|
||||
|
||||
这些不安全的方法已有安全高效的替代方案:
|
||||
|
||||
- `java.lang.invoke.VarHandle` :JDK 9 (JEP 193) 中引入,提供了一种安全有效地操作堆内存的方法,包括对象的字段、类的静态字段以及数组元素。
|
||||
- `java.lang.foreign.MemorySegment` :JDK 22 (JEP 454) 中引入,提供了一种安全有效地访问堆外内存的方法,有时会与 `VarHandle` 协同工作。
|
||||
|
||||
这两个类是 Foreign Function & Memory API(外部函数和内存 API) 的核心组件,分别用于管理和操作堆外内存。Foreign Function & Memory API 在 JDK 22 中正式转正,成为标准特性。
|
||||
|
||||
```java
|
||||
import jdk.incubator.foreign.*;
|
||||
import java.lang.invoke.VarHandle;
|
||||
|
||||
// 管理堆外整数数组的类
|
||||
class OffHeapIntBuffer {
|
||||
|
||||
// 用于访问整数元素的VarHandle
|
||||
private static final VarHandle ELEM_VH = ValueLayout.JAVA_INT.arrayElementVarHandle();
|
||||
|
||||
// 内存管理器
|
||||
private final Arena arena;
|
||||
|
||||
// 堆外内存段
|
||||
private final MemorySegment buffer;
|
||||
|
||||
// 构造函数,分配指定数量的整数空间
|
||||
public OffHeapIntBuffer(long size) {
|
||||
this.arena = Arena.ofShared();
|
||||
this.buffer = arena.allocate(ValueLayout.JAVA_INT, size);
|
||||
}
|
||||
|
||||
// 释放内存
|
||||
public void deallocate() {
|
||||
arena.close();
|
||||
}
|
||||
|
||||
// 以volatile方式设置指定索引的值
|
||||
public void setVolatile(long index, int value) {
|
||||
ELEM_VH.setVolatile(buffer, 0L, index, value);
|
||||
}
|
||||
|
||||
// 初始化指定范围的元素为0
|
||||
public void initialize(long start, long n) {
|
||||
buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
|
||||
ValueLayout.JAVA_INT.byteSize() * n)
|
||||
.fill((byte) 0);
|
||||
}
|
||||
|
||||
// 将指定范围的元素复制到新数组
|
||||
public int[] copyToNewArray(long start, int n) {
|
||||
return buffer.asSlice(ValueLayout.JAVA_INT.byteSize() * start,
|
||||
ValueLayout.JAVA_INT.byteSize() * n)
|
||||
.toArray(ValueLayout.JAVA_INT);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JEP 474:ZGC:默认的分代模式
|
||||
|
||||
Z 垃圾回收器 (ZGC) 的默认模式切换为分代模式,并弃用非分代模式,计划在未来版本中移除。这是因为分代 ZGC 是大多数场景下的更优选择。
|
||||
|
||||
### JEP 476:模块导入声明 (预览)
|
||||
|
||||
模块导入声明允许在 Java 代码中简洁地导入整个模块的所有导出包,而无需逐个声明包的导入。这一特性简化了模块化库的重用,特别是在使用多个模块时,避免了大量的包导入声明,使得开发者可以更方便地访问第三方库和 Java 基本类。
|
||||
|
||||
此特性对初学者和原型开发尤为有用,因为它无需开发者将自己的代码模块化,同时保留了对传统导入方式的兼容性,提升了开发效率和代码可读性。
|
||||
|
||||
```java
|
||||
// 导入整个 java.base 模块,开发者可以直接访问 List、Map、Stream 等类,而无需每次手动导入相关包
|
||||
import module java.base;
|
||||
|
||||
public class Example {
|
||||
public static void main(String[] args) {
|
||||
String[] fruits = { "apple", "berry", "citrus" };
|
||||
Map<String, String> fruitMap = Stream.of(fruits)
|
||||
.collect(Collectors.toMap(
|
||||
s -> s.toUpperCase().substring(0, 1),
|
||||
Function.identity()));
|
||||
|
||||
System.out.println(fruitMap);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### JEP 477:未命名类和实例 main 方法 (第三次预览)
|
||||
|
||||
这个特性主要简化了 `main` 方法的的声明。对于 Java 初学者来说,这个 `main` 方法的声明引入了太多的 Java 语法概念,不利于初学者快速上手。
|
||||
|
||||
没有使用该特性之前定义一个 `main` 方法:
|
||||
|
||||
```java
|
||||
public class HelloWorld {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Hello, World!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
使用该新特性之后定义一个 `main` 方法:
|
||||
|
||||
```java
|
||||
class HelloWorld {
|
||||
void main() {
|
||||
System.out.println("Hello, World!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
进一步简化(未命名的类允许我们省略类名)
|
||||
|
||||
```java
|
||||
void main() {
|
||||
System.out.println("Hello, World!");
|
||||
}
|
||||
```
|
||||
|
||||
### JEP 480:结构化并发 (第三次预览)
|
||||
|
||||
Java 19 引入了结构化并发,一种多线程编程方法,目的是为了通过结构化并发 API 来简化多线程编程,并不是为了取代`java.util.concurrent`,目前处于孵化器阶段。
|
||||
|
||||
结构化并发将不同线程中运行的多个任务视为单个工作单元,从而简化错误处理、提高可靠性并增强可观察性。也就是说,结构化并发保留了单线程代码的可读性、可维护性和可观察性。
|
||||
|
||||
结构化并发的基本 API 是[`StructuredTaskScope`](https://download.java.net/java/early_access/loom/docs/api/jdk.incubator.concurrent/jdk/incubator/concurrent/StructuredTaskScope.html)。`StructuredTaskScope` 支持将任务拆分为多个并发子任务,在它们自己的线程中执行,并且子任务必须在主任务继续之前完成。
|
||||
|
||||
`StructuredTaskScope` 的基本用法如下:
|
||||
|
||||
```java
|
||||
try (var scope = new StructuredTaskScope<Object>()) {
|
||||
// 使用fork方法派生线程来执行子任务
|
||||
Future<Integer> future1 = scope.fork(task1);
|
||||
Future<String> future2 = scope.fork(task2);
|
||||
// 等待线程完成
|
||||
scope.join();
|
||||
// 结果的处理可能包括处理或重新抛出异常
|
||||
... process results/exceptions ...
|
||||
} // close
|
||||
```
|
||||
|
||||
结构化并发非常适合虚拟线程,虚拟线程是 JDK 实现的轻量级线程。许多虚拟线程共享同一个操作系统线程,从而允许非常多的虚拟线程。
|
||||
|
||||
### JEP 481:作用域值 (第三次预览)
|
||||
|
||||
作用域值(Scoped Values)可以在线程内和线程间共享不可变的数据,优于线程局部变量,尤其是在使用大量虚拟线程时。
|
||||
|
||||
```java
|
||||
final static ScopedValue<...> V = new ScopedValue<>();
|
||||
|
||||
// In some method
|
||||
ScopedValue.where(V, <value>)
|
||||
.run(() -> { ... V.get() ... call methods ... });
|
||||
|
||||
// In a method called directly or indirectly from the lambda expression
|
||||
... V.get() ...
|
||||
```
|
||||
|
||||
作用域值允许在大型程序中的组件之间安全有效地共享数据,而无需求助于方法参数。
|
||||
|
||||
### JEP 482:灵活的构造函数体(第二次预览)
|
||||
|
||||
这个特性最初在 JDK 22 由 [JEP 447: Statements before super(...) (Preview)](https://openjdk.org/jeps/447)提出。
|
||||
|
||||
Java 要求在构造函数中,`super(...)` 或 `this(...)` 调用必须作为第一条语句出现。这意味着我们无法在调用父类构造函数之前在子类构造函数中直接初始化字段。
|
||||
|
||||
灵活的构造函数体解决了这一问题,它允许在构造函数体内,在调用 `super(..)` 或 `this(..)` 之前编写语句,这些语句可以初始化字段,但不能引用正在构造的实例。这样可以防止在父类构造函数中调用子类方法时,子类的字段未被正确初始化,增强了类构造的可靠性。
|
||||
|
||||
这一特性解决了之前 Java 语法限制了构造函数代码组织的问题,让开发者能够更自由、更自然地表达构造函数的行为,例如在构造函数中直接进行参数验证、准备和共享,而无需依赖辅助方法或构造函数,提高了代码的可读性和可维护性。
|
||||
|
||||
```java
|
||||
class Person {
|
||||
private final String name;
|
||||
private int age;
|
||||
|
||||
public Person(String name, int age) {
|
||||
if (age < 0) {
|
||||
throw new IllegalArgumentException("Age cannot be negative.");
|
||||
}
|
||||
this.name = name; // 在调用父类构造函数之前初始化字段
|
||||
this.age = age;
|
||||
// ... 其他初始化代码
|
||||
}
|
||||
}
|
||||
|
||||
class Employee extends Person {
|
||||
private final int employeeId;
|
||||
|
||||
public Employee(String name, int age, int employeeId) {
|
||||
this.employeeId = employeeId; // 在调用父类构造函数之前初始化字段
|
||||
super(name, age); // 调用父类构造函数
|
||||
// ... 其他初始化代码
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## JDK 22
|
||||
|
||||
### JEP 423:G1 垃圾收集器区域固定
|
||||
|
||||
JEP 423 提出在 G1 垃圾收集器中实现区域固定(Region Pinning)功能,旨在减少由于 Java Native Interface (JNI) 关键区域导致的延迟问题。
|
||||
|
||||
JNI 关键区域内的对象不能在垃圾收集时被移动,因此 G1 以往通过禁用垃圾收集解决该问题,导致线程阻塞及严重的延迟。通过在 G1 的老年代和年轻代中引入区域固定机制,允许在关键区域内固定对象所在的内存区域,同时继续回收未固定的区域,避免了禁用垃圾回收的需求。这种改进有助于显著降低延迟,提升系统在与 JNI 交互时的吞吐量和稳定性。
|
||||
|
||||
### JEP 454:外部函数和内存 API
|
||||
|
||||
Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过高效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),该 API 使 Java 程序能够调用本机库并处理本机数据,而不会像 JNI 那样危险和脆弱。
|
||||
|
||||
外部函数和内存 API 在 Java 17 中进行了第一轮孵化,由 [JEP 412](https://openjdk.java.net/jeps/412) 提出。Java 18 中进行了第二次孵化,由[JEP 419](https://openjdk.org/jeps/419) 提出。Java 19 中是第一次预览,由 [JEP 424](https://openjdk.org/jeps/424) 提出。JDK 20 中是第二次预览,由 [JEP 434](https://openjdk.org/jeps/434) 提出。JDK 21 中是第三次预览,由 [JEP 442](https://openjdk.org/jeps/442) 提出。
|
||||
|
||||
最终,该特性在 JDK 22 中顺利转正。
|
||||
|
||||
在 [Java 19 新特性概览](./java19.md) 中,我有详细介绍到外部函数和内存 API,这里就不再做额外的介绍了。
|
||||
|
||||
### JEP 456:未命名模式和变量
|
||||
|
||||
未命名模式和变量在 JDK 21 中由 [JEP 443](https://openjdk.org/jeps/443)提出预览,JDK 22 中就已经转正。
|
||||
|
||||
关于这个新特性的详细介绍,可以看看[Java 21 新特性概览(重要)](./java21.md)这篇文章中的介绍。
|
||||
|
||||
### JEP 458:启动多文件源代码程序
|
||||
|
||||
Java 11 引入了 [JEP 330:启动单文件源代码程序](https://openjdk.org/jeps/330),增强了 `java` 启动器的功能,使其能够直接运行单个 Java 源文件。通过命令 `java HelloWorld.java`,Java 可以在内存中隐式编译源代码并立即执行,而不需要在磁盘上生成 `.class` 文件。这简化了开发者在编写小型工具程序或学习 Java 时的工作流程,避免了手动编译的额外步骤。
|
||||
|
||||
假设文件`Prog.java`声明了两个类:
|
||||
|
||||
```java
|
||||
class Prog {
|
||||
public static void main(String[] args) { Helper.run(); }
|
||||
}
|
||||
|
||||
class Helper {
|
||||
static void run() { System.out.println("Hello!"); }
|
||||
}
|
||||
```
|
||||
|
||||
`java Prog.java`命令会在内存中编译两个类并执行`main`该文件中声明的第一个类的方法。
|
||||
|
||||
这种方式有一个限制,程序的所有源代码必须放在一个`.java`文件中。
|
||||
|
||||
[JEP 458:启动多文件源代码程序](https://openjdk.org/jeps/458) 是对 JEP 330 功能的扩展,允许直接运行由多个 Java 源文件组成的程序,而无需显式的编译步骤。
|
||||
|
||||
假设一个目录中有两个 Java 源文件 `Prog.java` 和 `Helper.java`,每个文件各自声明了一个类:
|
||||
|
||||
```java
|
||||
// Prog.java
|
||||
class Prog {
|
||||
public static void main(String[] args) { Helper.run(); }
|
||||
}
|
||||
|
||||
// Helper.java
|
||||
class Helper {
|
||||
static void run() { System.out.println("Hello!"); }
|
||||
}
|
||||
```
|
||||
|
||||
当你运行命令 `java Prog.java` 时,Java 启动器会在内存中编译并执行 `Prog` 类的 `main` 方法。由于 `Prog` 类中的代码引用了 `Helper` 类,启动器会自动在文件系统中找到 `Helper.java` 文件,编译其中的 `Helper` 类,并在内存中执行它。这个过程是自动的,开发者无需显式调用 `javac` 来编译所有源文件。
|
||||
|
||||
这一特性使得从小型项目到大型项目的过渡更加平滑,开发者可以自由选择何时引入构建工具,避免在快速迭代时被迫设置复杂的项目结构。该特性消除了单文件的限制,进一步简化了从单一文件到多文件程序的开发过程,特别适合原型开发、快速实验以及早期项目的探索阶段。
|
Loading…
x
Reference in New Issue
Block a user