Skip to content

String 不可变性/底层结构/内存分配

String 为什么如此特别

String 是 Java 中使用最频繁的类,也是面试中最常考的类之一。它的重要性不仅因为用得多,更因为它背后藏着一套完整的 JVM 内存管理和优化机制:不可变性保证安全、字符串常量池节省内存、intern() 实现驻留、拼接背后是 StringBuilder

理解 String,就是理解 JVM 内存管理的最佳切入点。

String 的不可变性

String不可变的(Immutable)——一旦创建,就不能改变其内容。

不可变性的证明

java
public class StringImmutable {
    public static void main(String[] args) {
        String s = "hello";

        // 表面上看起来改变了 s
        s = s + " world";
        // 实际上:
        // 1. 创建了新的 StringBuilder
        // 2. 拼接操作生成了新的 String 对象
        // 3. s 变量重新指向新的对象
        // 4. 原来的 "hello" 字符串没有任何变化

        System.out.println(s);  // hello world
        System.out.println("hello");  // 原来的字符串还在
    }
}

String 的底层结构

String 在 JDK 8 中使用 char[] 存储字符,JDK 9+ 改用 byte[]

java
// JDK 8 及之前:
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char[] value;  // 字符数组,存储字符串内容
    private final int offset;    // 偏移量(JDK 7 之前)
    private final int count;     // 字符数量(JDK 7 之前)
    private int hash;            // 缓存的 hashCode
}

// JDK 9+(Compact Strings):
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    private final byte[] value;  // 字节数组,存储字符串内容
    private final byte coder;    // 编码器:LATIN1(0) 或 UTF16(1)
    private int hash;            // 缓存的 hashCode
}

JDK 9+ 的改进:如果字符串只包含 Latin-1 字符(ASCII),使用 byte[] coder=LATIN1(1 字节/字符),节省内存。如果包含其他 Unicode 字符,使用 byte[] coder=UTF16(2 字节/字符)。

为什么 String 要设计成不可变

不可变性不是随意设计的,背后有深刻的安全和性能考量:

java
public class WhyImmutable {
    // 场景一:参数传递安全
    // 如果 String 可变,方法可以修改传入的字符串
    public void process(String input) {
        input = "modified";  // 如果 String 可变,调用者的字符串也会被改
    }

    // 场景二:线程安全
    // 不可变的 String 天生线程安全,不需要同步
    // 多个线程可以安全地共享同一个 String 对象
    private final String CONFIG = "/etc/app.conf";  // 绝对安全

    // 场景三:哈希表的安全
    // String 作为 HashMap 的 key,如果可变,hashCode 会变化
    Map<String, Integer> cache = new HashMap<>();
    String key = "user:1001";
    cache.put(key, 1);
    key = "user:1002";  // 如果 String 可变,这里可能破坏 cache 的内部结构
}

不可变性的好处:

原因说明
安全性文件路径、URL、网络地址等字符串不可被篡改
线程安全天然线程安全,无需同步
哈希表 keyString 作为 HashMap key 必须不可变,否则 hashCode 会变化
字符串常量池不可变性使得字符串常量池的共享是安全的
类加载器类名字符串等关键数据不会被意外修改

String 的内存布局

JDK 8 的 String 内存布局

java
// 在 JDK 8 中,一个 "hello" 字符串在内存中的样子:
String s = new String("hello");
String 对象(堆)
┌──────────────────────────────────────────────┐
│  对象头(Header)                             │
│  ├─ Mark Word: 锁信息、hashCode 等          │
│  └─ 类型指针: 指向 String 类元数据           │
├──────────────────────────────────────────────┤
│  char[] value(引用)                        │
│  ├─ 指向堆中的 char[] 数组                   │
│  └─ 通常与 String 对象本身在同一块区域(JDK 8)│
├──────────────────────────────────────────────┤
│  int hash(int)                            │
│  └─ 缓存的 hashCode,默认为 0                │
└──────────────────────────────────────────────┘

char[] 对象(堆,JDK 8)
┌──────────────────────────────────────────────┐
│  对象头                                      │
├──────────────────────────────────────────────┤
│  char[5] value                              │
│  ├─ value[0] = 'h'                         │
│  ├─ value[1] = 'e'                         │
│  ├─ value[2] = 'l'                         │
│  ├─ value[3] = 'l'                         │
│  └─ value[4] = 'o'                         │
└──────────────────────────────────────────────┘

JDK 9+ 的 String 内存布局

JDK 9 之后,String 内部使用 byte[],一个 "hello" 字符串在内存中更紧凑:

String 对象(堆,JDK 9+)
┌──────────────────────────────────────────────┐
│  对象头(Header)                             │
├──────────────────────────────────────────────┤
│  byte[] value(引用)                         │
│  ├─ byte[5] value                          │
│  └─ 如果只含 Latin-1:h,e,l,l,o 各占 1 字节 │
├──────────────────────────────────────────────┤
│  byte coder(byte)                         │
│  └─ LATIN1(0) 或 UTF16(1)                   │
├──────────────────────────────────────────────┤
│  int hash(int)                             │
└──────────────────────────────────────────────┘

String 的 hashCode

String 的 hashCode() 使用了惰性计算策略:

java
public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        // 只计算一次,之后缓存
        h = computeHashCode(value, coder);
        hash = h;
    }
    return h;
}

惰性计算意味着:如果 String 被放进 HashMap 但从未被 hashCode() 调用,hash 字段会一直为 0。

String 的创建方式与内存分配

String 有两种创建方式,内存分配完全不同:

java
public class StringCreation {
    public static void main(String[] args) {
        // 方式一:字面量
        String s1 = "hello";
        // → 先检查 StringTable
        // → 如果已存在,直接返回引用
        // → 如果不存在,在 StringTable 中添加

        // 方式二:new
        String s2 = new String("hello");
        // → 无论如何都在堆中创建新的 String 对象
        // → 同时 "hello" 字面量可能被加入 StringTable
    }
}
创建方式内存位置数量StringTable
String s = "hello"堆(StringTable)0 或 1 个对象在 StringTable 中
String s = new String("hello")堆(2 个对象)1 个 String + 1 个 char[]/byte[]"hello" 可能已在 StringTable 中

new String() 创建两个对象

java
String s = new String("hello");

这行代码实际上创建了两个对象

  1. "hello" 字符串对象(在 StringTable 中)
  2. new String() 构造出的新 String 对象(在堆中)

String 的相关参数

bash
# JDK 8:StringTable 大小
java -XX:StringTableSize=1000000 MyApp

# JDK 9+:Compact Strings(默认开启)
java -XX:+CompactStrings MyApp    # 开启(默认)
java -XX:-CompactStrings MyApp   # 关闭(使用 char[])

本节小结

String 的核心特性:

特性说明
不可变性final 修饰,无法被子类重写,内容不可变
底层存储JDK 8: char[];JDK 9+: byte[] + coder
hashCode惰性计算,缓存后不再重复计算
字面量创建在 StringTable 中,只创建 1 个对象
new 创建在堆中创建新 String 对象,可能创建 2 个对象
不可变原因安全、线程安全、哈希表安全、字符串常量池

理解 String 的不可变性和底层结构,是理解字符串拼接、intern()、StringTable 的基础。

下一节,我们来看 字符串拼接(原理/效率对比)

基于 VitePress 构建