ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

new String(“abc“)创建几个对象?从JVM字符串常量池到intern全解析

new String(“abc“)创建几个对象?从JVM字符串常量池到intern全解析 1. 一道送分题先看它到底在考什么作为Java面试题里的常青树String str new String(abc)创建了几个对象这个问题几乎出现在每一轮后端面试中。你可能会想这不就是背个答案吗常量池里一个堆里一个所以是两个。但真到了面试现场很多候选人连第一层都答不完整——更不用说面试官接下来抛出的那些变体了。我自己当面试官的时候经常拿这道题来测候选人的JVM基础和语言底层功底十个人里有八个能说出“两个对象”但能讲清楚为什么、能意识到答案需要分情况讨论的人最后往往都拿到了offer。今天我就以这道题为主线把字符串常量池、对象创建过程和intern()机制一次讲透。别小看这一道题它背后覆盖了JVM运行时数据区、类加载解析阶段、字节码指令、编译期常量折叠、String内存模型等多个核心知识点。更重要的是理解透这段简单代码能直接提升你在实际项目中处理高频字符串操作、排查内存问题时的决策质量。所以这篇文章不只是喂给你一个“标准答案”而是要把答案背后的逻辑链条完整展开。1.1 面试官问这道题的真正意图面试官问这个问题通常不是单纯为了考察你是否记住了答案。一个在简历上写“熟悉Java”或者“了解JVM”的候选人如果连这道经典题都回答得支支吾吾那基本可以判断某些内容是包装出来的。反过来如果候选人能主动联想到字节码指令、字符串常量池的存放位置、JDK 7的永久代移除、JDK 9的Compact Strings等知识点那大概率是真正读过相关源码或者系统性学过JVM。我通常在候选人答完“两个对象”之后会紧接着追问几个问题那String s abc创建了几个对象如果此前代码里已经执行过String ss abc现在再执行String str new String(abc)又创建了几个对象new String(abc).intern()和直接赋值有什么区别在JDK 6和JDK 8下你的答案会有变化吗这些问题环环相扣目的不是为难谁而是看候选人有没有真正理解字符串在JVM中的管理机制。所以不要把这题当成八股文去背而是要当成一个知识网络的入口。真正理解了原理花式变体都可以从容应对。1.2 这道题的标准答案第一步先把常量池讲清楚先给出大家最熟悉的版本也是很多技术博客里写的标准答案如果此时字符串常量池中还没有“abc”这个字符串执行String str new String(abc)会创建2个对象一个是编译期确定的、驻留在字符串常量池中的“abc”字符串对象。一个是运行期在堆中通过new创建出来的String对象。如果字符串常量池中已经有“abc”比如前面代码已经出现过字面量abc那么只会创建1个对象也就是new出来的那个String对象。但这个答案有一个非常容易被忽略的前提——abc这个字面量到底在什么时候被放入常量池是执行到new String(abc)这一行才放进去还是在更早的类加载阶段就已经完成了答案是更早。类加载的解析阶段JVM会将class文件常量池中的符号引用替换为直接引用字符串字面量在解析阶段就会尝试驻留到字符串常量池。也就是说在new String(abc)这行代码真正执行之前只要包含这行代码的类被加载过“abc”的驻留工作往往就已经完成了。这里再补充一个很常见的细节当我们说“字符串常量池”时在JDK 7之前的HotSpot虚拟机里它位于方法区永久代中JDK 7开始被移动到Java堆中JDK 8之后方法区被元空间取代但字符串常量池仍然在堆中。这个位置变化对“创建了几个对象”的理解影响很大后面我专门用一节来讲版本差异。2. 核心拆解从字节码和JVM内存看对象创建的整个过程我觉得最有说服力的方式不是上来就背结论而是直接看字节码。一行String str new String(abc)在JVM里到底执行了什么比任何口头解释都更清楚。2.1 一行代码背后的字节码指令先写一段最简单的代码public class StringCreate { public static void main(String[] args) { String str new String(abc); } }编译成class文件后用javap -c StringCreate反编译main方法里的字节码长这样0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String abc 6: invokespecial #4 // Method java/lang/String.init:(Ljava/lang/String;)V 9: astore_1 10: return这里有四个关键指令分别对应不同的动作new在堆中分配一块内存创建了一个String对象实例。注意此时对象还没有调用构造方法字段值都还是默认状态。dup复制操作数栈栈顶的引用。为什么要复制因为后面调用构造方法invokespecial会消耗掉一个引用但赋值给局部变量还需要一个引用所以先复制一份备用。ldc将常量池中的字符串“abc”的引用推送到操作数栈顶。这里就是字符串常量池参与的关键点。如果常量池中还没有“abc”ldc指令会触发字符串驻留在常量池中创建对应的字符串实例如果已经有了就直接复用已有引用。invokespecial调用String的构造方法init也就是String(String original)传入刚才从常量池拿到的“abc”引用。从构造方法的语义来看new String(abc)本质上是根据传入字符串的内容在堆中创建一个新的String对象。这也是为什么new String(abc).equals(abc)返回true但new String(abc) abc返回false——前者是内容比较后者是引用比较两个对象根本不是同一个。2.2 两个“对象”的诞生现场常量池 vs 堆“abc”这个字面量到底算不算一个对象严格从Java语言层面看字符串字面量也是String对象只是它被JVM特殊管理了。ldc指令执行时如果字符串常量池中找不到对应的字符串JVM会调用StringTable::intern相关逻辑在字符串常量池中创建该字符串对象并返回引用如果找到了就直接复用。所以第一次执行这行代码时的完整流程可以概括为四步类加载阶段“abc”这个字符串字面量已经通过解析过程进入运行时常量池的符号引用中。JVM检查字符串常量池中是否存在内容为“abc”的字符串。如果没有则在字符串常量池JDK 7实际在堆中中创建“abc”字符串对象。接着执行new在堆中再创建一个新的String对象构造时用常量池中“abc”的内容作为参数。两个对象都是java.lang.String类型但它们的来源、存放区域和管理方式完全不同。常量池中的对象是全JVM范围共享的所有出现abc字面量的类都会复用它堆里new出来的对象则独立存在不被StringTable管理只属于当前这一行代码产生的实例。用生活化的类比来理解常量池里的“abc”就像是图书馆里的一本共享参考书谁都可以拿来看new String(abc)则是你自己买了一本内容完全一样的新书。书的内容相同但实体不同你拿“图书馆那本”和“自己这本”去比较引用那当然不相等。2.3 new String(abc) 与 String s abc 的差别面试里最常出现的对比就是new String(abc)和String s abc我整理了一张对照表方便直接记忆对比维度String s abcString str new String(abc)对象创建数量最多创建1个常量池中没有时根据常量池的情况常见为1个或2个是否一定新建堆对象否只使用常量池对象是一定在堆中新建一个String对象引用指向指向常量池中的字符串对象指向堆中的String对象内存开销小可复用大多一份堆内存开销与 比较和另一个相同字面量直接用比较是true需要调用intern()才能获得常量池引用很多人觉得new String(abc)是多余的操作其实在业务代码里的确如此。除了极少数需要确保字符串内容不被外部引用共享的场景日常开发中没有任何理由用new去创建字符串。字符串是不可变对象常量池中的字符串可以安全地共享直接赋字面量是更优的选择。我还想提一个容易被忽略的层面new String(abc)内部那个底层的字符数组到底算不算“对象”如果从严谨角度去抠JDK 8及以前String内部是一个char value[]数组构造方法会用Arrays.copyOf把传入字符串的字符内容复制一份到新的char数组JDK 9之后内部改成了byte[]和编码标识coder。也就是说堆里那个新String对象背后还额外对应一个新的数组对象。面试时如果能把这一层也讲出来绝对是大大的加分项说明你真去看过String源码。3. 版本演进带来的“坑”不同JDK下答案会变字符串常量池的位置调整是HotSpot虚拟机历史上一个重要的改动点也是面试官最容易顺着往下深挖的方向。很多人背熟了答案却没意识到在不同JDK版本下这道题的细节答案其实是有差异的。3.1 JDK 6 与 JDK 7 的字符串常量池位置差异在JDK 6及之前HotSpot的字符串常量池存放在方法区PermGen永久代中。永久代有固定的大小上限字符串过多会直接抛出OutOfMemoryError: PermGen space。那时候全网的Java应用经常因为字符串滥用导致PermGen溢出于是就有了经典的调优参数-XX:MaxPermSize。JDK 7开始字符串常量池被移到了Java堆中永久代里不再存放字符串实例。这个移动最直接的影响是字符串对象和普通对象一样受堆空间管理可以通过-Xmx来控制不再受永久代大小限制。另外还有一个隐藏影响String.intern()的行为在不同版本间发生了变化。手动intern的字符串在JDK 6里是在永久代里存放一份由于永久代垃圾回收频率低很容易积累大量无法被回收的字符串导致永久代OOMJDK 7之后移到堆中字符串可以被常规的垃圾回收机制回收情况好了很多。那么问题来了JDK 7之后常量池中的String对象和new出来的String对象都在堆里是否还有本质区别区别依然很大。常量池中的String对象由JVM通过StringTable统一管理可以被所有类共享而new出来的String对象是独立的实例不被StringTable管理除非你显式调用intern()去驻留。这个“共享”语义是理解整个题目的核心。3.2 JDK 9 之后的内部存储变化JDK 9引入了一个叫Compact Strings的特性String内部从char[]改成了byte[]加一个编码标识coder。原来一个字符固定占2个字节UTF-16但如果字符串内容全是Latin-1字符比如abc1个字节就足够白白浪费了一半空间。改版之后纯英文字符串的内存占用直接减少一半。这对面试题的影响有两个点第一如果你在面试时说“String对象内部维护一个char数组”在JDK 9的语境下已经不准确了。面试官很可能追问现在的内部结构你要能说出是byte[] value加上byte coder以及LATIN1和UTF16两种编码模式。第二构造函数复制底层数组的行为也变了。JDK 8之前new String(String original)用Arrays.copyOf复制底层char数组JDK 9则是根据编码情况做不同的复制处理。但不管怎样new出来的String通常会复制传入字符串的内容不会直接把内部数组引用共享出去这是为了不可变性和安全性做的防御性复制。面试时能把这个细节答出来属于真正的加分项。3.3 为什么说“两个对象”的说法在新版本里要谨慎回到最经典的那个答案String str new String(abc)创建了2个对象。严格来讲在JDK 7之前这个说法基本准确在JDK 7之后仍然可以说创建了2个String对象但需要分情况讨论。有一种很多人没意识到的场景如果“abc”这个字符串之前已经在某个类中被使用过比如有人执行过String x abc那么在执行new String(abc)时常量池中已经存在“abc”这行代码就只会在堆中创建一个新的String对象。所以更严谨的回答是取决于常量池中是否已经存在“abc”存在就是1个不存在就是2个。再往深处说“abc”这个字符序列到底是什么时候进入字符串常量池的正常情况下类加载的解析阶段和实际执行ldc指令的阶段都可能发生具体时机跟JVM实现有关。HotSpot默认是惰性解析也就是在ldc指令第一次执行时触发字符串驻留。如果你用了某些JIT参数或者提前触发了类加载时机可能就不一样。对大多数面试来说能讲到“惰性解析”这个层次已经非常优秀了至少说明你不是停留在背答案层面。4. intern() 与字符串驻留把这道题吃透的最后一环聊到字符串常量池就绕不开intern()。这是String类上最容易被拿来扩展提问的方法理解了它你对上面所有内容的理解会更深一层。4.1 intern() 到底做了什么String.intern()的语义是如果字符串常量池中已经有一个字符串和当前字符串内容相等equals返回true就返回常量池中那个字符串的引用否则把当前字符串对象添加到字符串常量池中并返回它的引用。这个方法是理解和验证字符串驻留机制的最好工具。看个例子String s1 abc; String s2 new String(abc); System.out.println(s1 s2); // false System.out.println(s1 s2.intern()); // trues2.intern()返回的是常量池中已有的“abc”引用和s1指向同一个对象所以第二行打印true。很多人第一次看到这个结果会惊讶理解了intern()的语义之后就会觉得理所当然。注意一个反直觉的细节new String(abc).intern()在常量池没有“abc”时到底是在常量池中复制一个新的String对象还是直接把new出来的那个对象放进StringTable这在不同的JVM版本里有微妙差异。JDK 7之前是复制副本到永久代的常量池中JDK 7之后官方文档的表述是如果常量池中没有就把当前字符串对象的引用记录到StringTable中。这也就是为什么在某些特定场景下new String(abc).intern() new String(abc)可能为true前提是第一个intern把对象引用放入了StringTable而普通情况下new String(abc) new String(abc)永远是false。这个细节可以作为面试时的差异化回答。4.2 经典变体题new String(a) new String(b) 创建了几个对象如果面试官看你基础不错想增加难度可能会抛出这样一道变体String str new String(a) new String(b)创建了几个对象这题比原题更进一步考察的是字符串拼接的底层实现。在JDK 8及以前两个字符串用连接javac编译器通常会生成类似这样的代码逻辑StringBuilder sb new StringBuilder(); sb.append(a); sb.append(b); String str sb.toString();注意这里的a和b都是字符串字面量如果此前没出现过会分别被驻留到常量池这就是2个常量池对象。然后new StringBuilder()创建1个StringBuilder对象。append不会创建新的String对象只是往内部缓冲区追加字符。最后的toString()会调用new String(...)在堆中再创建1个String对象。所以最保守的统计是2个常量池对象a和b 1个StringBuilder 1个堆内String一共4个对象。这题的重点在于你要能说出Java编译器对字符串常量拼接的优化规则a b这种字面量拼接在编译期就会直接变成ab不会在运行期做任何拼接操作。而一旦有一方是变量比如new String(a) b就会走StringBuilder。到了JDK 9之后javac还会在某些条件下使用invokedynamic配合StringConcatFactory来做字符串拼接不再固定依赖StringBuilder底层优化空间更大。能把这个差异讲出来说明你对字节码和编译优化是有真实积累的。4.3 日常开发中怎么利用这些知识面试题归面试题这些知识在实际开发中也有直接价值。最常见的应用场景就是高并发接口里大量使用字符串拼接和比较。如果你用new String(id: userId)这种写法每次调用都会创建新对象频繁操作会给GC带来压力字符串对象快速堆积年轻代的GC频率就会上升严重时还会导致对象晋升到老年代带来更频繁的Full GC。我一般在代码规范里会规定这么几条字符串赋值用字面量不要用new String(...)包装。大量字符串拼接优先用StringBuilder不要在循环里用拼接。不要轻易调用intern()除非你有明确的业务语义。比如你需要一个全局唯一的字符串引用映射并且能控制字符串总量不会过大。否则在JDK 6时代intern进常量池的字符串无法被及时回收直接导致PermGen OOMJDK 7之后虽然可以回收但StringTable的哈希桶冲突和大规模驻留仍然有额外开销。用比较字符串是误用除非你非常确定两侧都是通过intern机制得到的引用。正确姿势永远是equals。这些经验不是纸上谈兵。我在排查线上Full GC问题时确实见过因为字符串对象大量创建导致年轻代晋升过快的情况。对象创建本身的成本看似不高但量一大GC压力就会显著上升尤其是那种每秒请求量上万的服务一段不规范的字符串拼接代码就可能成为性能瓶颈。5. 面试加分项与常见误区排查除了核心原理我还想整理一下这个题目里最容易踩的坑和额外的加分答法帮助你在面试中真正脱颖而出。5.1 三个最容易说错的点第一把abc字面量理解成只在堆里创建。有些人会讲abc不是已经放在常量池了吗常量池在堆里所以“abc”就是堆对象。这个理解方向没错但如果不加区分地说“都在堆里所以只算一个对象”就错了。关键在于StringTable的全局共享语义它代表JVM层面对字符串的特殊管理和普通new出来的堆对象不能一概而论。第二只记得“两个对象”不记得前提条件。很多八股答案会直接写“创建两个对象”但忽略了前提是“常量池中没有abc”。面试官特别容易设一个陷阱前面已经执行过String pre abc;现在执行new String(abc)创建了几个对象如果你不加思考就说两个就掉坑里了。第三把String s abc说成一定只创建一个对象。如果常量池中已经有“abc”它其实一个对象都不创建。这个说法听起来有点反直觉但却是事实。好在面试官通常不会用“0个”来作为标准答案而是看你能不能把共享逻辑讲清楚。5.2 面试官追问时怎么答更有深度如果面试官问到了版本差异我建议的回答思路是这样的先从字节码层面说明new、ldc、invokespecial三条指令各自干了什么。然后说明字符串常量池的位置变化JDK 7以前在永久代之后在堆中。再说明String内部存储变化JDK 9之前是char[]之后是byte[] coder。最后落到实际开发建议不要用new String(String)不要滥用intern()字符串比较始终用equals。这一套答下来面试官基本能确认你在JVM和Java源码层面下过功夫而不是单纯看了面经。另外如果聊到StringBuilder和StringBuffer的区别可以顺嘴提一句StringBuffer的线程安全来自方法上的synchronized修饰而StringBuilder是非线程安全的。在方法内部单线程拼接时用StringBuilder就足够了用StringBuffer反而白白承担了不必要的同步开销。这也是面试官喜欢顺带考察的点。我自己在面试经历里还发现能把String的不可变性和常量池的共享机制联系起来的候选人通常对Java这门语言的理解更整体。比如正因为编译期就能确定所有字符串字面量JVM才能设计常量池来共享实例正因为String不可变多个引用才能安全地指向同一个对象而不用担心内容被改坏。如果String像数组一样可以修改内部内容那常量池的共享设计就完全不可行了。这个因果逻辑比背多少题都重要。验题和排查的收尾方式我在前面几节其实已经写完了。最后再分享一个小习惯每当我带新同事聊到字符串相关的问题时都会让他们从这一道题开始梳理然后用javap -c反编译几个实例分别在JDK 8和JDK 17上跑一遍测试代码对比输出结果。这个动手过程比任何面经都管用因为亲眼看到字节码、看到不同JDK下的行为差异之后这些知识点就真正变成自己的了。
返回列表