String 在内存里到底是怎么存的——我的学习笔记
说在前面前面三篇写完了 JVM 五大区域按计划下一篇应该是垃圾回收。但我学到这里的时候发现有一个绕不开的东西——String 的存储机制。因为学 GC 之前得先知道对象在堆里怎么分配的而 String 又特别典型它既有对象实例在堆里又有字面量在常量池里还牵扯到两个对象还是一个新对象的问题。所以插了一篇专门搞明白 String 到底在内存里是怎么待着的。先问你一个问题Strings1abc;Strings2newString(abc);这两行代码s1 和 s2 指向的是同一个对象吗我之前一直以为是一样的——不都是abc吗结果答案是不一样。s1 指向常量池里的 “abc”s2 指向堆里 new 出来的实例。这个事当时颠覆了我对 String 的认知。这篇就是围绕这个问题展开的。字符串字面量存在哪里先说结论字符串字面量存在字符串常量池里。什么叫字面量就是你在代码里直接写的那些用双引号包起来的字符串比如helloabcJVM 真的很难学这些字符串在编译的时候就会被编译器收进 class 文件的常量池里。等到类加载之后它们会被放到字符串常量池中。常量池的位置变过有一点挺有意思——字符串常量池的位置在 JDK 7 的时候换过一次JDK 版本字符串常量池的位置JDK 6 及之前方法区永久代JDK 7 起堆中为什么要搬因为永久代PermGen的空间是有限且固定的字符串常量池里的字符串太多的话容易把永久代撑爆抛OutOfMemoryError: PermGen space。搬到堆里之后堆空间可以动态扩展而且 GC 也能更好地管理它。new String(abc)到底干了什么这个是我觉得最值得搞明白的地方也是面试常问的题。看这行代码StringsnewString(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 字面量 │ │ │ └───────────────────┘ │ └──────────────────────────┘注意箭头的方向s→new String(abc)实例 → 常量池中的abc作为内部 char[] 引用。那我怎么验证我学的时候写了一段代码验证了一下Strings1abc;Strings2newString(abc);System.out.println(s1s2);// falses1 指向常量池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对象所有引用都指向它。这就是为什么前面那个例子可以用判断Stringxabc;Stringyabc;System.out.println(xy);// true指向同一个常量池对象3. 动态性常量池不是编译完就定死了程序跑起来之后也可以往里面加东西。最典型的例子是String.intern()方法StringsnewString(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 三大内存区域。学完前三篇再来看这个确实有一种哦原来如此的感觉。