jdk 9 compact strings通过byte[]+coder字段实现按需编码:coder=0时用latin-1单字节存储,coder=1时升格utf-16双字节;相比java 8的固定char[],纯ascii字符串内存减半,实测堆内存节省25%~30%。
![如何通过 compact strings 理解 jdk 9 对字符串内部 byte[] 存储的内存优化](https://img.php.cn/upload/article/001/242/473/177607884421850.jpeg?x-oss-process=image/resize,p_40)
JDK 9 的 Compact Strings 不是“压缩”,而是按内容自动选编码:Latin-1 字符用 byte[] 单字节存,含中文等字符才升格为 UTF-16 双字节存 —— 内存节省真实、可观,但必须理解 coder 字段才是关键判据。
为什么 char[] 改成 byte[] 能省内存
Java 8 中 String 用 char[],每个 char 固定占 2 字节(UTF-16),哪怕只存 "hello" 这种纯 ASCII 字符,也浪费一半空间。JDK 9 改用 byte[] + coder 字段,实现“按需分配”:
-
coder == 0(即LATIN1):字符串所有字符码点 ≤ 255,value是纯byte[],每个字符占 1 字节 -
coder == 1(即UTF16):含中文、emoji 等,value仍是byte[],但按 UTF-16 编码打包,每字符占 2 字节(如"你好"→byte[4]) - 混合字符串(如
"abc你好")直接升格为UTF16,整个字符串统一用 2 字节/字符,不拆分编码
实测中,大量英文日志、JSON key、HTTP header 场景下,堆中字符串内存平均减少 25%~30%,GC 压力明显下降。
如何验证某个 String 是否启用 Latin-1 存储
不能只看内容是否“看起来像 ASCII”,必须查 coder 字段 —— 它才是 JVM 决策的最终依据:
- 反射读取:
String.class.getDeclaredField("coder").get(string),返回0表示 Latin-1,1表示 UTF-16 - 调试时观察:IDE 调试器里展开
String实例,能看到coder和value字段值 - 注意陷阱:
new String("a".getBytes(), StandardCharsets.ISO_8859_1)仍可能被升格为 UTF-16,因为构造逻辑走的是String(byte[], Charset)重载,内部会先 decode 再 encode 判定,不是简单看输入字节
哪些操作会意外触发 UTF-16 升格
看似无害的操作,可能让本可 Latin-1 存储的字符串被迫用双字节:
- 拼接含非 Latin-1 字符的字符串:
"abc" + "你好"→ 整个结果为UTF16 - 调用某些
String构造方法:new String(char[])或new String(byte[], int, int, String)(尤其指定"UTF-8"时),JVM 无法静态判定内容,保守升格 - 使用
StringBuffer/StringBuilderappend 后转toString():若中间过程引入宽字符,结果必为UTF16
这些场景下,即使原始片段全是 ASCII,最终字符串也失去 Latin-1 优势 —— 不是 bug,是语义一致性要求。
开发者真正需要关注的兼容性边界
绝大多数代码完全不受影响,但以下三处容易踩坑:
- 反射直接访问
String.value:JDK 8 返回char[],JDK 9+ 返回byte[],且必须配合coder解码,否则读出乱码或越界 - JNI 层硬编码假设
value是jchar*:需改用GetStringCritical+GetStringUTFChars等安全 API,避免直接指针解引用 - 序列化自定义逻辑:若手动序列化
value字段,必须同时序列化coder,否则反序列化后charAt()行为错乱
Compact Strings 的精妙在于它把复杂性锁在 JVM 内部:对外接口(length()、charAt()、equals())全部保持语义不变,但底层已从“固定宽度数组”变成“带元数据的紧凑结构”——这个 coder 字段,就是你理解一切优化与边界的钥匙。










