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、网络地址等字符串不可被篡改 |
| 线程安全 | 天然线程安全,无需同步 |
| 哈希表 key | String 作为 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");这行代码实际上创建了两个对象:
- "hello" 字符串对象(在 StringTable 中)
- 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 的基础。
下一节,我们来看 字符串拼接(原理/效率对比)。
