[Java] 观察 CompactStrings 选项的影响

观察 javaCompactStrings 选项的影响

背景

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[]

ASCIILatin-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' 2char,在 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 为例,它会占据 16bit ⬇️

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 选项开启时,每个 char1byte 来存储 (请注意,这里的大前提是: <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 选项关闭时,每个 char2byte 来存储

本文会用一些代码来验证 CompactStrings 选项的影响。

要点

JDK 9 开始,String 类改为使用 byte[] 来存储 char

  • 如果开启 CompactStrings 选项(⬅️ 这是默认行为)
    • 如果一个 String 完全由 Latin-1char 组成,每个 char 占据 byte[] 中的 1 个元素,例如
      • 'A' 会用 1byte 来表示
    • 当一个 String 包含非 Latin-1char 时,每个 char 占据 byte[] 中的 2 个元素,例如
      • '人' 会用 2byte 来表示
      • 'A' 会用 2byte 来表示
  • 如果通过 -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();
        }
    }
}

这段代码里会处理以下 3String ⬇️

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 H0x48 对应, i0x69 对应
第二种方式 1 (和 UTF-16 对应) 0x48, 0x00, 0x69, 0x00 H0x48 对应, i0x69 对应

"人" 的结果对比

两种方式的输出一样,所以写到下表的同一行里了 ⬇️

标题 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 中,🌴 是用以下 2char 表示的 ⬇️

  • \uD83C
  • \uDF34
char 对应的 byte 的值 在我的电脑上value 中存储的 byte 的顺序
\uD83C 0xD80x3C 先存 0x3C,后存 0xD8
\uDF34 0xDF0x34 先存 0x34,后存 0xDF

参考资料

相关推荐
代码萌新知3 小时前
设计模式学习(五)装饰者模式、桥接模式、外观模式
java·学习·设计模式·桥接模式·装饰器模式·外观模式
你的人类朋友4 小时前
【Node】单线程的Node.js为什么可以实现多线程?
前端·后端·node.js
iナナ5 小时前
Spring Web MVC入门
java·前端·网络·后端·spring·mvc
驱动探索者5 小时前
find 命令使用介绍
java·linux·运维·服务器·前端·学习·microsoft
卷Java5 小时前
违规通知功能修改说明
java·数据库·微信小程序·uni-app
CoderYanger5 小时前
优选算法-双指针:2.复写零
java·后端·算法·leetcode·职场和发展
小雨凉如水5 小时前
k8s学习-pod的生命周期
java·学习·kubernetes
李宥小哥6 小时前
C#基础10-结构体和枚举
java·开发语言·c#
领创工作室6 小时前
安卓设备分区作用详解-测试机红米K40
android·java·linux
数据知道7 小时前
Go基础:用Go语言操作MongoDB详解
服务器·开发语言·数据库·后端·mongodb·golang·go语言