Java 中String.intern()方法的作用可以用一句话概括将字符串对象加入到字符串常量池中并返回该字符串在常量池中的引用。为了真正理解它是干嘛用的需要结合字符串常量池的机制来看。Q1: java字符串的intern()是干嘛用的1. 核心机制它是怎么工作的在 JVM 中有一个专门的内存区域叫“字符串常量池”。当你调用一个字符串的intern()方法时JVM 会执行以下操作去池子里找检查常量池中是否已经存在内容相同的字符串。如果存在直接返回常量池中那个已存在字符串的引用。如果不存在把当前字符串的内容复制或记录引用取决于JVM版本到常量池中然后返回常量池里的这个新引用。2. 代码示例直观感受publicclassInternDemo{publicstaticvoidmain(String[]args){// 场景1字面量创建直接在常量池中Strings1hello;// 场景2new 关键字创建在堆内存中开辟了一个新对象// 注意此时常量池里其实也有 hello因为 s1 已经把它放进去了Strings2newString(hello);// 场景3调用 intern()Strings3s2.intern();// 比较地址引用System.out.println(s1s2);// false (一个是常量池一个是堆)System.out.println(s1s3);// true (s3 指向了常量池中已有的 hello)System.out.println(s2s3);// false (堆地址 ! 常量池地址)}}3. 什么时候需要用到intern()应用场景在日常业务代码中我们很少主动去调intern()因为直接用双引号声明的字符串本来就在常量池里。但在以下两种场景intern()非常有用场景一极大节省内存应对海量重复字符串假设你从数据库或日志文件中读取了 1000 万条用户数据其中有一个字段是“省份”。全国只有 34 个省级行政区但如果不加处理你会在堆内存中创建几百万个内容为“广东省”、“北京市”的 String 对象极其浪费内存。用法Stringprovincers.getString(province);// 如果不 intern每次都会在堆里 new 新对象// 调用 intern 后这 1000 万个引用最终都会指向常量池里的那 34 个字符串provinceprovince.intern();场景二提升字符串比较速度用替代equals()比较的是内存地址速度极快而equals()需要逐个字符比较相对较慢。如果确认两个字符串都经过了intern()处理那么直接用判断是否相等即可。(注现代 JVM 对equals()的优化已经很好了为了代码可读性通常不推荐仅仅为了用而去调intern()但这个理论知识点面试常考)4. 面试必知的“坑”与历史演变重要关于intern()JDK 1.6 和 JDK 1.7 有一个巨大的分水岭JDK 1.6 及之前字符串常量池放在永久代方法区的一部分和堆内存是物理隔离的。调用intern()时如果池子里没有会把字符串对象本身复制一份扔进常量池。坑永久代空间很小如果动态生成大量不同的字符串并intern()极其容易导致OutOfMemoryError: PermGen space。JDK 1.7 及之后字符串常量池被移到了堆内存中。调用intern()时如果池子里没有不再复制对象而是在常量池里记录一个指向堆中该字符串对象的引用。好处大大减少了内存拷贝且受堆内存的垃圾回收GC管理不容易 OOM 了。5. 总结建议是什么返回字符串在常量池中的引用。怎么用str.intern()。该不该用平时写代码不要滥用如new String(abc).intern()是纯纯的脱裤子放屁因为abc本来就在池里。适合用的场景在读取外部数据数据库、网络、文件时如果明确知道会有海量且高度重复的字符串使用intern()可以把内存占用从 GB 级别降到 MB 级别。Q2: 如果是静态代码写的时候能写在代码里的字符串肯定都是在字符串常量池存在了的吧绝大多数情况下你的直觉是对的但并非绝对。关键在于你“怎么写”这个字符串。我们可以把写在代码里的字符串分为以下几种情况来盘点1. 纯字面量声明100% 在常量池如果你直接用双引号写一个字符串它在编译期就会被确定并直接放入字节码文件的常量池中类加载时直接进入字符串常量池。Strings1hello;// 绝对在常量池里Strings2你好世界;// 绝对在常量池里2. “编译期常量”的拼接100% 在常量池如果是由多个纯字面量或者加了final的基本类型/字符串拼接而成Java 编译器javac非常聪明它会在编译阶段直接帮你把结果算出来当成一个纯字面量处理。Strings3hello;// 编译器直接优化成 hello在常量池finalStringprefixhel;Strings4prefixlo;// 因为 prefix 是 final编译器也会优化成 hello在常量池finalintnum1;Strings5数字num;// 优化成 数字1在常量池3. 变量的拼接不在常量池—— 这是最大的陷阱如果参与拼接的变量没有final修饰编译器就无法在编译期确定它的值。这种情况只能在运行期通过StringBuilder或 JDK 9 的底层机制动态拼接结果会创建在堆内存中而不会自动进入字符串常量池Stringahel;// a 在常量池Stringblo;// b 在常量池Stringcalo;// 【注意】c 是在堆内存中新建的对象不在常量池// 证明System.out.println(chello);// 输出 false这时候就需要intern()了如果你想强行让c指向常量池你就必须手动调用Stringdc.intern();// 现在 d 指向常量池了System.out.println(dhello);// 输出 true4.new String()创建不在常量池但在堆里StringenewString(hello);这行代码实际上创建了两个东西hello这个字面量本身在常量池。new出来的那个 String 对象在堆内存。变量e指向的是堆内存里的对象而不是常量池里的对象。System.out.println(ehello);// 输出 false总结你说的“写在代码里的字符串都在常量池”更准确的说法应该是写在代码里的“字符串字面量”和“编译期可确定的常量表达式”肯定在常量池里。但是只要你的代码里包含了变量的动态拼接或者new** 关键字**哪怕你把变量声明死在代码里比如String a a; String b a b;它生成的结果也是在堆内存中并不在常量池中。所以如果你在静态代码里通过变量拼接得到了一个字符串又想用它去和别的字面量做比较或者想复用它的内存这时候对它调一下intern()就是有意义的。Q3: 为什么说如 new String(“abc”).intern() 是纯纯的脱裤子放屁说new String(abc).intern()是“脱裤子放屁”纯纯的多此一举是因为这行代码在底层白白在堆内存中创建了一个立刻就会被丢弃的垃圾对象。我们可以把这句代码拆解成三个阶段看看 JVM 到底做了什么多余的事拆解执行过程第一阶段类加载时处理abc当你把代码写进.java文件并编译后abc作为一个字面量在类加载的时候就已经被放进字符串常量池了。此时池子里已经有了abc。第二阶段执行new String(abc)多余的操作开始了当程序运行到这一行时JVM 会在堆内存中强行开辟一块新的空间创建一个新的String对象。这个对象里面包含的字符数据和常量池里的abc一模一样。此时内存里有两个 “abc”一个在常量池一个在堆里。第三阶段执行.intern()把刚才的努力全废了intern()方法会去常量池里找“喂池子里有没有abc啊”池子“有啊第一阶段就放进来了。”intern()“好嘞那我就直接返回池子里那个abc的地址吧。”最终结果这行代码执行完你拿到的是常量池里那个abc的引用。而第二步在堆内存里辛辛苦苦new出来的那个String对象因为没有任何引用指向它瞬间变成了孤儿只能等着被垃圾回收器GC清理掉。形象的比喻这就好比公司档案室常量池本来就已经有一份原件《员工手册》。你new花钱找复印机重新复印了一份拿在手里。然后你调用了intern()意思是去档案室找找有没有这本手册有啊那把我手里这份复印件扔进垃圾桶我还是用档案室的原件吧。你花钱复印了最后又扔了这不就是脱裤子放屁吗正确的做法如果你本来就想要常量池里的abc直接写Stringsabc;这就完了一步到位不创建任何多余的堆对象。⚠️ 补充纠正什么时候new String(...).intern()是有意义的必须强调的是我上面骂它“多此一举”前提是括号里传的是一个“字面量”比如abc。如果括号里传的是运行时动态计算出来的变量那new String(...).intern()就非常有意义了有意义的场景举例// 从网络或文件读取了一堆字符char[]chars{a,b,c};// 你必须用 new String 把字符数组转成字符串对象此时在堆内存中StringdynamicStrnewString(chars);// 此时调 intern() 就不叫脱裤子放屁了// 因为 abc 可能之前不在常量池里这一步把它成功存入常量池供全项目复用StringpooledStrdynamicStr.intern();总结new String(字面量).intern() 纯纯的脱裤子放屁。new String(动态变量/数组).intern() 合理的优化手段。Q4: 上面给的有意义场景举例中String pooledStr dynamicStr.intern(); 后存入常量池为什么就是优化手段了我不能继续引用dynamicStr吗这是一个非常敏锐的问题你当然可以继续使用dynamicStr它里面确实有abc这个数据你拿它做任何业务逻辑比如打印、截取、当参数传递完全没问题代码不会报错。但为什么说intern()是优化手段核心在于这个优化的目的不是为了方便你“用”这个字符串而是为了“省内存”和“减轻垃圾回收GC的压力”。为了让你彻底明白我们来看一个高并发/大量重复的场景模拟。场景模拟解析 1000 万条日志假设你写了一个方法用来解析外部传来的字符数组并提取出状态码。这个方法会被调用 1000 万次且状态码高度重复大部分都是abc。路径一不使用intern()只用dynamicStrpublicvoidparseLog(char[]chars){// 每次调用都会在堆内存中 new 一个全新的 String 对象StringdynamicStrnewString(chars);// 假设你把这个字符串存到了一个全局的 List 里globalList.add(dynamicStr);}发生了什么这行代码执行 1000 万次JVM 就会在堆内存里老老实实地创建 1000 万个内容都是abc的独立对象。即使它们内容一样但在 JVM 眼里它们是 1000 万个不同的个体。后果 1内存爆炸。1000 万个 String 对象及其内部char数组会吃掉大量堆内存。后果 2GC 疯狂。当内存满了垃圾回收器GC不得不疯狂工作去清理这些废对象导致你的程序卡顿STW。路径二使用intern()publicvoidparseLog(char[]chars){StringdynamicStrnewString(chars);// 核心优化把引用指向常量池dynamicStrdynamicStr.intern();// 存入全局 ListglobalList.add(dynamicStr);}发生了什么第 1 次调用堆里 new 了一个abcintern()发现池子里没有就把堆里这个对象的引用记录到池子里并返回。此时 List 里存的是池子的引用。第 2 次调用堆里又new 了一个abcintern()一看池子里已经有了于是直接抛弃刚 new 的那个堆对象返回池子里那个引用。刚 new 的对象瞬间变成垃圾。第 1000 万次调用依然如此。最终结果你的globalList里确实存了 1000 万个元素但这 1000 万个元素指向的是同一个内存地址常量池里的那个abc。中间那些被new出来又被抛弃的堆对象因为没有任何人引用它们会被极快地被轻量级 GC 清理掉。收益全局只需要维护 1 份abc的真实数据内存占用断崖式下降。回到你的疑问“我不能继续引用 dynamicStr 吗”你可以引用它但你要明白你引用的是谁如果你写String dynamicStr new String(chars);你手里拿的是刚刚从复印机里出来的一份复印件。如果你写dynamicStr dynamicStr.intern();你手里的复印件被扔了换成了档案室原件的借阅证。如果你只是在当前方法里用一下dynamicStr用完就扔了局部变量那确实没必要intern()因为方法结束它就被 GC 回收了不占长期内存。但只要这个字符串有以下两种情况之一intern()** 就有巨大意义**被长期存活的对象引用比如存进了全局的List、Map、Set或者存入了数据库缓存。被极高频率地创建比如在一个千万级的 for 循环里。一句话总结intern()不是为了改变字符串里的内容而是为了让成千上万个本该各自占内存的重复字符串合并成对同一个内存地址的引用。这就叫空间优化。
深入解析 Java String.intern():从内存模型到实战优化
发布时间:2026/6/30 1:03:13
Java 中String.intern()方法的作用可以用一句话概括将字符串对象加入到字符串常量池中并返回该字符串在常量池中的引用。为了真正理解它是干嘛用的需要结合字符串常量池的机制来看。Q1: java字符串的intern()是干嘛用的1. 核心机制它是怎么工作的在 JVM 中有一个专门的内存区域叫“字符串常量池”。当你调用一个字符串的intern()方法时JVM 会执行以下操作去池子里找检查常量池中是否已经存在内容相同的字符串。如果存在直接返回常量池中那个已存在字符串的引用。如果不存在把当前字符串的内容复制或记录引用取决于JVM版本到常量池中然后返回常量池里的这个新引用。2. 代码示例直观感受publicclassInternDemo{publicstaticvoidmain(String[]args){// 场景1字面量创建直接在常量池中Strings1hello;// 场景2new 关键字创建在堆内存中开辟了一个新对象// 注意此时常量池里其实也有 hello因为 s1 已经把它放进去了Strings2newString(hello);// 场景3调用 intern()Strings3s2.intern();// 比较地址引用System.out.println(s1s2);// false (一个是常量池一个是堆)System.out.println(s1s3);// true (s3 指向了常量池中已有的 hello)System.out.println(s2s3);// false (堆地址 ! 常量池地址)}}3. 什么时候需要用到intern()应用场景在日常业务代码中我们很少主动去调intern()因为直接用双引号声明的字符串本来就在常量池里。但在以下两种场景intern()非常有用场景一极大节省内存应对海量重复字符串假设你从数据库或日志文件中读取了 1000 万条用户数据其中有一个字段是“省份”。全国只有 34 个省级行政区但如果不加处理你会在堆内存中创建几百万个内容为“广东省”、“北京市”的 String 对象极其浪费内存。用法Stringprovincers.getString(province);// 如果不 intern每次都会在堆里 new 新对象// 调用 intern 后这 1000 万个引用最终都会指向常量池里的那 34 个字符串provinceprovince.intern();场景二提升字符串比较速度用替代equals()比较的是内存地址速度极快而equals()需要逐个字符比较相对较慢。如果确认两个字符串都经过了intern()处理那么直接用判断是否相等即可。(注现代 JVM 对equals()的优化已经很好了为了代码可读性通常不推荐仅仅为了用而去调intern()但这个理论知识点面试常考)4. 面试必知的“坑”与历史演变重要关于intern()JDK 1.6 和 JDK 1.7 有一个巨大的分水岭JDK 1.6 及之前字符串常量池放在永久代方法区的一部分和堆内存是物理隔离的。调用intern()时如果池子里没有会把字符串对象本身复制一份扔进常量池。坑永久代空间很小如果动态生成大量不同的字符串并intern()极其容易导致OutOfMemoryError: PermGen space。JDK 1.7 及之后字符串常量池被移到了堆内存中。调用intern()时如果池子里没有不再复制对象而是在常量池里记录一个指向堆中该字符串对象的引用。好处大大减少了内存拷贝且受堆内存的垃圾回收GC管理不容易 OOM 了。5. 总结建议是什么返回字符串在常量池中的引用。怎么用str.intern()。该不该用平时写代码不要滥用如new String(abc).intern()是纯纯的脱裤子放屁因为abc本来就在池里。适合用的场景在读取外部数据数据库、网络、文件时如果明确知道会有海量且高度重复的字符串使用intern()可以把内存占用从 GB 级别降到 MB 级别。Q2: 如果是静态代码写的时候能写在代码里的字符串肯定都是在字符串常量池存在了的吧绝大多数情况下你的直觉是对的但并非绝对。关键在于你“怎么写”这个字符串。我们可以把写在代码里的字符串分为以下几种情况来盘点1. 纯字面量声明100% 在常量池如果你直接用双引号写一个字符串它在编译期就会被确定并直接放入字节码文件的常量池中类加载时直接进入字符串常量池。Strings1hello;// 绝对在常量池里Strings2你好世界;// 绝对在常量池里2. “编译期常量”的拼接100% 在常量池如果是由多个纯字面量或者加了final的基本类型/字符串拼接而成Java 编译器javac非常聪明它会在编译阶段直接帮你把结果算出来当成一个纯字面量处理。Strings3hello;// 编译器直接优化成 hello在常量池finalStringprefixhel;Strings4prefixlo;// 因为 prefix 是 final编译器也会优化成 hello在常量池finalintnum1;Strings5数字num;// 优化成 数字1在常量池3. 变量的拼接不在常量池—— 这是最大的陷阱如果参与拼接的变量没有final修饰编译器就无法在编译期确定它的值。这种情况只能在运行期通过StringBuilder或 JDK 9 的底层机制动态拼接结果会创建在堆内存中而不会自动进入字符串常量池Stringahel;// a 在常量池Stringblo;// b 在常量池Stringcalo;// 【注意】c 是在堆内存中新建的对象不在常量池// 证明System.out.println(chello);// 输出 false这时候就需要intern()了如果你想强行让c指向常量池你就必须手动调用Stringdc.intern();// 现在 d 指向常量池了System.out.println(dhello);// 输出 true4.new String()创建不在常量池但在堆里StringenewString(hello);这行代码实际上创建了两个东西hello这个字面量本身在常量池。new出来的那个 String 对象在堆内存。变量e指向的是堆内存里的对象而不是常量池里的对象。System.out.println(ehello);// 输出 false总结你说的“写在代码里的字符串都在常量池”更准确的说法应该是写在代码里的“字符串字面量”和“编译期可确定的常量表达式”肯定在常量池里。但是只要你的代码里包含了变量的动态拼接或者new** 关键字**哪怕你把变量声明死在代码里比如String a a; String b a b;它生成的结果也是在堆内存中并不在常量池中。所以如果你在静态代码里通过变量拼接得到了一个字符串又想用它去和别的字面量做比较或者想复用它的内存这时候对它调一下intern()就是有意义的。Q3: 为什么说如 new String(“abc”).intern() 是纯纯的脱裤子放屁说new String(abc).intern()是“脱裤子放屁”纯纯的多此一举是因为这行代码在底层白白在堆内存中创建了一个立刻就会被丢弃的垃圾对象。我们可以把这句代码拆解成三个阶段看看 JVM 到底做了什么多余的事拆解执行过程第一阶段类加载时处理abc当你把代码写进.java文件并编译后abc作为一个字面量在类加载的时候就已经被放进字符串常量池了。此时池子里已经有了abc。第二阶段执行new String(abc)多余的操作开始了当程序运行到这一行时JVM 会在堆内存中强行开辟一块新的空间创建一个新的String对象。这个对象里面包含的字符数据和常量池里的abc一模一样。此时内存里有两个 “abc”一个在常量池一个在堆里。第三阶段执行.intern()把刚才的努力全废了intern()方法会去常量池里找“喂池子里有没有abc啊”池子“有啊第一阶段就放进来了。”intern()“好嘞那我就直接返回池子里那个abc的地址吧。”最终结果这行代码执行完你拿到的是常量池里那个abc的引用。而第二步在堆内存里辛辛苦苦new出来的那个String对象因为没有任何引用指向它瞬间变成了孤儿只能等着被垃圾回收器GC清理掉。形象的比喻这就好比公司档案室常量池本来就已经有一份原件《员工手册》。你new花钱找复印机重新复印了一份拿在手里。然后你调用了intern()意思是去档案室找找有没有这本手册有啊那把我手里这份复印件扔进垃圾桶我还是用档案室的原件吧。你花钱复印了最后又扔了这不就是脱裤子放屁吗正确的做法如果你本来就想要常量池里的abc直接写Stringsabc;这就完了一步到位不创建任何多余的堆对象。⚠️ 补充纠正什么时候new String(...).intern()是有意义的必须强调的是我上面骂它“多此一举”前提是括号里传的是一个“字面量”比如abc。如果括号里传的是运行时动态计算出来的变量那new String(...).intern()就非常有意义了有意义的场景举例// 从网络或文件读取了一堆字符char[]chars{a,b,c};// 你必须用 new String 把字符数组转成字符串对象此时在堆内存中StringdynamicStrnewString(chars);// 此时调 intern() 就不叫脱裤子放屁了// 因为 abc 可能之前不在常量池里这一步把它成功存入常量池供全项目复用StringpooledStrdynamicStr.intern();总结new String(字面量).intern() 纯纯的脱裤子放屁。new String(动态变量/数组).intern() 合理的优化手段。Q4: 上面给的有意义场景举例中String pooledStr dynamicStr.intern(); 后存入常量池为什么就是优化手段了我不能继续引用dynamicStr吗这是一个非常敏锐的问题你当然可以继续使用dynamicStr它里面确实有abc这个数据你拿它做任何业务逻辑比如打印、截取、当参数传递完全没问题代码不会报错。但为什么说intern()是优化手段核心在于这个优化的目的不是为了方便你“用”这个字符串而是为了“省内存”和“减轻垃圾回收GC的压力”。为了让你彻底明白我们来看一个高并发/大量重复的场景模拟。场景模拟解析 1000 万条日志假设你写了一个方法用来解析外部传来的字符数组并提取出状态码。这个方法会被调用 1000 万次且状态码高度重复大部分都是abc。路径一不使用intern()只用dynamicStrpublicvoidparseLog(char[]chars){// 每次调用都会在堆内存中 new 一个全新的 String 对象StringdynamicStrnewString(chars);// 假设你把这个字符串存到了一个全局的 List 里globalList.add(dynamicStr);}发生了什么这行代码执行 1000 万次JVM 就会在堆内存里老老实实地创建 1000 万个内容都是abc的独立对象。即使它们内容一样但在 JVM 眼里它们是 1000 万个不同的个体。后果 1内存爆炸。1000 万个 String 对象及其内部char数组会吃掉大量堆内存。后果 2GC 疯狂。当内存满了垃圾回收器GC不得不疯狂工作去清理这些废对象导致你的程序卡顿STW。路径二使用intern()publicvoidparseLog(char[]chars){StringdynamicStrnewString(chars);// 核心优化把引用指向常量池dynamicStrdynamicStr.intern();// 存入全局 ListglobalList.add(dynamicStr);}发生了什么第 1 次调用堆里 new 了一个abcintern()发现池子里没有就把堆里这个对象的引用记录到池子里并返回。此时 List 里存的是池子的引用。第 2 次调用堆里又new 了一个abcintern()一看池子里已经有了于是直接抛弃刚 new 的那个堆对象返回池子里那个引用。刚 new 的对象瞬间变成垃圾。第 1000 万次调用依然如此。最终结果你的globalList里确实存了 1000 万个元素但这 1000 万个元素指向的是同一个内存地址常量池里的那个abc。中间那些被new出来又被抛弃的堆对象因为没有任何人引用它们会被极快地被轻量级 GC 清理掉。收益全局只需要维护 1 份abc的真实数据内存占用断崖式下降。回到你的疑问“我不能继续引用 dynamicStr 吗”你可以引用它但你要明白你引用的是谁如果你写String dynamicStr new String(chars);你手里拿的是刚刚从复印机里出来的一份复印件。如果你写dynamicStr dynamicStr.intern();你手里的复印件被扔了换成了档案室原件的借阅证。如果你只是在当前方法里用一下dynamicStr用完就扔了局部变量那确实没必要intern()因为方法结束它就被 GC 回收了不占长期内存。但只要这个字符串有以下两种情况之一intern()** 就有巨大意义**被长期存活的对象引用比如存进了全局的List、Map、Set或者存入了数据库缓存。被极高频率地创建比如在一个千万级的 for 循环里。一句话总结intern()不是为了改变字符串里的内容而是为了让成千上万个本该各自占内存的重复字符串合并成对同一个内存地址的引用。这就叫空间优化。