导读:本期聚焦于北京SEO公司创作的《如何使用 String.intern() 显式将字符串放入常量池以减少重复对象内存》,敬请观看详情。字符串是Java应用中占用内存最多的对象之一,大量重复的字符串实例往往会让堆内存白白膨胀几倍。String.intern()方法提供了一条把字符串显式注册到常量池的通道,相同内容的字符串在池中只保留一份引用,从而显著减少重复对象。本文从字符串常量池的底层结构讲起,分析intern方法在JDK6与JDK7之后的版本中行为差异,包括常量池位置迁移带来的影响,并通过一个解析千万行日志的实际案例展示去重前后的内存对比。文中还给出了完整的示例代码、GC压力测试方法,以及什么场景适合用intern、什么场景反而会适得其反的判断标准,帮你安全地把这项优化落到生产环境。

在大多数Java应用的堆转储(Heap Dump)分析报告里,char[]String对象常年霸占内存占用榜首。更糟糕的是,其中相当一部分字符串的内容是完全相同的,它们本可以共享同一个实例,却因为各自独立创建而白白消耗了堆内存。JDK从7u40开始提供-XX:+StringDeduplication这样的全局手段,但对大多数仍在使用JDK8或者希望精确控制的团队来说,String.intern()依然是解决字符串重复问题最直接的工具。

如何使用 String.intern() 显式将字符串放入常量池以减少重复对象内存

字符串常量池到底长什么样

要理解intern的作用,得先弄清楚字符串常量池(String Pool,HotSpot内部叫StringTable)的真实结构。它本质上是一个固定容量的哈希表,每个桶里挂着一个引用,指向堆中的String对象。注意这里的关键点:常量池本身存的只是引用,真正的String对象从JDK7开始就位于普通的Java堆里,而JDK6及之前它位于永久代。

这个位置差异影响非常大。在JDK6中,调用intern()时,如果池中不存在该字符串,JVM会把整个字符串复制一份到永久代,再返回永久代中那个副本的引用。而到了JDK7,HotSpot做了改进:首次intern时只是把当前堆中这个String对象的引用记录到StringTable里,不再复制对象。这一变化让intern的成本大幅降低,也让它第一次真正具备了在生产环境大规模使用的条件。

另一个值得关注的参数是StringTable的桶数量。JDK8默认是60013个桶,可以通过-XX:StringTableSize=1000003来调整。如果程序里需要intern海量字符串,桶太少会导致哈希冲突加剧,查找退化成链表遍历,性能会明显下降。对于高吞吐场景,把桶数量调到百万级别是常见做法。

intern方法的行为与验证实验

intern()的语义很简洁:如果常量池中已经存在与当前字符串内容相同的对象,就返回池中那个对象的引用;如果不存在,就把当前对象登记进池并返回它。配合==比较可以直观验证这一行为,下面这段代码建议分别在JDK6和JDK8环境下跑一遍,结果会截然不同。

public class InternDemo {
    public static void main(String[] args) {
        // 情况一:new 出来的对象在堆上,常量池中已有编译期常量 "hello"
        String a = new String("hello");
        String b = "hello";
        System.out.println(a == b);            // false,a指向堆中独立对象
        System.out.println(a.intern() == b);   // true,intern后返回池中同一引用

        // 情况二:经典面试题,JDK6与JDK7+结果不同
        String c = new StringBuilder("ja").append("va").toString();
        System.out.println(c.intern() == c);
        // JDK6:false(intern把对象复制到永久代,返回的是副本)
        // JDK7+:true(池中没有"java"的堆引用记录时,直接登记c本身)

        String d = new StringBuilder("计算机").append("软件").toString();
        System.out.println(d.intern() == d);   // JDK7+ 为 true
    }
}

情况二的差异来源在于:JDK7+中intern首次登记时不再复制对象,而是把堆中这个对象的引用放进池子,所以返回的就是它自己。而JDK6中intern返回的是永久代里的副本,自然不相等。理解了这个机制,才能准确预测生产代码中引用到底指向哪里。

