首页 > 编程开发 > Java    日期:2026-07-03 / 浏览

说在前面:

前面三篇写完了 JVM 五大区域,按计划下一篇应该是垃圾回收。但我学到这里的时候,发现有一个绕不开的东西——String 的存储机制。因为学 GC 之前得先知道对象在堆里怎么分配的,而 String 又特别典型:它既有对象实例在堆里,又有字面量在常量池里,还牵扯到两个对象还是一个新对象的问题。所以插了一篇,专门搞明白 String 到底在内存里是怎么待着的。

先问你一个问题

String s1 = "abc";
String s2 = new String("abc");

这两行代码,s1 和 s2 指向的是同一个对象吗?

我之前一直以为是一样的——不都是"abc"吗?结果答案是:不一样。 s1 指向常量池里的 “abc”,s2 指向堆里 new 出来的实例。

这个事当时颠覆了我对 String 的认知。这篇就是围绕这个问题展开的。

字符串字面量存在哪里

先说结论:字符串字面量存在字符串常量池里。

什么叫字面量?就是你在代码里直接写的那些用双引号包起来的字符串,比如:

"hello"
"abc"
"JVM 真的很难学"

这些字符串在编译的时候就会被编译器收进 class 文件的常量池里。等到类加载之后,它们会被放到 字符串常量池 中。

常量池的位置变过

有一点挺有意思——字符串常量池的位置在 JDK 7 的时候换过一次:

JDK 版本 字符串常量池的位置
JDK 6 及之前 方法区(永久代)
JDK 7 起 堆中

为什么要搬?因为永久代(PermGen)的空间是有限且固定的,字符串常量池里的字符串太多的话,容易把永久代撑爆,抛 OutOfMemoryError: PermGen space。搬到堆里之后,堆空间可以动态扩展,而且 GC 也能更好地管理它。

new String("abc")到底干了什么

这个是我觉得最值得搞明白的地方,也是面试常问的题。

看这行代码:

String s = new String("abc");

很多人以为它就创建了一个对象。实际上最多创建了两个对象

我拆一下步骤:

第一步:new 指令在堆里创建一个 String 实例

new 关键字干的事就是在堆上分配内存,创建一个 String 对象。这个对象是"空壳"——它的值还没有确定。

第二步:JVM 拿着字面量去常量池里找

因为构造方法里传了 "abc" 这个字符串字面量,JVM 会去字符串常量池里找一找,有没有 "abc" 这个字符串。

这时候有两种情况:

情况一:常量池里之前没有 “abc”

那 JVM 就在常量池里创建一个 "abc" 对象。

这时候就有两个对象了:一个是常量池里的 "abc",一个是堆里的 new String() 实例。后者会引用前者的值。

情况二:常量池里已经有 “abc” 了

那就不创建新的了,直接复用已有的。这时候只创建了一个对象——就是堆里那个 new String() 实例。

第三步:引用 s 存在当前方法的栈帧里

不管创建了几个对象,最终 s 这个引用存在当前方法的栈帧里,指向堆里的那个 String 实例,不是直接指向常量池里的 “abc”

一张图看清楚

栈(Stack)               堆(Heap)
┌──────────┐            ┌──────────────────────────┐
│ s (引用)  │ ────────→ │  new String("abc") 实例   │
└──────────┘            │                         │
                        │  字符串常量池(堆中)     │
                        │  ┌───────────────────┐  │
                        │  │    "abc" 字面量    │  │
                        │  └───────────────────┘  │
                        └──────────────────────────┘

注意箭头的方向:snew String("abc") 实例 → 常量池中的 "abc"(作为内部 char[] 引用)。

那我怎么验证?

我学的时候写了一段代码验证了一下:

String s1 = "abc";
String s2 = new String("abc");

System.out.println(s1 == s2);       // false:s1 指向常量池,s2 指向堆
System.out.println(s1.equals(s2));  // true:内容是一样的

