String str="abc" 和 new String("abc") 核心区别+内存存储详解(JDK7+)
先明确核心结论:首次执行时堆中会生成两个不同的String对象;非首次执行(常量池已有"abc"引用)时,堆中仅生成1个新的String对象。二者的核心差异体现在栈引用指向、堆对象创建规则、内存复用性三个维度,以下结合内存存储细节、对象数量逐一拆解,所有分析基于目前主流的JDK7+版本(常量池位于堆区,存储字符串对象引用)。
一、String str = "abc":字面量创建,堆中仅1个对象+常量池复用
这是JVM推荐的字符串创建方式,核心遵循常量池唯一引用机制,全程堆中仅创建1个字符串对象,具体内存存储流程:
- JVM首先检查堆区的字符串常量池(String Table),判断是否存在"abc"的字符串引用;
- 首次执行时,常量池无对应引用,JVM在堆区的普通内存区域创建1个真正的"abc"字符串对象;
- 将堆中这个"abc"对象的引用地址存入字符串常量池(哈希表结构,实现引用唯一);
- 栈区的引用变量
str,直接指向字符串常量池中的"abc"引用,最终间接指向堆中的唯一"abc"对象。
核心特点:堆中仅1个对象,所有后续声明String str2="abc"的栈引用,都会直接复用常量池中的引用,不会在堆中新建任何对象,实现内存最大化复用。
二、new String("abc"):构造方法创建,堆中至少1个、最多2个对象
这是强制在堆中新建对象的方式,栈引用永远不指向常量池,直接指向堆中新对象,具体内存存储流程(分两种场景):
场景1:首次执行(常量池无"abc"引用)→ 堆中2个独立String对象
这是你问题中“堆中是否是两个对象”的核心场景,步骤如下:
- JVM先检查字符串常量池,无"abc"引用,触发字面量创建的默认流程:在堆区创建第1个"abc"字符串对象,并将其引用存入常量池;
- 执行
new String("abc"),JVM在堆区的另一块内存区域,创建第2个全新的String对象(该对象与第1个对象共享底层的字符数组(char[]/byte[]),但二者是堆中独立的对象,内存地址不同); - 栈区的引用变量
str,直接指向堆中这个新创建的第2个String对象,与常量池无直接关联。
场景2:非首次执行(常量池已有"abc"引用)→ 堆中仅1个新String对象
常量池已有"abc"的引用(堆中已有第1个"abc"对象),JVM会跳过字面量创建流程,直接执行new String("abc"),在堆区新建1个独立的String对象,栈引用直接指向该新对象。
核心特点:无论常量池是否有对应引用,堆中必然会新建1个String对象;仅当常量池无引用时,会因“兜底创建字面量对象”导致堆中出现2个对象,且两个对象始终是堆中独立实例。
三、二者核心区别对比表(JDK7+)
为了更清晰区分,从栈引用指向、堆对象数量、内存复用性、==判断结果四个核心维度总结,对比场景为首次执行(常量池无"abc"):
| 特性 | String str = "abc"(字面量) | new String("abc")(构造方法) |
|---|---|---|
| 栈引用指向 | 指向字符串常量池的"abc"引用 | 直接指向堆区的新String对象 |
| 堆中对象数量 | 仅1个 | 2个(独立实例,共享底层字符数组) |
| 内存复用性 | 极致复用,后续创建直接复用常量池引用 | 无复用,每次new都在堆中新建对象,内存冗余 |
| 与字面量==判断 | str == "abc" → true(均指向常量池引用) | str == "abc" → false(一个指向堆,一个指向常量池) |
| 底层字符数组 | 独用(堆中仅1个对象) | 与常量池对应的堆对象共享 |
四、关键补充:易混淆的核心细节
1. 堆中两个对象的“共享”与“独立”
new String("abc")创建的新对象,与常量池对应的堆对象,仅共享底层存储字符的数组(char[]/byte[]),但String对象本身是完全独立的:二者的hashCode()(重写后与字符内容一致)相同,但==判断为false(内存地址不同),equals判断为true(内容相同)。
2. 为何JVM不推荐使用new String("abc")?
唯一的问题是内存冗余:堆中出现重复的String对象,且栈引用无法复用常量池,若大量使用会导致堆内存中充斥重复字符串对象,增加GC压力,甚至可能引发OOM,仅在极少数需要“独立字符串对象”的场景才会使用。
3. 如何让new String("abc")的对象实现常量池复用?
可通过手动调用intern()方法,将堆中新对象的引用存入常量池,实现复用:
String str = new String("abc").intern(); // 调用intern后,栈引用指向常量池引用
String str2 = "abc";
System.out.println(str == str2); // true:二者均指向常量池同一引用
五、极简内存模型示意图(首次执行场景)
用简单的层级关系展示三者(栈、常量池、堆)的关联,直观理解存储差异:
1. String str = "abc" 的内存模型
栈区:str → 常量池区:"abc"引用 → 堆区:唯一的"abc"字符串对象(char[]={'a','b','c'})
2. new String("abc") 的内存模型
栈区:str → 堆区:新String对象(共享char[])
常量池区:"abc"引用 → 堆区:原"abc"字符串对象(char[]={'a','b','c'})
(堆中两个String对象共享同一个char[],但对象本身是独立的两个内存地址)
总结
- 首次执行时,堆中是两个独立的String对象(字面量的原对象+new的新对象),非首次执行时堆中仅1个new的新对象;
- 字面量创建的栈引用指向常量池,new创建的栈引用直接指向堆,这是二者最核心的区别;
- 字面量方式天然实现内存复用,new方式必然创建堆新对象,无复用性,日常开发优先使用字面量创建字符串;
- 堆中所有字符串对象的本体始终唯一存储在堆区,常量池仅存储堆对象的引用(JDK7+),不会存储对象本身。

浙公网安备 33010602011771号