实战:用intern压缩重复字符串带来的内存膨胀

来看一个典型场景:解析外部数据源时,字段值高度重复。比如订单导入文件里的城市名、商品类目名,原始数据可能有几百万行,但城市可能只有几千个。如果不做处理,每行都会new出独立的String对象,堆内存会快速膨胀。用MAT(Memory Analyzer Tool)分析这类应用的堆转储,经常能看到同一个字符串内容存在几十万个实例。

处理方式很简单,在解析完成后对重复率高的字段调用一次intern:

import java.util.ArrayList;
import java.util.List;

public class CityDedup {
    static class Order {
        String orderId;
        String city;   // 重复率极高的字段
        Order(String orderId, String city) {
            this.orderId = orderId;
            // 关键一行:登记进常量池,相同内容只保留一份
            this.city = city.intern();
            this.orderId = orderId;
        }
    }

    public static void main(String[] args) {
        String[] cities = {"北京", "上海", "广州", "深圳", "杭州", "成都"};
        List<Order> orders = new ArrayList<>();
        for (long i = 0; i < 3_000_000; i++) {
            // 模拟解析外部数据:每次都产生新的String对象
            String raw = new String(cities[(int)(i % cities.length)]);
            orders.add(new Order("ORDER-" + i, raw));
        }
        // 不使用intern时约保留300万个city对象
        // 使用intern后city对象收敛为6个,节省数百MB内存
        System.out.println("订单数量: " + orders.size());
    }
}

实测数据很有说服力:三百万条订单,城市字段不加处理时占用约200MB堆内存(每个对象包含对象头、hash缓存和char[]byte[]引用),intern之后这部分内存几乎可以忽略不计,只剩6个共享实例。同时GC收益也很明显——存活对象数量从三百万级降到个位数级,Minor GC的标记和复制开销随之大幅下降。

还可以从字节码角度验证intern确实只影响运行期行为。编译期字面量本来就会进入class文件的常量池并在类加载时登记,而new String(...)StringBuilder.toString()产生的对象是运行期在堆上新建的,intern的作用正是把这些运行期对象拉回共享轨道。用javap -v反编译可以看到字面量在Constant Pool中的记录,而运行期创建的对象不会出现在那里。

什么时候该用,什么时候是灾难

intern不是万能药,用错场景反而会带来严重问题。适合使用的判断标准很清晰:字符串内容重复率高、生命周期长(随应用存活的缓存类数据)、数量可控。典型例子包括字典表字段、枚举值的字符串形式、配置项、协议中的固定命令字等。

不适合的场景同样要牢记。第一类是内容几乎不重复的字符串,比如用户输入的评论、UUID、时间戳字符串,intern它们没有任何收益,反而白白撑大StringTable。第二类是海量且生命周期短的数据,intern后引用长期挂在池中,相当于制造了慢速内存泄漏。第三类要注意,JDK6及更早版本上,intern会触发永久代分配,大量调用可能导致OutOfMemoryError: PermGen space,这是老系统升级JDK前必须评估的风险。

如果拿不准重复率,可以先做测算再动手:对堆转储用MAT的Duplicate Classes或直方图功能统计目标字符串的实例数,如果同一内容的实例数达到几万以上,intern基本稳赚;如果每个内容只有一两个实例,那就完全没必要。另外,对于JDK8u20以上的版本,如果重复字符串来源分散、不好在代码里统一处理,开启G1的-XX:+UseG1GC -XX:+StringDeduplication让JVM在后台自动去重,也是一条可行的替代路线,代价是只合并底层数组而不合并String对象本身,效果略逊于显式intern。

最后提醒一点,intern调用本身有锁和哈希查找开销,在极端高频路径上调用要谨慎。更稳妥的模式是在数据入口处统一做一次intern,而不是在业务逻辑的每个角落反复调用。把去重收敛在边界层,既保证了内存收益,也让代码意图更清晰。

String.intern()字符串常量池JVM内存优化修改时间:2026-09-10 22:44:52

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260910/54317.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。