s1 == s2 是 false,说明它们不是同一个对象——一个在常量池里,一个在堆里。

s1.equals(s2) 是 true,因为 equals() 比较的是字符串的内容,不是引用地址。

字符串常量池的四个关键特性

学完之后我把字符串常量池的特性总结了四条:

1. 不可变性

String 对象一旦创建,它的值就不能改了。你做的任何"修改"操作(比如 concat()replace()substring())都是创建了一个新对象,原来的字符串不变。

我之前不理解为什么 String 要设计成不可变的。后来才知道,不可变是常量池能工作的前提——如果字符串可变,两个引用共享同一个 “abc”,其中一个改了,另一个就乱了。

2. 共享性

相同的字符串字面量在常量池里只存一份。不管你在代码里写了多少次 "hello",常量池里只会有一个 "hello" 对象,所有引用都指向它。

这就是为什么前面那个例子可以用 == 判断:

String x = "abc";
String y = "abc";
System.out.println(x == y);  // true:指向同一个常量池对象

3. 动态性

常量池不是编译完就定死了,程序跑起来之后也可以往里面加东西。

最典型的例子是 String.intern() 方法:

String s = new String("abc").intern();

intern() 的作用是:去常量池里找有没有相同内容的字符串,如果有就返回常量池里的那个引用;如果没有,就把当前字符串的内容放到常量池里。

intern() 这个方法的本质是:手动把堆里的字符串"注册"到常量池里。

4. 位置挪到了堆里

JDK 7 之前常量池在永久代,JDK 7 之后挪到了堆里。这样做的直接好处是:

  • 堆空间可以动态扩展,不用担心 PermGen 太小
  • 堆的 GC 机制更强,常量池里的无用字符串可以被回收(虽然概率不高,但至少有了这个能力)

一个让我懵了好久的细节

学这篇的时候有一个细节我纠结了很久:new String("abc") 创建的两个对象,在内存里到底是什么关系?

我画了一个更详细的图,帮自己理解:

栈                    堆
┌─────────┐         ┌────────────────────────────┐
│ s        │ ──────→│ new String (value = char[]) │
│ (引用)   │         │  ┌──────────────────────┐  │
└─────────┘         │  │ char[] 引用 ────────→ │  │
                    │  └──────────────────────┘  │
                    │                            │
                    │ 字符串常量池               │
                    │  ┌──────────────────────┐  │
                    │  │ "abc" → char[]对象    │  │
                    │  └──────────────────────┘  │
                    └────────────────────────────┘

new 出来的 String 对象内部有一个 value 字段(char[] 类型),它引用的就是常量池里那个 "abc" 的底层 char 数组。

所以整个链路是:s(栈引用)→ new String 实例(堆对象)→ 共享的 char[] 数组(常量池的底层数据)。

也就是说,常量池里的 "abc" 和 new 出来的 String,共享了底层的字符数据。常量池存了一份 char[],堆里的对象拿着一个引用指向它,不再重复存一份内容。

这个机制我之前完全不知道。了解了之后再看 “两个对象” 这个说法,才真正理解了是哪两个对象。

最后串一下:和前面几篇的关系

学到这里我发现,这一篇其实是把前面三篇的知识点串起来了:

  • ——方法里的 String 引用存在栈帧里
  • ——new String() 的实例存在堆里
  • 方法区/元空间——类的字节码信息、class 文件常量池在这个区域
  • 字符串常量池——JDK 7+ 在堆里,存字符串字面量

一个简单的 String s = new String("abc"),牵扯到了 JVM 三大内存区域。学完前三篇再来看这个,确实有一种"哦原来如此"的感觉。

觉得上面的内容有用吗?快来点个赞吧!

点赞() 我要打赏

温馨提示 : 本站内容来自会员投稿以及互联网,所有源码及教程均为作者总结编辑,请大家在使用过程中提前做好备份,以免发生无法预知的错误,源码类教程请勿直接用于生产环境!

 可能感兴趣的文章