商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > Java中String在内存里到底是怎么存的

Java中String在内存里到底是怎么存的

  发布于2026-07-04 阅读(0)

扫一扫,手机访问

说在前面:

关于JVM的五个核心数据区域,前面已经聊了三块内容。按照计划,接下来应该讲垃圾回收了。但学到这里时,发现有一个知识点绕不过去——String的存储机制。原因很简单:要搞懂GC,先得弄明白对象在堆里是怎么分配的。而String恰好是个典型例子——它既有对象实例在堆里,又有字面量待在常量池中,还牵扯到“两个对象还是一个新对象”这类经典问题。所以,趁这个机会插一篇文章,专门把String在内存里的位置彻底捋清楚。

Ja va中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三大内存区域。学完前三篇再回头看这个,确实有一种“哦,原来如此”的感觉。

本文转载于:https://www.jb51.net/program/3666886ry.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注