问题

jdk9为何要将String的底层实现由char[]改成了byte[]?

回答
JDK 9 对 `String` 底层实现的修改,从 `char[]` 转向 `byte[]`,这背后是一个复杂的技术演进和优化考量,旨在提升 Java 在处理文本数据时的效率和内存占用。这不是一个突兀的改变,而是基于对字符串本质的重新认识以及对现代应用场景的适应。

为什么会有这个改变? 根源在于 "字符" 本身的复杂性

在深入细节之前,我们先回顾一下 `char` 和 `byte` 的概念。

`char` (Java):在 Java 中,`char` 是一个 16 位的无符号整数,用来表示一个 UTF16 编码的字符。UTF16 是一种变长编码,它可以表示 Unicode 字符集中的所有字符。ASCII 字符(如英文字母、数字)在 UTF16 中只占用一个 `char`,而很多其他语言的字符(如中文、日文、韩文)则需要两个 `char`(一个代理对,surrogate pair)。

`byte` (Java):`byte` 是一个 8 位的有符号整数,用于表示一个字节。字节是计算机存储和传输数据的基本单位。

早期 `String` 的 `char[]` 实现: 优点与局限

JDK 8 及之前,`String` 的核心就是 `char[]`。这种设计有其历史合理性:

1. Java 的设计哲学:Java 从一开始就强调跨平台性和对 Unicode 的良好支持。`char` 的 16 位设计,为表示大部分 Unicode 字符提供了一个直接且统一的单元。
2. 处理 ASCII 和 Latin1:对于主要使用 ASCII 或 Latin1 字符集的英语国家用户来说,`char[]` 的效率很高。每个字符都只需要一个 `char` (16 bits),与 `byte[]` (8 bits) 相比,虽然内存占用翻倍,但在编码和解码上更简单直接。
3. API 的一致性:String 的许多 API,如 `charAt(int index)`,天然地与 `char[]` 的索引访问契合,直接返回一个 `char`。

然而,随着 Java 在全球范围内的普及,以及互联网应用的爆炸式增长,`char[]` 的局限性开始显现:

内存浪费:全球化的应用需要处理大量的非 ASCII 字符。当字符串包含很多中文、日文、韩文等字符时,每个字符都需要两个 `char` 来表示。而很多时候,这些字符如果用更紧凑的编码(如 UTF8)来存储,只需要 1 到 4 个字节(`byte`)。这样一来,`char[]` 就会在很多地方造成不必要的内存浪费。想想看,一个包含大量中文的字符串,其 `char[]` 占用的内存可能是实际需要存储的 UTF8 编码的两倍甚至更多。
IO 操作的瓶颈:当进行文件读写、网络传输等 IO 操作时,数据通常是以字节流(`byte[]`)的形式进行的。如果 `String` 内部是 `char[]`,就需要频繁地在 `char[]` 和 `byte[]` 之间进行编码和解码(例如,从 UTF8 字节流转为 UTF16 字符数组,或者反之)。这个转换过程会消耗 CPU 资源,并可能成为性能瓶颈,尤其是在处理大量文本数据时。

JDK 9 的 `byte[]` 实现: 革命性的优化

JDK 9 的核心改动是引入了一个 “压缩字符串”(Compact Strings) 的特性。这个特性允许 `String` 的底层存储根据字符串内容的实际编码情况,在 `char[]` 和 `byte[]` 之间进行选择。

具体来说,`String` 类增加了一个 `coder` 字段:

`coder` 字段:这是一个 `byte` 类型,用来标记字符串是使用单字节编码 (`LATIN1`) 还是双字节编码 (`UTF16`)。
`LATIN1` (Code 0): 表示字符串中的所有字符都只需要一个字节(0255)来表示。这对应于 ISO88591 或 ASCII 兼容的编码。
`UTF16` (Code 1): 表示字符串中至少有一个字符需要两个字节(UTF16)来表示。

JDK 9 的 `String` 内部结构(简化版)

