[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 字符中的编号范围是从 0 0 0 到 127 127 127, Latin-1 字符(如上图所示,它包含了所有 ASCII 字符)的编号范围是从 0 0 0 到 255 255 255。所以每个 Latin-1 字符都可以用 8 bit 来表示。

如果代码中只处理 Latin-1 字符,char[] 这样的存储方式会造成空间浪费。 例如在 "Hi" 这个 String 里,只有 'H', 'i' 2char,在 char[] 类型的 value 字段中,需要用 16 × 2 = 32 16\times 2=32 16×2=32 bit(java 中的一个 char 大小为 16 bit)来存储它们。假如我们改用 byte[] 来保存 'H', 'i',那么只需要用 8 × 2 = 16 8\times 2=16 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 s s s 而言 ⬇️

CompactStrings 选项 s s s 中 value 字段的 length s . l e n g t h ( ) s.length() s.length() 的关系 解释
开启(⬅️ 这是默认的行为) v a l u e . l e n g t h = s . l e n g t h ( ) value.length = s.length() value.length=s.length() CompactStrings 选项开启时,每个 char1byte 来存储 (请注意,这里的大前提是: s s s 中的每个 char 都来自 Latin-1
关闭 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 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

参考资料

相关推荐
GetcharZp29 分钟前
别再盲目用 OpenCV 读图了,这才是 CV 预处理的终极杀手锏!
后端
何极光43 分钟前
IDEA集成Maven
java·maven·intellij-idea
程序员二叉1 小时前
【JUC】ThreadLocal底层原理|内存泄漏|弱引用|跨线程传递方案
java·开发语言·面试·职场和发展·juc
程序员二叉1 小时前
【JUC】线程池全套深度详解|参数|流程|拒绝策略|调优|异常处理
java·开发语言·jvm·算法·面试·juc
老马识途2.01 小时前
在AI的帮助下理解spring的启动过程
java·前端·spring
青山木1 小时前
Hot 100 --- 轮转数组
java·数据结构·算法
Qt程序员2 小时前
掌握 Linux 内核调度:从原理到实现(进程篇)
java·开发语言
code bean2 小时前
【LangChain】检索器完全指南:从向量检索到生产级 RAG 架构
java·开发语言·微服务
大白菜和MySQL2 小时前
java应用排查高线程
java·python
KobeSacre2 小时前
ReentrantLock源码
java