String.intern()探索当“new String”遇上常量池诞生了多少个对象摘要String.intern() 是Java面试中一个经典的“深坑”话题。当new String与字符串常量池相遇到底创建了多少个对象不同的JDK版本、不同的构造方式、不同的调用时机结果截然不同。本文将从JVM内存模型出发逐行拆解代码辅以流程图和内存图解彻底讲清intern()的底层逻辑。1. 前置知识字符串的“两界”与JDK版本差异1.1 字符串的两大存储区域堆Heap所有通过new String()显式创建的对象都分配在堆上。每个new必定产生一个独立的对象。字符串常量池String Pool存放字符串字面量和 intern 后的字符串引用具有唯一性。1.2 JDK 版本的关键差异| 特性 | JDK 6 及之前 | JDK 7 及之后 ||------|-------------|-------------|| 常量池位置 | 永久代PermGen | 堆Heap || intern 行为 | 首次遇到字符串时在永久代拷贝一份 | 首次遇到字符串时在常量池中记录堆中对象的引用|| 内存影响 | 永久代空间有限容易OOM | 堆空间受GC管理 |JDK 7 的改动是核心intern 不再是“拷贝一份”而是常量池直接指向堆中首次出现的那个对象。本文的分析默认基于 JDK 8。---2. 源码剖析intern 的 native 真意2.1 方法签名/** * 当调用intern方法时如果常量池中已经包含一个等于此String对象的字符串 * 用equals方法判定则返回常量池中的引用。 * 否则将此String对象加入常量池并返回此对象的引用。 */ public native String intern();2.2 intern 的执行逻辑流程图是否是JDK7否调用 str.intern常量池中是否存在equals相同的字符串?返回常量池中已有的引用当前str是否已在堆中?常量池记录str的堆引用在常量池创建新字符串返回str本身结束关键结论JDK 7 中如果堆中的字符串是首次出现intern 不会创建新对象只是在常量池中登记它的地址。---3. 经典考题拆解到底创建了几个对象3.1 题目一String s new String(abc);String s new String(abc);问题这行代码创建了几个对象分析类加载时遇到字面量abcJVM 在常量池中创建abc字符串对象对象1。new String(abc)执行时在堆上创建一个新的 String 对象内容复制常量池中的abc对象2。所以共计2 个对象s 指向堆中的对象2。字符串常量池堆空间底层的value数组指向同一个字符数组String对象2value[a,b,c]String对象1abcs3.2 题目二new String(a) new String(b);String s1 new String(a) new String(b);问题这行代码创建了几个对象分析new String(a)常量池a对象1 堆对象对象2。new String(b)常量池b对象3 堆对象对象4。操作符编译器优化为new StringBuilder().append(a).append(b).toString()。toString()内部new String(value, 0, count)创建堆对象对象5。注意ab这个字面量在代码中并未出现所以常量池中不会自动存入ab。共计5 个对象s1 指向堆中对象5。3.3 题目三经典面试题String s1 new String(a) new String(b); // 行1 String s2 s1.intern(); // 行2 String s3 ab; // 行3 System.out.println(s1 s2); // ? System.out.println(s1 s3); // ? System.out.println(s2 s3); // ?答案s1 s2→trues1 s3→trues2 s3→true全 true 解析行1创建了堆对象ab5个对象分析同上。此时常量池中没有ab只有a和b。行2s1.intern()。常量池查找ab不存在。JDK 7 不会拷贝一份新的而是将常量池直接指向 s1 这个堆对象。返回常量池中的引用也就是 s1 本身。行3String s3 ab。常量池中已经有了指向 s1直接返回s3 同样指向 s1。结果是 s1、s2、s3 三个引用全部指向堆上的同一个对象。字符串常量池堆空间String对象value[a,b]常量池槽位(引用地址)s1s2s33.4 变种调换顺序后的结果String s3 ab; // 行1 String s1 new String(a) new String(b); // 行2 String s2 s1.intern(); // 行3 System.out.println(s1 s2); // ? System.out.println(s1 s3); // ? System.out.println(s2 s3); // ?答案s1 s2→falses1 s3→falses2 s3→true解析行1字面量ab直接存入常量池创建常量池对象对象1。s3 指向它。行2s1指向堆上新创建的ab对象对象5。此时堆和常量池各有独立的ab。行3s1.intern()。常量池中已存在ab所以直接返回常量池中已有的引用即对象1。s2 指向常量池对象。最终s1 指向堆对象s2 和 s3 指向常量池对象两者不是同一个对象。---4. 延伸考点4.1 两个java的诡计String s1 new StringBuilder(ja).append(va).toString(); System.out.println(s1.intern() s1); // false原因java这个字符串在 JVM 启动时就已经被加载到常量池了系统类等会使用到所以s1.intern()返回的是早已存在的常量池对象s1 这个堆对象并不会进入常量池。4.2 深入理解《深入理解Java虚拟机》的题目书中经典的题目如果基于 JDK 8 运行答案会从 JDK 6 的false, true变为 JDK 8 的true, true因为 intern 不再拷贝对象。---5. 总结与最佳实践new String(xxx) 创建 2 个对象常量池的字面量 堆上的副本。new String(a) new String(b) 创建 5 个对象两个常量池单字、两个堆单字、一个堆拼接结果。注意常量池不会自动生成ab。JDK 7 的 intern常量池直接存储堆引用不再拷贝。但若常量池已存在返回的是旧引用。实际开发极少主动调用 intern()。它的设计初衷是节省内存但不当使用可能导致常量池膨胀。正确做法是让 JVM 自行管理字符串去重G1 的-XX:UseStringDeduplication。