【Java】字符串底层与常量池演变全解析

发布时间:2026/7/7 9:25:17

【Java】字符串底层与常量池演变全解析 一、String底层存放的是字节数组还是字符数组结论要看 JDK 版本JDK 8 及之前String底层是 字符数组char[] value。JDK 9 及之后String底层是 字节数组byte[] value再配合一个编码标识字段coder。1.JDK 7/8String底层是 char[]在 JDK 7/8 的java.lang.String源码里可以看到/** The value is used for character storage. */privatefinalcharvalue[];这说明每个字符是一个 16 位的charUTF-16 code unit所以底层就是字符数组。2.JDK 9String底层改成 byte[]Compact Strings紧凑字符串从 JDK9 开始官方通过 JEP 254 把String的内部表示从UTF-16char[]改成byte[] 编码标志。在新的String源码中可以看到Stableprivatefinalbyte[]value;privatefinalbytecoder;工作方式大致是如果字符串中的每个字符都能用 Latin-1ISO-8859-1表示就 一个字节存一个字符。否则使用 UTF-16这时每个字符占两个字节仍按 UTF-16 编码存到byte[]里。coder字段就是用来标记当前使用的是哪种编码HotSpot 和许多字符串相关 API 都会根据这个标志做相应处理。3.怎么理解“字节数组还是字符数组”从语义层面看String始终代表“字符序列”对外 API 都是基于char、int codePoint、CharSequence等。底层实现早期用char[]直接存 UTF-16 码元JDK 9 以后为了节省内存改成用byte[]编码标识对用户透明。所以如果面试或考试问String 底层存放的是字节数组还是字符数组比较严谨的回答是JDK 8 及之前底层是字符数组char[]。JDK 9 及之后底层是字节数组byte[]配合编码标记Compact Strings紧凑字符串。二、字符串常量池的位置不同JDK版本下字符串常量池的位置1.详细演变过程与原因1.JDK 1.6及之前永久代时期在这个阶段字符串常量池位于方法区在 HotSpot热点虚拟机中的具体实现是永久代PermGen。存储内容池中存储的是字符串对象实例本身。痛点永久代大小固定通过-XX:MaxPermSize设定且垃圾回收频率极低通常只在Full GC时回收。而程序中字符串的创建和销毁非常频繁极易导致java.lang.OutOfMemoryError: PermGen space错误。2.JDK 7关键转折点从 JDK 7 开始字符串常量池被移动到了 Java 堆Heap中。这是一个重要的优化。存储内容变化池中不再存储对象实例而是存储字符串对象的引用。实际的字符串对象仍然在堆中分配。移动原因堆的空间更大且垃圾回收尤其是新生代非常频繁。将字符串常量池移入堆中可以极大提升无用字符串的回收效率有效缓解了永久代溢出的问题。3.JDK 8及以后元空间时期在 JDK 8 中永久代被彻底废除取而代之的是使用本地内存的元空间Metaspace。但字符串常量池的位置没有改变仍然在 Java 堆中。逻辑与物理的区分这是一个容易混淆的地方。根据《Java虚拟机规范》方法区是一个逻辑概念。在 JDK 8 中字符串常量池在逻辑上属于方法区但在物理实现上位于 Java 堆中。因此“在堆中”和“在方法区”两种说法它们从不同角度描述都是对的这需要我们区分逻辑规范与具体实现。2.核心结论与对比特性JDK 1.6及之前JDK 7JDK 8位置方法区永久代Java堆Java堆存储内容字符串对象实例字符串对象的引用字符串对象的引用方法区实现永久代JVM内存永久代JVM内存元空间本地内存主要优势-回收效率高避免PermGen OOM堆空间更灵活元空间不受JVM堆限制❓延伸理解运行时常量池 vs 字符串常量池运行时常量池是方法区的一部分。它用于存放编译期生成的各种字面量和符号引用。每个类加载后都会有一个运行时常量池。字符串常量池是 JVM 专门为字符串优化提供的独立区域。它全局唯一所有类共享。在 JDK 1.6 及之前字符串常量池是运行时常量池的一部分。从 JDK 7 开始字符串常量池被独立出来并移到堆中而运行时常量池仍然留在方法区JDK 8 后是元空间。3.JDK字符串内存管理的核心设计字符串常量池和字符串对象实例都位于 Java 堆中但它们是堆中两个不同区域的概念。JDK 7及以后版本中一个字符串字面量如何被创建和引用的完整过程详细解读这个流程中的每一个环节。核心解答实例在哪里根据多个权威资料结论非常明确字符串常量池的位置从 JDK 7 开始字符串常量池被从方法区永久代移动到了 Java 堆中。常量池中存储的内容在 JDK 7 及之后字符串常量池中存储的是字符串对象的引用而不是对象实例本身。字符串对象实例的位置真正的字符串对象实例即String对象包含字符数组等数据同样存储在 Java 堆中。因此可以这样理解字符串常量池是堆中的一个“引用列表”它指向了堆中其他位置实际存在的字符串对象实例。演进对比JDK版本差异特性JDK 1.6及之前JDK 7JDK 8及之后字符串常量池位置方法区永久代Java堆Java堆常量池存储内容字符串对象实例字符串对象的引用字符串对象的引用字符串对象实例位置在常量池永久代中在Java堆中在Java堆中方法区实现永久代PermGen永久代元空间Metaspaceintern()行为若池中无则复制对象实例到池中若池中无则将堆中对象的引用存入池中与JDK 7相同设计初衷为何要移动将字符串常量池从永久代移至堆并非简单的位置调整而是为了解决根本性问题解决永久代溢出永久代大小固定由-XX:MaxPermSize设定且垃圾回收效率极低。而程序中字符串极其常见大量使用intern()或字面量容易导致java.lang.OutOfMemoryError: PermGen space。移至堆后其大小仅受堆内存限制极大降低了此风险。提升垃圾回收效率堆是垃圾回收的主要区域拥有成熟的分代回收算法。将字符串常量池移至堆使得池中引用的字符串对象可以被更高效地回收优化了内存管理。深入理解创建与引用过程通过两种常见创建方式来彻底理解对象实例与引用的关系1.字面量方式创建Strings1abc;Strings2abc;执行流程如下JVM 解析到字面量abc首先检查字符串常量池。若池中不存在abc的引用则在堆中创建一个字符串对象实例其内容为abc。将这个对象实例的引用存入字符串常量池。将这个引用返回给变量s1。当创建s2时常量池中已存在abc的引用因此直接将该引用返回给s2。结果s1和s2指向堆中同一个字符串对象实例。2.new关键字创建Strings3newString(abc);执行流程如下JVM 同样会先检查常量池中是否有abc的引用。如果没有会按照上述流程在堆中创建一个对象实例并将引用存入池中。关键在于new关键字会在堆中单独创建一个新的、内容相同this.value original.value和常量池对象共享同一份底层数组的字符串对象实例。将这个新对象实例的引用返回给变量s3。结果s3指向堆中一个新创建的对象实例而常量池中的引用指向另一个实例。s1 s3的结果为false。✅总结总而言之在 JDK 7 及之后的版本中字符串常量池是 Java 堆中一个用于存储字符串对象引用的哈希表。字符串对象实例同样是存储在 Java 堆中它们被常量池中的引用所指向。这种设计将容易导致内存溢出的常量池从空间受限的永久代解放出来并利用了堆更高效的垃圾回收机制是 JVM 内部优化的重要一环。理解这个区别对于排查内存溢出问题以及优化字符串使用如合理使用intern()方法至关重要。4.面试题创建了几个对象解释一下核心机制编译器优化与常量池的写入规则。核心机制Class 文件常量池Java 源代码经过编译生成.class文件时编译器不仅会将代码翻译成字节码还会在.class文件中生成一个常量池。这个常量池类似于一个“资源清单”里面记录了代码中用到的所有字面量和符号引用。关键规则如果编译器判断某个字符串字面量只是中间产物仅用于拼接且后续代码中没有独立使用它编译器会优化掉它不会把它写入.class文件的常量池中。面试题一Stringaabcbcd;编译期分析编译器发现abc和bcd是字面量拼接直接计算出结果abcbcd。写入常量池编译器将最终结果abcbcd写入.class文件常量池。中间产物处理由于abc和bcd在代码其他地方没有被使用编译器认为它们是临时变量不写入常量池。运行期JVM 加载类时只看到常量池里有abcbcd于是只在堆中创建了 1 个对象。结论只创建 1 个对象。面试题二Strings1abcdef;Strings2abcdefxy;第一行String s1 abcdef;字面量abcdef被写入.class常量池。运行时创建对象abcdef。第二行String s2 abcdef xy;编译期优化编译器识别出这是两个常量的拼接直接计算结果abcdefxy。写入常量池编译器将最终结果abcdefxy写入.class常量池。中间产物处理这里的abcdef虽然在代码里出现了两次第1行和第2行但第2行中是作为拼接操作的一部分。关键点在于因为第1行已经强制使用了abcdef所以常量池里一定会有。但是xy作为拼接的中间部分且代码中没有独立使用它编译器不会将其写入常量池。运行期JVM 加载类解析常量池。创建对象abcdef对应 s1。创建对象abcdefxy对应 s2。不存在xy对象。结论总共创建了 2 个对象。面试题三Stringbabc;Stringabbcd;在这段代码中内存中总共创建了 4 个对象。详细解析过程第 1 行String b abc;JVM 检查字符串常量池发现没有abc。在字符串常量池中创建对象abc。变量b指向池中的abc。当前对象数1 个常量池中的abc第 2 行String a b bcd;这里是问题的关键因为b是一个变量即使它的值没有变但在编译器眼里它是变量编译器无法在编译期确定最终的结果因此无法进行常量折叠优化。Java 会将其转换为StringBuilder进行处理。代码在底层执行逻辑等价于// 伪代码演示底层逻辑StringBuildertempnewStringBuilder();// 创建对象2temp.append(b);// 使用已有的abctemp.append(bcd);// 这里的bcd是常量需要加载对象3Stringatemp.toString();// 创建对象4细究这一步创建的对象对象2bcd代码中出现了字面量bcd。因为是运行时需要使用它所以它会作为类常量池的一部分在类加载或执行时被解析并创建在字符串常量池中。对象3StringBuilder对象这是在堆中创建的一个普通对象用于拼接字符串。对象4String对象abcbcdStringBuilder调用toString()方法时底层是调用new String(char value[], int offset, int count)。这个对象存放在堆中注意它不在字符串常量池中。变量a指向这个堆中的对象。总结与对比总共 4 个对象位置和类型如下序号对象值位置说明1abc字符串常量池第1行代码创建2bcd字符串常量池第2行代码中的字面量3StringBuilder实例堆用于拼接的中间工具对象4abcbcd堆最终拼接结果toString产生不在常量池核心考点常量折叠 vs 动态拼接a b常量常量- 编译期优化 - 结果在常量池。变量 b变量常量- 运行时处理 - 结果在堆。StringBuilder.toString()通过StringBuilder生成的 String 对象默认是在堆内存中不会自动放入常量池。字面量的加载只要代码里写了bcd运行时需要用它它就会在常量池里不管是否被优化掉取决于它是否参与运行。在这个例子中bcd是append的参数必须存在所以常量池里会有它。new String() 和 StringBuilder.toString() 的实现原理1.显式调用new String()StringanewString(abcbcd);这会创建 2 个对象堆对象new关键字在堆上创建的对象。常量池对象构造函数new String(String s)接收一个字符串参数。如果常量池里没有abcbcdJVM 会解析这个字面量并将其放入常量池或者作为参数传入时已经确认常量池存在。2.StringBuilder.toString()的源码Stringbabc;Stringabbcd;编译器将其转换为StringBuilder最后调用的是toString()方法。请看 JDK 中StringBuilder.toString()的源码JDK 8 及之后OverridepublicStringtoString(){// Create a copy, dont share the arrayreturnnewString(value,0,count);}关键点来了这里调用的new String(value, 0, count)构造函数并不是我们常用的new String(String str)构造函数。它调用的是下面这个构造函数接收一个char[]数组publicString(charvalue[],intoffset,intcount){// ... 省略边界检查 ...this.valueArrays.copyOfRange(value,offset,offsetcount);}3.为什么不会在常量池创建对象这个构造函数的逻辑非常简单接收一个字符数组value。把这个数组复制一份赋值给当前 String 对象的内部属性this.value。注意在这个过程中它接收的参数是字符数组而不是 字符串字面量。它的内部逻辑没有任何代码去检查或操作字符串常量池。它仅仅是堆内存上的对象实例化。因此StringBuilder.toString()产生的abcbcd对象纯粹是一个堆上的对象它不会自动在常量池里创建对应的字符串。只有当你显式调用intern()方法时它才会去常量池排队。总结对比场景代码示例涉及常量池吗创建对象数显式 new String(字面量)new String(xyz)是。构造函数需要解析字面量xyz。2个堆 池StringBuilder.toString()new StringBuilder().append(x)...toString()否。底层调用new String(char[], ...)1个仅堆所以在String a b bcd;这一步中StringBuilder生成的abcbcd只创建了 1 个对象位于堆不在常量池。加上前面的abc、bcd和StringBuilder实例总共 4 个对象。对这两方法的底层原理进行学习这正是区分“语法糖”和“底层实现”的关键所在

相关新闻