String str = new String("abc") 创建了几个对象?
结论:标准答案是"一个或两个"——new 一定在堆(heap)上创建一个 String 实例;字面量 "abc" 还会让字符串常量池(String Pool / StringTable)里存在一个 "abc"。如果池中已有 "abc"(同名字面量此前出现过),就只创建堆上那 1 个;如果池中没有,则先创建池中的 1 个,再创建堆上的 1 个,共 2 个。答"1 个"或"2 个"都不完整,面试官要听的是池与堆的分工。
1. 池与堆各自负责什么
执行 new String("abc") 的两步:
① ldc "abc" ────→ 字符串常量池 StringTable
有 "abc"?复用引用 : 创建 String 并驻留(interned)
② new + dup + invokespecial ────→ 堆上新对象(构造器复制内容/共享 value)
StringTable(堆内,JDK 7+) Java 堆
┌────────────────────┐ ┌─────────────────────────┐
│ "abc" ─────────────┼──────┐ │ str ──→ String@0x1a2b │
└────────────────────┘ └────→│ value ──→ byte[]{a,b,c}│
└─────────────────────────┘
要点:
ldc指令读取的是**类的常量池(constant pool)**里那个CONSTANT_String,首次执行时解析到运行时常量池StringTable,没有则创建并驻留(intern);new String(String)构造器在 JDK 9 之后直接共享入参的value数组(coder相同时),因此两个String对象可能指向同一块byte[],但对象本身仍是两个;==比较的是对象地址,所以a == b必然为false。
2. 代码验证
public class NewStringDemo {
public static void main(String[] args) {
String a = "abc"; // 只在池中,0 或 1 个对象(视此前是否已有)
String b = new String("abc"); // 堆上 +1
System.out.println(a == b); // false:池引用 vs 堆对象
System.out.println(a == b.intern()); // true:intern 返回池中引用
String c = "ab" + "c"; // 编译期折叠成 "abc",运行期不新建
System.out.println(a == c); // true
String d = new String("abc").intern();
System.out.println(a == d); // true,堆对象被丢弃
}
}
对应的字节码(javap -c,截取核心几条):
0: new #9 // class java/lang/String
3: dup
4: ldc #7 // String abc ← 这一步可能触发池中创建
6: invokespecial #11 // Method "<init>":(Ljava/lang/String;)V
new 在 ldc 之前执行,说明"堆上对象"与"池中对象"是两条独立的创建路径,谁先谁后不影响数量结论。
3. 对象数量对照表
| 写法 | 池中新建 | 堆上新建 | 合计 |
|---|---|---|---|
String a = "abc"(池中无) | 1 | 0 | 1 |
String a = "abc"(池中已有) | 0 | 0 | 0 |
new String("abc")(池中无) | 1 | 1 | 2 |
new String("abc")(池中已有) | 0 | 1 | 1 |
"ab" + "c" | 1(折成 "abc") | 0 | 1 |
new String("abc") + "d" | 2("abc"、"d") | 1(结果) | 3 |
注意最后一行:拼接结果不进入常量池(除非调用 intern()),它只是堆上的普通对象。
4. 常见追问
| 追问 | 回答要点 |
|---|---|
| 常量池到底在哪个区域? | JDK 6 在永久代(PermGen),JDK 7 起移到堆,JDK 8 起永久代被元空间(Metaspace)取代,但字符串池仍在堆 |
intern() 做了什么? | 池中有则返回池中引用,没有则把当前对象引用登记进池并返回;JDK 7+ 登记的是堆对象引用,不再复制 |
大量 intern() 有什么风险? | StringTable 默认桶数约 60013,字符串多时冲突链表变长、查询变慢,可用 -XX:StringTableSize 调大 |
为什么同一个类里两个 "abc" 只有一个对象? | 类常量池本身就做了去重,运行期 ldc 解析结果相同 |
| 字符串池会被 GC 回收吗? | 会,池中只剩弱可达引用的字符串会被清理(JDK 7+ 池在堆内,参与 GC) |
substring 会共享数组吗? | JDK 6 会共享导致内存泄漏,JDK 7+ 已改为复制新数组 |
一个延伸细节:new String("abc") 里的 "abc" 在类加载后的首次执行时才入池,而不是类加载时就创建。因此同一段代码放在不同执行路径里,"池中是否已有"取决于谁先跑到。
小结
回答这道题的关键不是数数,而是讲清"字面量走池、new 走堆"。一句话版本:池中没有 "abc" 时创建 2 个(池 1 + 堆 1),已有则只创建 1 个(堆 1),而 "ab" + "c" 这类常量折叠根本不会新建。