文章目录
-
- [一、先说结论:1 个或 2 个](#一、先说结论:1 个或 2 个)
- 二、拆解过程:这行代码到底做了什么?
-
- 第一步:处理字面量 "hello"
- [第二步:执行 new String()](#第二步:执行 new String())
- 三、用图理解内存布局
-
- [场景一:常量池中不存在 "hello"(首次出现)](#场景一:常量池中不存在 "hello"(首次出现))
- 场景二:常量池中已存在 "hello"
- [四、什么情况下常量池会提前有 "hello"?](#四、什么情况下常量池会提前有 "hello"?)
- 五、一个更复杂的变体
- [六、intern() 方法:手动入池](#六、intern() 方法:手动入池)
-
- [intern 的经典面试题(JDK 7+)](#intern 的经典面试题(JDK 7+))
- [七、JDK 6 vs JDK 7+:常量池的位置变化](#七、JDK 6 vs JDK 7+:常量池的位置变化)
- 八、面试速答模板
这道题看似简单,实则是 Java 内存模型的试金石。很多面试者脱口而出"两个",但答案远没有这么简单------"几个"取决于上下文,而能说清上下文的人,才是真正理解 JVM 内存模型的人。
一、先说结论:1 个或 2 个
java
String s = new String("hello");
- 如果字符串常量池中已经存在
"hello"→ 创建 1 个对象(堆上的 String 实例) - 如果字符串常量池中不存在
"hello"→ 创建 2 个对象(常量池中的字面量 + 堆上的 String 实例)
为什么会有这种不确定性?因为 "hello" 这个字面量在编译期就被确定了,而它是否入池取决于在这行代码执行之前,常量池中是否已经有了 "hello"。
二、拆解过程:这行代码到底做了什么?
第一步:处理字面量 "hello"
当 JVM 遇到 "hello" 这个字面量时,会先去字符串常量池中查找:
- 找到了 → 直接返回引用,不做任何创建
- 没找到 → 在常量池中创建一个
"hello"字符串对象,然后返回引用
第二步:执行 new String()
new 关键字一定会在堆内存 上创建一个新的 String 对象,用常量池中 "hello" 的值来初始化。
java
String s = new String("hello");
用伪代码描述整个过程:
1. 查找常量池是否有 "hello"
- 没有 → 常量池创建 "hello" 对象(对象 A)
- 有 → 跳过
2. 堆上 new 一个 String 对象(对象 B),值为 "hello"
3. s 指向对象 B(堆上的对象)
所以,对象 A 是常量池中的,对象 B 是堆上的,它们是两个独立的对象,只是值相同。
三、用图理解内存布局
场景一:常量池中不存在 "hello"(首次出现)
字符串常量池 堆
┌──────────────┐ ┌──────────────┐
│ "hello" (A) │ │ "hello" (B) │
└──────────────┘ └──────────────┘
↑
│
s 指向 B
创建了 2 个对象
场景二:常量池中已存在 "hello"
字符串常量池 堆
┌──────────────┐ ┌──────────────┐
│ "hello" (A) │ (之前已存在)│ "hello" (B) │
└──────────────┘ └──────────────┘
↑
│
s 指向 B
创建了 1 个对象
四、什么情况下常量池会提前有 "hello"?
只要在此之前有代码触发了 "hello" 的入池,常量池中就会存在:
java
// 情况 1:直接使用字面量
String a = "hello"; // "hello" 入池
String b = new String("hello"); // 常量池已有,只创建 1 个
// 情况 2:同一个类中前面的代码已经用过
System.out.println("hello"); // "hello" 入池
String s = new String("hello"); // 常量池已有,只创建 1 个
// 情况 3:intern() 方法
String c = new String("hel") + new String("lo");
c.intern(); // "hello" 入池
String d = new String("hello"); // 常量池已有,只创建 1 个
五、一个更复杂的变体
java
String s = new String("hello") + new String("world");
这行代码创建了几个对象?逐步拆解:
| 步骤 | 操作 | 创建的对象 |
|---|---|---|
| 1 | 字面量 "hello" 入池 |
常量池 "hello"(如果不存在) |
| 2 | new String("hello") |
堆上 String 对象 |
| 3 | 字面量 "world" 入池 |
常量池 "world"(如果不存在) |
| 4 | new String("world") |
堆上 String 对象 |
| 5 | + 拼接操作 |
底层用 StringBuilder,最终 toString() 创建堆上 String 对象 |
最坏情况:5 个对象(2 个常量池 + 2 个 new + 1 个拼接结果)
注意:StringBuilder 本身也是对象,但它是中间过程,通常不计入"这行代码创建了几个对象"的答案中。
六、intern() 方法:手动入池
intern() 是 String 类的一个 native 方法,作用是:
- 如果常量池中已存在该字符串,返回常量池中的引用
- 如果不存在,将该字符串放入常量池,并返回引用
java
String s1 = new String("hello"); // 堆上对象
String s2 = s1.intern(); // 返回常量池中的 "hello"
String s3 = "hello"; // 常量池中的 "hello"
System.out.println(s1 == s2); // false(s1 在堆,s2 在常量池)
System.out.println(s2 == s3); // true(都指向常量池中的同一个对象)
intern 的经典面试题(JDK 7+)
java
String s1 = new String("a") + new String("b");
// 此时常量池中有 "a" 和 "b",但没有 "ab"
// s1 指向堆上的 "ab" 对象
s1.intern();
// JDK 7+:常量池中存储的是堆上 "ab" 对象的引用(不需要再拷贝一份)
String s2 = "ab";
// "ab" 在常量池中已存在(是 s1 的引用),所以 s2 也指向同一个对象
System.out.println(s1 == s2); // JDK 7+ : true
// JDK 6 : false(常量池在永久代,存的是拷贝)
七、JDK 6 vs JDK 7+:常量池的位置变化
这是理解 String 内存模型的关键背景:
| 版本 | 常量池位置 | intern() 行为 |
|---|---|---|
| JDK 6 及之前 | 永久代(PermGen) | 把字符串复制一份到永久代 |
| JDK 7 及之后 | 堆(Heap) | 把堆上对象的引用存入常量池 |
这个变化直接导致了 s1 == s2 在不同 JDK 版本下结果不同。JDK 7 以后,常量池不再拷贝字符串内容,而是直接记录引用,避免了重复存储。
八、面试速答模板
Q:new String("hello") 创建了几个对象?
A:1 或 2 个。如果常量池中已有 "hello",只在堆上创建 1 个 String 对象;如果常量池中没有 "hello",会先在常量池创建 1 个,再在堆上 new 1 个,共 2 个。关键点是
new一定会在堆上创建新对象,而字面量是否入池取决于之前是否已经出现过。
Q:String s = new String("a") + new String("b") 创建了几个对象?A:最多 5 个------常量池中 "a" 和 "b" 各 1 个(如果不存在),堆上
new String("a")和new String("b")各 1 个,拼接结果的堆上对象 1 个。拼接底层使用 StringBuilder 的toString()方法。
Q:intern() 方法的作用?A:将字符串放入常量池。如果常量池已有则返回常量池引用;如果没有,JDK 6 会复制字符串到永久代,JDK 7+ 会记录堆上对象的引用。这也是为什么 JDK 7+ 中
new String("a")+new String("b")后调用 intern,再用字面量赋值,两个引用会指向同一个对象。
相关文章
内容有帮助?点赞、收藏、关注三连!评论区等你 💪