String str = new String("abc") 创建了几个对象?

#Java#面试八股

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"(池中无)101
String a = "abc"(池中已有)000
new String("abc")(池中无)112
new String("abc")(池中已有)011
"ab" + "c"1(折成 "abc")01
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" 这类常量折叠根本不会新建。

笔记加载中…