```java
// 简化示意,实际结构更复杂
public final class String {
// private final byte[] value; // 存储字符串内容的字节数组
// private final byte coder; // 标记编码方式:0 for LATIN1, 1 for UTF16

// ... 其他字段和方法
}
```

`byte[]` 实现带来的好处

1. 显著的内存节省:
如果字符串只包含 ASCII 或 Latin1 字符:`String` 会选择 `LATIN1` 编码,并将字符直接存储在 `byte[]` 中。这样,每个字符只需要 1 个字节。相比于之前的 `char[]` (16 bits/char),内存占用直接减半!
如果字符串包含非 ASCII 字符:`String` 会选择 `UTF16` 编码,并将字符存储在 `char[]` 中。这时,内存占用与之前类似(16 bits/char)。
整体效果:在实际应用中,很大一部分字符串(例如,配置文件、UI 文本、大部分编程语言关键字等)都属于 ASCII 或 Latin1 范围。因此,通过 `LATIN1` 编码,JDK 9 的 `String` 在内存占用上可以实现非常显著的节省。有测试表明,在某些场景下,内存占用可以降低 30%50%。

2. 提升 IO 性能:
当字符串内容是 `LATIN1` 时,JVM 在进行 IO 操作时,可以将 `byte[]` 直接写入输出流,或者直接从输入流读取到 `byte[]`,避免了频繁的 `char[]` 到 `byte[]` 的编码/解码转换。这极大地提高了 IO 吞吐量。
即便是 `UTF16` 的情况,JVM 内部也有更优化的 IO 策略来处理 UTF16 数据流。

3. 更灵活的字符串表示:
这种基于内容的动态选择编码方式,使得 `String` 的内部表示更加灵活和高效,能更好地适应不同语言和字符集的应用需求。

这个改变是如何实现的? 内部机制

字符串创建时的检测:当一个新的 `String` 对象被创建时(例如,通过 `new String(...)`,或者从字符数组、字节数组构造),JVM 会遍历字符串的内容,判断是否所有字符都能用单字节(0255)表示。
`StringFactory` 的作用:在 `String` 创建过程中,JVM 的 `StringFactory` 会负责进行这个编码检测,并根据检测结果,选择使用 `byte[]` 还是 `char[]` 来存储字符串数据,并设置 `coder` 字段。
API 的适配:虽然底层存储发生了变化,但 `String` 的公共 API 保持了向后兼容。例如,`charAt(int index)` 方法,即使底层是 `byte[]`,它也会通过内部逻辑,将对应位置的字节转换为 `char` 并返回。这通常是在 `LATIN1` 编码下,将 `byte` 值直接转换为 `char`。对于 `UTF16` 编码,则直接返回 `char`。

并非所有字符串都受益,但整体效率提升

需要强调的是,并非所有的 `String` 对象都会直接受益于 `byte[]` 的存储。如果一个字符串中包含任何超出 0255 范围的 Unicode 字符,它仍然会被存储在 `char[]` 中,使用 `UTF16` 编码。

但是,由于绝大多数应用场景中,ASCII 和 Latin1 字符占有相当大的比例,这个“压缩字符串”的特性能够平均地提高 Java 应用程序的内存使用效率和 IO 性能。

总结

JDK 9 将 `String` 的底层实现从固定使用 `char[]` 改为 “压缩字符串”,允许根据字符串内容在 `byte[]` (LATIN1) 和 `char[]` (UTF16) 之间动态选择。这一改变的核心目的是:

1. 大幅节省内存:对于只包含 ASCII 或 Latin1 字符的字符串,使用 `byte[]` 存储将内存占用减半。
2. 提升 IO 效率:减少了在 IO 操作中 `char[]` 与 `byte[]` 之间的频繁编码解码转换。
3. 适应全球化需求:更有效地处理不同字符集的数据。

