这道题我在面试里问了无数遍,也在无数个技术群里看人争过。String str = new String("abc") 到底创建了几个对象?有人脱口而出“两个”,有人犹豫半天说“一个”,也有人反问“是不是要看情况”。每次听到这里我都挺感慨——这题看起来基础到不能再基础,但它能暴露的东西,比你想象中多得多。
先说个现场画面。上个月面一个三年经验的候选人,前面聊项目、聊并发都还行,我顺势问了一句这道题。他几乎是抢答的:“两个对象,引用str一个,堆里new出来的String一个。”然后我就追问了一句:“那‘abc’这个字符串本身呢?它算不算对象?它在哪?”他愣了一下,之后就开始绕了。
这就是我想写这篇文章的原因。你背过答案不等于你懂了,你懂了点皮毛也未必经得住追问。更关键的是,不同JDK版本、不同写法、不同运行阶段,这个问题的答案真的会变。今天我从底层到面试话术,把这道题彻底拆开聊一遍。
1. 一道看似送分、实则筛人的面试题,到底在考什么
先说个可能让你意外的结论:这道题的标准答案从来不是唯一的,它取决于你站在哪个时间点看问题。
很多人都把这道题当成JVM内存模型的背诵题,实际上面试官问它,核心是想知道三件事:你有没有真正理解字符串常量池和堆的关系;你有没有区分“编译期创建的东西”和“运行期创建的东西”;你能不能把一个Java表达式拆成JVM层面的一系列指令去理解。
就我自己的面试经验而言,能把这三点讲清楚的人,项目里处理内存优化、SQL拼接、缓存Key设计这些事,往往也不会差到哪里去。反过来,只会背“两个对象”的人,遇到线上字符串导致的卡顿或OOM,大概率是一脸懵的。
1.1 从候选人最常见的两种回答说起
先把这两种典型回答摆出来:
回答A:“一个对象,String指向的就是‘abc’啊。”
回答B:“两个对象,一个是new出来的String,一个是字符串常量池里的‘abc’。”
回答A显然是错的,因为它把字面量“abc”和new出来的String对象混为一谈了。字符串常量池里存的东西是一个独立的String实例,或者至少是一个可被引用的对象形态的数据,不能因为你平时写String s = "abc"用起来“看起来一样”,就认为引用和对象是同一个东西。
回答B听起来正确,其实也只说对了一半。为什么?因为你只描述了代码运行的那一刻,却没考虑代码运行之前发生了什么。“abc”这个字符串对象,其实在类加载阶段就已经被创建好并放进常量池了,这与new String执行不执行无关。也就是说,你在执行这一行代码之前,“abc”就已经作为对象存在于JVM内部了。
所以当面试官问“创建了几个对象”时,你必须定义清楚:你问的是类加载期创建的,还是这一行代码运行期创建的?如果把两个阶段合在一起算,结论和单独算运行期是截然不同的。
1.2 标准答案“两个对象”为什么不够用
市面上流传最广的答案是:“创建了两个对象,一个在堆里,一个在字符串常量池里。” 这个答案能应付八成的初级面试,但漏洞很明显。
第一个漏洞:字符串常量池里的对象是在类加载阶段创建的,不属于这行代码运行时的“创建动作”。如果你严格限定“执行这行代码时”,那你只能说new动作创建了一个对象,并引用了一个早已存在的常量池对象。
第二个漏洞:如果这个类在运行前,字符串常量池里根本没有“abc”呢?比如你之前执行过一句String s = new String(new char[]{...}),把字符串给变出来了,但池子里没有对应字面量。此时执行String str = new String("abc"),实际上类加载阶段还会先把“abc”扔进池子。这种情况下的对象计数又不一样。
第三个漏洞:JIT编译后,对象能不能创建出来都不一定。现代JVM会做逃逸分析,如果new出来的String没有逃逸出当前方法,它可能被栈上分配甚至完全标量化,也就是不实际创建对象。真要抬杠,“两个对象”连运行期都不一定成立。
所以这道题表面上考String,实际考的是你对JVM规范的掌握程度,以及你说话的逻辑边界。能把边界讲清的人,已经不是背题选手了。
1.3 这题真正考察的三个底层知识点
我把这道题的底层知识点拆成三层:
第一层是Java对象创建机制。new关键字一定会触发对象创建吗?在解释执行阶段确实会,在JIT阶段可能被优化掉。这是很多两年经验以内的人没想过的。
第二层是字符串的驻留机制,也就是intern相关逻辑。JVM维护了一个字符串常量池,用字面量方式赋值的字符串会进入池中,后续相同内容的引用可以直接复用池中对象。这个机制的本质是“用空间换时间”还是“用时间换空间”,也值得聊一聊。
第三层是类加载和常量池的关系。一个类中出现的所有字符串字面量,会作为CONSTANT_String_info常量存放在Class文件的常量池中。类加载阶段,JVM会把这些常量解析成对应的String对象实例,放到运行时常量池或堆的特殊区域里。也就是说,字面量“abc”并不是你写String str = new String("abc")那一刻才诞生的,它在类加载时已经存在了。
理解了这三层,你就能明白为什么同一个问题换个写法、换台JVM、换个时间点,答案都会变。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先给标准结论:一行的执行在JVM里拆成了几件事
我们把场景限定在最常见的情况:JDK 8及以上,HotSpot虚拟机,代码所在类第一次被加载,且此前程序中没有以任何形式出现过“abc”这个字符串内容。
在这种设定下,String str = new String("abc") 的整体流程可以拆成两部分。
类加载阶段,JVM读取该类对应的Class文件,发现字符串常量池中有一个“abc”的字面量。此时JVM会在字符串常量池中创建(或者说驻留)一个内容为“abc”的String对象。注意这里用的是“驻留”,如果常量池已经存在相同内容的字符串,则不重复创建。
