观察 java
中 CompactStrings
选项的影响
背景
JDK 版本 |
String 中的 value 字段的类型 |
代码 | 图 |
---|---|---|---|
JDK 8 |
char[] |
value 字段 | ![]() |
JDK 9 及之后 |
byte[] |
value 字段 | ![]() |
在 JDK 8
及之前,java
中的 String
使用 char[]
来保存 String
的各个 char
。以 JDK 8
为例,在 String.java 中可以看到,String
中的 value
字段的类型是 char[]
。
ASCII
和 Latin-1
ASCII
字符在 Unicode
字符中的编号范围是从 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 到 <math xmlns="http://www.w3.org/1998/Math/MathML"> 127 127 </math>127, Latin-1
字符(如上图所示,它包含了所有 ASCII
字符)的编号范围是从 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 到 <math xmlns="http://www.w3.org/1998/Math/MathML"> 255 255 </math>255。所以每个 Latin-1
字符都可以用 8 bit
来表示。
如果代码中只处理 Latin-1
字符,char[]
这样的存储方式会造成空间浪费。 例如在 "Hi"
这个 String
里,只有 'H', 'i'
2
个 char
,在 char[]
类型的 value
字段中,需要用 <math xmlns="http://www.w3.org/1998/Math/MathML"> 16 × 2 = 32 16\times 2=32 </math>16×2=32 bit
(java
中的一个 char
大小为 16 bit
)来存储它们。假如我们改用 byte[]
来保存 'H', 'i'
,那么只需要用 <math xmlns="http://www.w3.org/1998/Math/MathML"> 8 × 2 = 16 8\times 2=16 </math>8×2=16 bit
(java
中的一个 byte
大小为 8 bit
)。
从 JDK 9
开始,String
内部改用 byte[]
来保存 char
。对 Latin-1
范围的 char
而言,char
的高 8 bit
都是 0
,所以可以只用一个 byte
来保存这个 char
的低 8 bit
而不会造成信息的损失。
以 'A'
这个 char
为例,它会占据 16
个 bit
⬇️
text
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 (Bit Index)
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | (Bit Content)
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
'A' = 65 (Decimal)
如果我们忽略掉高 8 bit
,那就可以这样保存 'A'
⬇️
text
7 6 5 4 3 2 1 0 (Bit Index)
+---+---+---+---+---+---+---+---+
| 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | (Bit Content)
+---+---+---+---+---+---+---+---+
'A' = 65 (Decimal)
请注意:虽然 String
中的 value
字段的类型发生了变化,但是从概念上讲,我们还是可以认为一个 String
实例是由若干个 char
组成的。至少有两个理由 ⬇️
String
类中的charAt(int)
方法依然存在String
类中的length()
方法的返回值仍旧和对应的char
的数量相同
所以本文还是会采用 "String
中的 char
" 这类的说法。
对完全由 Latin-1
构成的 String
<math xmlns="http://www.w3.org/1998/Math/MathML"> s s </math>s 而言 ⬇️
CompactStrings 选项 |
<math xmlns="http://www.w3.org/1998/Math/MathML"> s s </math>s 中 value 字段的 length 与 <math xmlns="http://www.w3.org/1998/Math/MathML"> s . l e n g t h ( ) s.length() </math>s.length() 的关系 |
解释 |
---|---|---|
开启(⬅️ 这是默认的行为) | <math xmlns="http://www.w3.org/1998/Math/MathML"> v a l u e . l e n g t h = s . l e n g t h ( ) value.length = s.length() </math>value.length=s.length() | CompactStrings 选项开启时,每个 char 用 1 个 byte 来存储 (请注意,这里的大前提是: <math xmlns="http://www.w3.org/1998/Math/MathML"> s s </math>s 中的每个 char 都来自 Latin-1 ) |
关闭 | <math xmlns="http://www.w3.org/1998/Math/MathML"> v a l u e . l e n g t h = s . l e n g t h ( ) × 2 value.length = s.length() \times 2 </math>value.length=s.length()×2 | CompactStrings 选项关闭时,每个 char 用 2 个 byte 来存储 |
本文会用一些代码来验证 CompactStrings
选项的影响。
要点
从 JDK 9
开始,String
类改为使用 byte[]
来存储 char
。
- 如果开启
CompactStrings
选项(⬅️ 这是默认行为) - 如果通过
-XX:-CompactStrings
来关闭CompactStrings
,则每个char
占据byte[]
中的2
个元素
代码及准备工作
请将以下代码保存为 CompactStringTest.java
⬇️
java
import java.lang.reflect.Field;
public class CompactStringTest {
public static void main(String[] args) throws Exception {
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
Field coderField = String.class.getDeclaredField("coder");
coderField.setAccessible(true);
String decorator = "=".repeat(5);
for (String str : new String[]{"Hi", "人", "\uD83C\uDF34"}) {
System.out.printf("%s Details for [%s] %s%n", decorator, str, decorator);
byte[] value = (byte[]) valueField.get(str);
System.out.println("Each element of value:");
for (byte b : value) {
System.out.printf("0x%02X%n", b);
}
System.out.println();
byte coder = (byte) coderField.get(str);
System.out.println("Coder:");
System.out.println(coder);
System.out.println();
}
}
}
这段代码里会处理以下 3
个 String
⬇️
String 的内容 |
这个 String 由哪些 char 组成? |
char 的数量 |
是否完全由 Latin-1 组成? |
---|---|---|---|
"Hi" |
'H', 'i' |
2 |
✅ |
"人" |
'人' |
1 |
❌ |
"\uD83C\uDF34" |
'\uD83C', '\uDF34' |
2 |
❌ |
以下命令可以编译 CompactStringTest.java
⬇️
bash
javac CompactStringTest.java
用两种方式来运行
我们可以用两种不同的方式来运行其中的 main(...)
方法。具体的命令如下 ⬇️
bash
# 下面这一行是第一种方式
java --add-opens=java.base/java.lang=ALL-UNNAMED CompactStringTest
# 下面这一行是第二种方式
java --add-opens=java.base/java.lang=ALL-UNNAMED -XX:-CompactStrings CompactStringTest
在我的电脑上 ,运行结果如下 ⬇️ (在您的电脑上,byte[]
中的值的顺序可能会不同)
第一种方式的运行结果
text
===== Details for [Hi] =====
Each element of value:
0x48
0x69
Coder:
0
===== Details for [人] =====
Each element of value:
0xBA
0x4E
Coder:
1
===== Details for [🌴] =====
Each element of value:
0x3C
0xD8
0x34
0xDF
Coder:
1
第二种方式的运行结果
text
===== Details for [Hi] =====
Each element of value:
0x48
0x00
0x69
0x00
Coder:
1
===== Details for [人] =====
Each element of value:
0xBA
0x4E
Coder:
1
===== Details for [🌴] =====
Each element of value:
0x3C
0xD8
0x34
0xDF
Coder:
1
对两种方式的运行结果进行对比
"Hi" 的结果对比
标题 | coder 的值 |
byte[] 中每个元素的值 |
图示 | 补充说明 |
---|---|---|---|---|
第一种方式 | 0 (和 Latin-1 对应) |
0x48, 0x69 |
![]() |
H 和 0x48 对应, i 和 0x69 对应 |
第二种方式 | 1 (和 UTF-16 对应) |
0x48, 0x00, 0x69, 0x00 |
![]() |
H 和 0x48 对应, i 和 0x69 对应 |
"人"
的结果对比
两种方式的输出一样,所以写到下表的同一行里了 ⬇️
标题 | coder 的值 |
byte[] 中每个元素的值 |
图示 | 补充说明 |
---|---|---|---|---|
第一种方式/第二种方式 | 1 (和 UTF-16 对应) |
0xBA, 0x4E |
![]() |
人 和 0x4EBA 对应,在我的电脑上 ,value 字段中字节的顺序是先 0xBA ,后 0x4E |
"\uD83C\uDF34"
的结果对比
两种方式的输出一样,所以写到下表的同一行里了 ⬇️
标题 | coder 的值 |
byte[] 中每个元素的值 |
图示 | 补充说明 |
---|---|---|---|---|
第一种方式/第二种方式 | 1 (和 UTF-16 对应) |
0x3C, 0xD8, 0x34, 0xDF |
![]() |
🌴 在 CompactStringTest.java 中是用 \uD83C , \uDF34 这两个 char 来表示的。 |
关于 🌴 这个 code point
,我多解释一下。在 CompactStringTest.java
中,🌴 是用以下 2
个 char
表示的 ⬇️
\uD83C
\uDF34
char |
对应的 byte 的值 |
在我的电脑上 ,value 中存储的 byte 的顺序 |
---|---|---|
\uD83C |
0xD8 和 0x3C |
先存 0x3C ,后存 0xD8 |
\uDF34 |
0xDF 和 0x34 |
先存 0x34 ,后存 0xDF |