一、先理解"字符串常量池"是什么
JVM 里有一块特殊区域叫 String Pool(字符串常量池) ,
你可以把它理解成一个全局公告栏:
String Pool(字符串常量池)
┌─────────────────────────────────┐
│ "2025" │
│ "Q1" │
│ "AP" │
│ "M&S" │
│ "hello" │
│ ... │
└─────────────────────────────────┘
规则:
同一个内容,在 Pool 里永远只存一份
任何人想用这个字符串,直接拿 Pool 里那个的地址
Java 7 之前 :String Pool 在 PermGen(永久代),不受 GC 管理
Java 7 之后:String Pool 搬到了堆里,可以被 GC
二、字符串是怎么进 Pool 的?
方式 1:直接写字符串字面量(自动进 Pool)
java
String a = "2025";
// JVM 在编译时就把 "2025" 放进 Pool
// a 直接指向 Pool 里的那个对象
String b = "2025";
// JVM 发现 Pool 里已经有 "2025" 了
// b 也指向同一个对象,不创建新的
System.out.println(a == b); // true!同一个对象!
Pool:
┌──────────────┐
│ "2025"@1001 │ ← a 和 b 都指向这里
└──────────────┘
堆里只有一个 "2025",不管你写多少次 "2025" 字面量
方式 2:new String() 或 运行时拼接(不进 Pool)
java
String c = new String("2025");
// 强制在堆上 new 了一个新对象
// 不管 Pool 里有没有,都是全新的
String d = someVar + "suffix";
// 运行时拼接,结果是一个全新的堆对象
System.out.println("2025" == c); // false!不是同一个对象!
Pool: 堆(普通区域):
┌──────────────┐ ┌──────────────┐
│ "2025"@1001 │ │ "2025"@5566 │ ← new 出来的,Pool 外面的副本
└──────────────┘ └──────────────┘
方式 3:JDBC/MyBatis 从数据库读出来的(不进 Pool)
java
// rs.getString("fiscal_year") 内部:
// 从网络 Buffer 读字节流 → new String(bytes) → 堆上全新对象
// 和 Pool 没有任何关系
三、intern() 做了什么?
intern() 就是把一个在 Pool 外面的 String,拉进 Pool 里,然后返回 Pool 里那个的地址。
java
String fromDb = rs.getString("fiscal_year"); // 堆上新对象,Pool 外
// fromDb → @5566 ("2025"),在 Pool 外
String interned = fromDb.intern();
// JVM 做了 3 步:
// 1. 去 Pool 里查:有没有内容等于 "2025" 的字符串?
// 2a. 有!→ 直接返回 Pool 里那个的引用(@1001)
// 2b. 没有!→ 把 fromDb 这个对象放进 Pool,返回它的引用
// 结果:interned → @1001(Pool 里的那个)
// fromDb → @5566(Pool 外那个,没人引用了,等 GC 清掉)
用流程图表示:
调用 intern() 之前:
Pool: 堆(普通区域):
┌──────────────┐ ┌──────────────┐
│ "2025"@1001 │ │ "2025"@5566 │ ← fromDb 指向这里
└──────────────┘ └──────────────┘
调用 intern() 之后:
Pool: 堆(普通区域):
┌──────────────┐ ┌──────────────┐
│ "2025"@1001 │ │ "2025"@5566 │ ← 没人引用了,等 GC
└──────────────┘ └──────────────┘
↑
interned(和之后存入 DO 的字段)都指向这里
四、节省内存的原理
没有 intern,10000 条数据都有 fiscalYear = "2025":
DO_1.fiscalYear → @1001 ("2025")
DO_2.fiscalYear → @1002 ("2025") ← 内容一样,但是不同对象
DO_3.fiscalYear → @1003 ("2025") ← 又一个
...
DO_10000.fiscalYear → @10000 ("2025") ← 又一个
堆里有 10000 个内容相同的 "2025" String 对象
每个对象 ~72 字节
10000 × 72 = 720KB,全部在 Old Gen 占着
有了 intern,10000 条数据同样的情况:
DO_1.fiscalYear → @1001(Pool 里的 "2025")
DO_2.fiscalYear → @1001(同一个!)
DO_3.fiscalYear → @1001(同一个!)
...
DO_10000.fiscalYear → @1001(同一个!)
堆里只有 1 个 "2025" 对象,在 Pool 里
那 9999 个 rs.getString() 创建出来的临时对象全被 GC 快速清掉
内存:10000 × 72字节 → 1 × 72字节,节省 99.99%
五、回到代码里那个例子
java
// KafkaReportListener.java 第 84 行:
StringUtils.equalsIgnoreCase("M&S".intern(), taskEntity.getReportCatalog())
这里的 "M&S".intern() 是多余的,完全没有意义!
java
String s = "M&S";
// "M&S" 是字符串字面量
// 编译时 JVM 就已经把它放进 Pool 了
// 它本身就已经是 Pool 里的对象!
"M&S".intern()
// 等于:去 Pool 里查有没有 "M&S" → 有!就是你自己!→ 返回自己
// 相当于绕了一圈,什么都没做
intern() 真正有意义的用法:
java
// ✅ 有意义:对运行时产生的字符串 intern
String fromDb = rs.getString("fiscal_year"); // 堆上全新对象
this.fiscalYear = fromDb.intern(); // 拉进 Pool,共享
// ✅ 有意义:对拼接或计算出来的字符串 intern
String key = year + quarter;
String cached = key.intern();
// ❌ 没意义:对字面量 intern(字面量本来就在 Pool 里)
"M&S".intern() // 多此一举
"hello".intern() // 多此一举
六、intern() 的工作原理(底层)
Pool 内部是一个哈希表(类似 HashMap):
intern() 的执行步骤:
1. 计算当前 String 的 hashCode
│
▼
2. 在 Pool 的哈希表里查找
│
├── 找到了(内容相同的对象已存在)
│ └── 返回 Pool 里那个对象的引用
│ 当前这个对象变孤儿,等 GC
│
└── 没找到(第一次出现这个内容)
└── 把当前对象加入 Pool 的哈希表
返回当前对象的引用
时间复杂度:O(1) 平均,和 HashMap.get() 一样快
七、intern() 的代价和注意事项
代价 1:查哈希表有开销
每次调用 intern(),都要做哈希表查找
高频调用(百万次/秒)时,这个开销不可忽视
对于短命对象,不如直接让 Minor GC 清掉
代价 2:Pool 里的对象不会被 GC(Java 6)
Java 6 及以前:
Pool 在 PermGen,GC 不管
intern 太多 → PermGen 撑满 → OOM
Java 7+:
Pool 搬到堆里,可以被 GC
但 GC 回收 Pool 里的对象条件较苛刻(需要没有任何引用)
高基数字段 intern(如 userId、orderId)→ Pool 无限膨胀 → OOM
什么时候用 intern() 合适:
✅ 适合:
字段取值范围固定,重复率高(低基数)
对象生命周期长,会晋升 Old Gen
例:fiscalYear("2025"等)、geo("AP"等)、quarter("Q1"等)
❌ 不适合:
字面量(已经在 Pool 里)
短命对象(Minor GC 会清,不用 intern)
高基数字段(orderId、userId,几乎不重复,intern 只会撑爆 Pool)
用户输入的自由文本(不可预测,不适合 intern)
八、总结
intern() = 把堆上的 String 拉进"全局公告栏"(String Pool)
Pool 的规则:同一内容只存一份
intern() 做的事:
1. 去 Pool 查有没有内容相同的
2. 有 → 返回 Pool 里的,当前对象成为垃圾
3. 没有 → 把当前对象注册进 Pool,返回自己
节省内存的本质:
N 个内容相同的 String → Pool 里只剩 1 个
N-1 个临时对象被 GC 清掉
正确使用场景:
低基数、长生命周期的字符串字段(如各种维度字段)
错误使用场景:
字面量直接调用 intern()(多此一举)
高基数字段 intern()(撑爆 Pool)
短命对象 intern()(加了 ConcurrentHashMap 查找开销,得不偿失)