String 底层原理与常量池全解析:byte []+coder、intern 陷阱、substring 内存泄漏,一篇讲透
2026/8/31 4:51:39 网站建设 项目流程


文章收录专栏:Java 核心原理全解:源码・并发・面试实战

引子

String是 Java 里用得最多的类,也是最容易 “灯下黑” 的类:new String("abc")到底几个对象?s1.intern() == s1到底 true 还是 false?JDK 9 为什么把char[]换成byte[]?JDK 7 的substring又藏着什么内存泄漏事故?

今天一次讲透 String 的底层存储、常量池机制、拼接原理、内存模型与经典方法变迁,面试背熟、生产避坑两不误。

一、String 的核心特性:final + 不可变

final类、内部存储数组final、对外无 setter,所有 “修改” 操作都返回全新对象。不可变 ⇒天生线程安全,可多线程安全共享。

二、底层存储:char [] → byte [] + coder

JDK 8 及以前

private final char value[]; // 每字符固定 2 字节,纯 ASCII 也浪费

JDK 9 起(JEP 254 Compact Strings)

private final byte[] value; // 存储字节 private final byte coder; // 0 = LATIN1(1字节/字符),1 = UTF16(2字节/字符)
  • 全在 Latin-1 范围 → LATIN1,每字符 1 字节;否则 UTF16,2 字节;
  • 方法分StringLatin1/StringUTF16两套;LATIN1 下哈希、indexOf 计算更快;
  • 可用-XX:-CompactStrings关闭(关闭后全走 UTF16);
  • 主流场景平均省一半内存。

⚠️length()返回char 数不是逻辑字符数:emoji 等增补字符以代理对表示,1 个逻辑字符占 2 个 char。数真实字符用s.codePointCount(0, s.length())—— 参数是起止索引(beginIndex, endIndex),且该方法需遍历、有代价,日常用length()即可。

三、字符串常量池

3.1 本质与引用性质

全局唯一的表(HashTable 结构),存字符串对象的引用。存的是强引用不是弱引用;但 GC 时 JVM 会专门扫描 StringTable,对除池外无其他 GC Roots 的 String 回收并清理对应条目 —— 这是特殊强引用 + GC 清理策略,不是弱引用语义。

3.2 位置变迁(必考)

版本位置
JDK 6 及以前方法区(永久代)
JDK 7移到堆
JDK 8+永久代→元空间,池仍在堆

调优-XX:StringTableSize设桶数(JDK 7 前约 1009、JDK 8 起约 60013,较新版本支持动态扩容、参数近于初始桶数);-XX:+PrintStringTableStatistics打印统计排查 intern 滥用。

3.3 三个 “常量池” 别混淆

Class 文件常量池(每类一个)、运行时常量池(每类一个,元空间)、字符串常量池(全局唯一,堆)。

3.4 intern () 的两个陷阱

陷阱一:版本差异——JDK 6 intern 复制进永久代(两份);JDK 7+ 直接复用堆对象引用(一份)。

陷阱二:intern()==s的成立条件—— 只有调用前池中不存在该字符串才 true;只要池中已存在就必 false:

String s1 = new String("abc"); // 字面量 "abc" 已入池 s1.intern() == s1; // false!返回池引用 // 前提:程序执行前从未加载过字面量 "abc" String s2 = new String(new char[]{'a','b','c'}); s2.intern() == s2; // true(JDK7+),池原本没有

四、new String (“abc”) 到底几个对象?(分版本)

场景JDK 6JDK 7+
池中无 “abc”2 个:永久代池对象 + 堆对象(两份独立实例)2 个:堆内池对象 + 堆内 new 对象
池中已有 “abc”1 个堆对象1 个堆对象
String s1 = "abc"; // 1 个池对象 String s2 = new String("abc"); // 只创建 1 个堆对象 s1 == s2 // false s1 == "abc" // true

JDK 7+ 的 “池对象” 本身在堆上;计数默认只算 String 实例,若把底层数组也算作对象则数量更多。