这是一个非常成功的优化,它在保持 Java API 兼容性的前提下,显著提升了 Java 语言在现代应用中的性能和资源利用率。它体现了 Java 平台团队对实际应用场景的深刻理解和持续的工程改进能力。

网友意见

user avatar

这个特性是JDK9放出来的,主要是为了节约String占用的内存。

众所周知,在大多数Java程序的堆里,String占用的空间最大,并且绝大多数String只有Latin-1字符,这些Latin-1字符只需要1个字节就够了。JDK9之前,JVM因为String使用char数组存储,每个char占2个字节,所以即使字符串只需要1字节/字符,它也要按照2字节/字符进行分配,浪费了一半的内存空间。

JDK9是怎么解决这个问题的呢?一个字符串出来的时候判断,它是不是只有Latin-1字符,如果是,就按照1字节/字符的规格进行分配内存,如果不是,就按照2字节/字符的规格进行分配(UTF-16编码),提高了内存使用率。

举个类似的例子说就是,工厂生产的有多种型号的零件,一批零件必须装入同一种型号的包装发货,零件有大号和小号两种,而且绝大多数都是小号,之前包装的时候不管一批零件是大是小全部放到大号的包装里,造成了空间浪费,现在如果一批里面有大号就全装大号,如果没有大号就全装小号,提升了空间利用率。

这种做法带来的好处是显而易见的:

  • 原本一个仓库装不下的零件,现在可以装下了(用更少的内存跑更大的应用)
  • 原本仓库一天往外运一次,现在可以一天半甚至两天运一次(减少GC次数)

为什么用UTF-16而不用UTF-8呢,这就要从这两个字符集的设计说起了。

UTF-8实际上是对空间利用效率最高的编码集,它是不定长的,可以最大限度利用内存和网络。它是小包装装得下就用小包装,小包装装就看看能不能用大包装装,大包装都装不下就用超大包装。但是这种编码集只适用于传输和存储,并不适合拿来做String的底层实现。这是为什么呢?

因为String有随机访问的方法,所谓随机访问,就是charAt、subString这种方法,随便指定一个数字,String要能给出结果。如果字符串中的每个字符占用的内存是不定长的,那么进行随机访问的时候,就需要从头开始数每个字符的长度,才能找到你想要的字符。试想,如果大小包装混装,你想拿到第N个零件,你必须一个一个数包装盒,数到N,才能找到你要的零件。而如果包装是一样的大小,你就可以通过简单的计算知道你要找的零件距离你有多少远,直接过去拿就行了。

但是又有人会问了,UTF-16也是变长的啊,一个字符可能在UTF-16里面占用4个字节咧。是的,是的,UTF-16是变长的,但这是在现实世界里是这样。在java的世界里,一个字符(char)就是2个字节,从u0000到uFFFF,占4个字节的字符,在java里是用两个char来存储的,而String的各种操作,都是以java的字符(char)为单位的,charAt是取得第几个char,subString取的也是第几个到第几个char组成的子串,甚至length返回的都是char的个数,从来没有哪个方法可以让你“通过下标取出字符串中第几个'现实意义'中的字符”,所以UTF-16在java的世界里,就可以视为一个定长的编码。

还是工厂的例子:如果哪天出现了一个超大的零件咋办?简单,我这就只有大号包装,一个大号的包装都装不下的时候怎么办呢,把超大号的零件切成两份就行了,用两个大号包装去装,出厂也视为两个零件。

关于这种超大零件,可以试着跑以下下面的代码体会下:

因为知乎的字符集并不全,所以贴不出来,需要复制的话可以去这里复制:

gitee.com/yzyang/test/b

类似的话题

  • 回答
    JDK 9 对 `String` 底层实现的修改,从 `char[]` 转向 `byte[]`,这背后是一个复杂的技术演进和优化考量,旨在提升 Java 在处理文本数据时的效率和内存占用。这不是一个突兀的改变,而是基于对字符串本质的重新认识以及对现代应用场景的适应。为什么会有这个改变? 根源在于 ".............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有