Java面试题new String(“abc“)创建几个对象?JVM常量池与堆内存深度解析 1. 这道题为什么能成为面试分水岭1.1 从一道“背答案”的题说起“new String(abc)创建了几个对象”——这道题在面试圈流传了太多年几乎成了Java基础部分的保留曲目。我见过太多候选人的反应有人脱口而出“两个”有人犹豫半天说“一个吧”还有人反问“看常量池里有没有吧”。这三种回答恰好对应了三个完全不同的理解层次。先给一个最直接的结论在绝大多数实际运行场景下答案是两个。但如果你只记住“两个”这两个字面试官稍微换个问法比如“那new String(abc)和直接写abc有什么区别”“字符串常量池到底存的是引用还是对象”“JDK 1.7之后常量池挪到堆里对这道题有什么影响”你大概率会卡壳。我自己做面试官这些年问这道题从来不是为了听那个数字。数字谁都能背我要看的是你能不能把类加载、常量池、堆内存分配、字符串不可变性这几条线串起来。这道题真正的价值在于它是一个极小的切口但能剖开JVM内存模型的一大块内容。1.2 先明确讨论的前提JDK版本和运行时机在展开之前必须先把前提钉死否则这道题根本没有唯一答案。第一个变量是JDK版本。JDK 1.6、1.7、1.8以及之后的版本字符串常量池的位置发生了变化。JDK 1.6时常量池在方法区永久代里JDK 1.7开始字符串常量池被移到了堆内存中JDK 1.8之后永久代被元空间取代但字符串常量池依然在堆里。这个变化直接影响“创建了几个对象”的答案细节。第二个变量是代码执行的时机。这行代码是第一次执行还是常量池里早就有了abc是在类加载阶段就触发了常量池的写入还是运行到这一行才触发这些都会影响对象的实际创建数量。第三个变量是“对象”这个词的界定。字符串常量池里那个abc算不算一个“对象”如果算那答案偏向两个如果只把堆上new出来的算对象那答案就是一个。面试时如果不先把“对象”的定义对齐讨论就是鸡同鸭讲。我通常会在面试时先问候选人“你说的对象包不包括常量池里的那个”这一问就能看出他有没有真正想过这个问题。1.3 这道题考察的底层能力到底是什么把这道题拆开它实际在考四件事字符串常量池的机制什么时候写入、写入的是什么、如何复用。堆内存分配new关键字到底做了什么对象在堆上怎么布局。引用与对象的区别常量池里存的是字符串对象本身还是指向堆中对象的引用。编译期与运行期的分工字面量abc是在编译期就确定的而new是运行期行为。能把这四点讲清楚的人JVM基础一定不会差。反过来只会背“两个”的人遇到“String s a b c创建了几个对象”这种变体立刻就露馅了。2. 把“两个对象”拆开看一个在常量池一个在堆2.1 字面量abc触发了什么当JVM执行到new String(abc)这一行时它并不是“先new再处理字面量”而是有一个明确的先后顺序。第一步JVM遇到字面量abc。它会去字符串常量池里查看有没有一个内容等于abc的字符串。如果没有就在常量池里创建一个abc的字符串对象并返回它的引用如果已经有了就直接返回已有对象的引用不再新建。这一步的关键在于常量池的检查和写入是字面量触发的跟前面的new没有关系。很多人误以为是new导致了常量池的操作其实不是。哪怕你只写String s abc;这一行同样会触发常量池的检查与写入。第二步才是new String(...)。new关键字在堆上分配一块内存创建一个新的String对象。这个新对象的字符内容是从常量池那个abc复制过来的准确说是共享同一个char[]或byte[]取决于JDK版本对String内部存储的实现。所以到这里确实产生了两个对象常量池里一个堆上一个。2.2 常量池里存的到底是对象还是引用这是这道题最容易含糊的地方也是区分“背答案”和“真理解”的分水岭。在JDK 1.6及之前字符串常量池在方法区永久代中池里存放的是字符串对象本身。也就是说常量池里那个abc是一个实实在在的String实例。从JDK 1.7开始字符串常量池被移到了堆中。此时池里存放的变成了指向堆中String对象的引用。这个变化带来的直接影响是String.intern()方法的行为变了。在1.6中intern()会把字符串复制到永久代的常量池在1.7之后intern()直接把堆中对象的引用放进常量池不再复制。回到new String(abc)这道题在JDK 1.7之后常量池里那个abc和堆上new出来的对象其实是两个不同的String实例但常量池里保存的是它自己那个实例的引用。这个细节在面试时如果能主动说出来加分非常明显。2.3 用一段代码把对象数量“验”出来光靠嘴说不够我习惯用代码把这件事验证给候选人看。下面这段代码可以直接跑public class StringTest { public static void main(String[] args) { String s1 abc; String s2 new String(abc); String s3 s2.intern(); System.out.println(s1 s2); // false System.out.println(s1 s3); // true System.out.println(s2 s3); // false } }运行结果false、true、false。这三个结果把关系说得很清楚s1指向常量池里的abcs2指向堆上new出来的对象两者地址不同所以第一个是false。s2.intern()返回的是常量池里那个abc的引用所以s3和s1指向同一个对象第二个是true。s2和s3一个在堆一个在常量池第三个自然是false。如果你把这段代码在JDK 1.6和JDK 1.8下分别跑前两个结果不变但intern()的底层行为是有差异的。这也是为什么我强调面试时要先问清楚JDK版本。2.4 一个容易被忽略的细节char数组的共享还有一个细节值得说。在JDK 1.8及之前String内部用char[]存储字符。new String(abc)创建新对象时这个新对象的char[]并不是重新复制一份而是直接指向常量池那个abc的char[]。也就是说两个String对象共享同一个字符数组。到了JDK 9之后String的内部存储从char[]改成了byte[]配合一个coder字段来标识编码LATIN1或UTF16。这个改动是为了节省内存但对“创建了几个对象”这个问题的答案没有影响——依然是两个String对象只是底层数组的类型变了。这个细节在面试时说出来能体现你对String演进历史的了解属于加分项但不是必答项。3. 面试官真正想听的追问链条3.1 追问一那String s abc创建了几个对象这是最常见的追问。答案是如果常量池里没有abc创建一个如果已经有了创建零个。注意这里的措辞。很多人会脱口而出“一个”但严格来说只有在常量池为空的前提下才是一个。如果之前已经有别的代码写过abc这一行就只是把引用拿过来不创建任何新对象。我面试时喜欢连着问“先执行String a abc;再执行String b abc;第二行创建了几个对象”正确答案是零个。能答对的人说明他真正理解了常量池的复用机制而不是死记“字面量创建一个对象”。3.2 追问二new String(abc)和abc有什么区别这个问题的核心是对象身份和复用性。abc是编译期就确定的字面量直接指向常量池多次使用同一个字面量拿到的是同一个对象可以用比较且结果为true。new String(abc)是运行期在堆上强制新建的对象每次new都会产生一个新对象即使内容相同比较也是false必须用equals()比较内容。实际开发中除非有特殊需求比如需要确保对象身份唯一或者从外部数据构造字符串否则不应该用new String来创建字符串。这是一个典型的性能反模式在循环里写new String更是灾难。3.3 追问三String s new String(abc) def创建了几个对象这是进阶变体考察的是字符串拼接的编译期优化。abc def如果两边都是字面量编译器会直接在编译期合并成abcdef只涉及常量池。但这里左边是new String是运行期对象编译器无法优化会走StringBuilderJDK 9之后是invokedynamic StringConcatFactory来拼接。具体对象数量常量池里abc一个如果没有的话堆上new的String一个拼接过程中StringBuilder对象一个拼接结果String一个def字面量一个如果没有的话。加起来至少四到五个。这种题没有标准答案关键是看你能不能把每一步的对象来源说清楚。3.4 追问四intern()方法在这道题里扮演什么角色intern()是一个native方法作用是检查常量池里有没有内容相等的字符串有就返回池中那个的引用没有就把当前字符串的引用放进池里JDK 1.7之后并返回。在new String(abc)的场景里因为字面量abc已经先一步写入了常量池所以后面调用intern()一定返回的是常量池里那个而不是堆上new出来的那个。这里有个经典陷阱new String(abc).intern() abc的结果是true因为intern()返回的就是常量池里的引用。但new String(abc) abc是false。这两个表达式放在一起能瞬间检验出候选人有没有真正理解引用和对象的关系。4. 从这道题延伸出的实战避坑经验4.1 循环里拼接字符串的代价理解了这道题就能明白为什么在循环里用拼接字符串是性能杀手。// 反例 String result ; for (int i 0; i 10000; i) { result i; }这段代码每次循环都会new一个StringBuilder、append、再toString出一个新String。一万次循环就是一万个StringBuilder和一万个String对象全部压在堆上GC压力巨大。正确做法是在循环外建一个StringBuilder循环内append最后toString一次。这个优化背后的原理正是这道面试题讲的“new会强制创建新对象”。4.2 字符串常量池的容量与GC字符串常量池在JDK 1.7之后位于堆中这意味着它也会被GC管理。如果一个字符串在常量池里没有任何引用它是可以被回收的。这一点和很多人“常量池里的东西永远不回收”的直觉相反。在JDK 1.8中可以通过-XX:StringTableSize调整常量池的表大小默认是60013。如果应用中有大量不同的字符串字面量或intern操作适当调大这个值可以减少哈希冲突提升intern性能。这个参数在字符串去重场景下很有用。4.3 面试时的表达策略最后说点面试技巧。这道题如果只答“两个”面试官大概率会继续追问你需要在追问中把细节补上。更好的策略是主动分层回答先给结论“通常是两个”然后立刻补上前提“这取决于JDK版本和常量池的初始状态”接着把常量池一个、堆一个的来龙去脉讲清楚最后主动提一句“如果常量池里已经有abc那字面量这一步就不创建新对象了”。这种回答方式既给了明确答案又展示了边界意识还主动暴露了知识深度。面试官听到这种回答基本就能判断你对JVM内存模型是有系统理解的而不是临时背的。我在实际带团队时也发现能把这道题讲透的人后面排查内存泄漏、分析GC日志、优化字符串相关性能问题时上手速度明显更快。这道题的价值从来不在那个数字本身而在于它逼着你去把JVM的几块核心知识真正串起来。