五、字符串拼接:常量折叠 vs StringBuilder vs invokedynamic

  • 常量折叠"a"+"b"+"c"编译期变"abc",零开销;final String x="a"; x+"b"也是编译期折叠入池(高频变体);
  • JDK 8 变量拼接:编译为new StringBuilder().append().toString()
  • JDK 9+(JEP 280):javac不再硬编码 StringBuilder,改invokedynamic+StringConcatFactory,运行期策略选优(精确预分配 / 预估容量 / BC_SB=ByteCode StringBuilder);
  • 误区澄清:JEP 280 只是编译期不再强制 StringBuilder,运行期策略仍可能内部使用 StringBuilder
  • 变量拼接结果都是堆中新对象,只有常量折叠才指向常量池;
  • 反常识澄清:JDK 9+ 的StringBuilder.toString()依然是复制数组(源码注释 “Create a copy, don’t share the array”),不存在 “共享 byte [] 零拷贝”—— 真正的优化来自预分配策略,不是 toString;
  • 循环+=是 O (n²),必须 StringBuilder。

六、equals /hashCode 底层

  • equals 完整流程==引用 →instanceof→ 比coder编码 →比长度→ 逐字节对比;
  • hashCodes[0]*31^(n-1)+...+s[n-1],用 31(奇素数 +(h<<5)-h优化);hash字段默认 0 懒计算,空字符串""恒为 0、不重复计算;哈希恰为 0 的非空字符串会失去缓存收益。

七、String 与 StringBuilder:别再混用

可变性线程安全场景
String不可变安全常量、key
StringBuilder可变不安全单线程拼接(推荐)
StringBuffer可变安全多线程共享(极少)
  • 构造容量:无参 16、String(str)= len+16、带参按指定;
  • 扩容:JDK 8 为旧*2+2ensureCapacity取 max),JDK 9+ 结合 coder 调整;扩容要拷贝数组 → 预分配;
  • toString 结果不入池
String s = new StringBuilder().append("a").append("b").toString(); s == "ab" // false s.equals("ab") // true

八、常量池与 JVM 内存模型

堆:String 对象(byte[]+coder)、静态变量(JDK7+)、字符串常量池(JDK7+) 元空间:类元数据、运行时常量池(每类一份) 栈:局部变量引用
  • 引用路径:一个被 intern 的 String 有两条引用路径—— 栈上局部变量 和 StringTable 条目,都指向堆中同一个对象;StringTable 是堆中的哈希表,不是独立 “内存层”;
  • 静态变量在堆的 Class 镜像,不在方法区
  • 池可被 GC 回收,但别滥用 intern;需可控去重用Map/Set自管。

九、为什么 String 不可变?—— 以及它的边界

  1. 线程安全;2. 哈希稳定;3. 池复用省内存;4. 参数安全;5. 可缓存 hashCode。

边界:反射可setAccessible修改内部value(JDK 8 改char[];JDK 9+ 改byte[]且必须同步改coder,否则错乱);JDK 16+ 默认强封装拦截,需--add-opens java.base/java.lang=ALL-UNNAMED放行。仅研究用,生产禁用。

十、12 个生产坑点

  1. 循环+→ O(n²);
  2. new String("abc")双对象;
  3. 误用==比内容(应equals);
  4. 密码存储:char[]可清零但堆 dump 仍见明文、只能降险不能杜绝,叠加 PBKDF2/BCrypt、禁入日志;
  5. intern 滥用 → StringTable 冲突、内存上涨;
  6. 误区:JDK9 后内部还是char[]→ 是byte[]
  7. 误区:常量池在元空间 → 在堆;
  8. 误区:静态变量在方法区 → 在堆;
  9. StringBuilder 不预分配 → 频繁拷贝;
  10. 单线程用 StringBuffer → 无谓同步;
  11. JDK6substring:复用大 char [] + offset/count,小片段钉住大数组 → 内存泄漏(JDK7u6 修复);
  12. 高频String.format→ 性能差。

十一、经典方法变迁与进阶优化

substring(高频追问):JDK 6 复用原始完整char[]、仅调 offset/count 标记区间,大字符串截小片段 → 大数组无法回收 →内存泄漏;JDK 7u6 起复制新数组,牺牲性能换安全。

G1 字符串去重(JEP 192)-XX:+UseStringDeduplication仅 G1 生效,ZGC/Shenandoah 不支持;GC 时让内容相同的 String 共享底层数组,与 intern 无关、不走 StringTable

总结(记忆金句)

  • 存储:JDK 9byte[]+coder;常量池:JDK 7 起在、存引用、强引用 + GC 扫描清理;
  • 对象数:池中无 = 2(JDK7+ 都在堆),池中有 = 1;
  • intern:==s仅 “池中原本没有” 时 true;
  • 拼接:常量折叠(含 final)/ JDK8 StringBuilder / JDK9+ invokedynamic;toString 不入池且始终复制
  • substring:JDK 7u6 由共享改复制;比内容务必equals

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询