intern() 方法(原理/面试题/练习)
intern() 的核心语义
String.intern() 是 JVM 提供的一个 native 方法,它的作用是:将字符串加入 StringTable,并返回 StringTable 中的标准引用。
这个方法看似简单,但背后藏着 JDK 6 和 JDK 8 之间最著名的行为差异。
intern() 的工作原理
java
public class Intern原理 {
public static void main(String[] args) {
// intern() 的执行逻辑:
String s = new String("hello");
String interned = s.intern();
// intern() 检查 StringTable
// 如果 "hello" 已在 StringTable 中 → 返回 StringTable 中的引用
// 如果不在 → 把当前字符串加入 StringTable,返回 StringTable 中的引用
}
}intern() 的三种情况
调用 intern() 时:
│
▼
检查 StringTable
│
├── "hello" 已在 StringTable 中
│ │
│ └── 返回 StringTable 中已有的引用
│
└── "hello" 不在 StringTable 中
│
└── JDK 6: 复制 "hello" 到 PermGen 的 StringTable,返回复制后的引用
└── JDK 7+: 把对 "hello" 的引用添加到 StringTable,返回该引用核心区别
| 版本 | 不存在时的行为 | 内存位置 |
|---|---|---|
| JDK 6 | 复制字符串到 PermGen StringTable | PermGen(堆内) |
| JDK 7+ | 把字符串引用添加到 StringTable | 堆(StringTable 移入堆) |
JDK 6 vs JDK 8 的经典面试题
题目一
java
public class InternInterview1 {
public static void main(String[] args) {
String s1 = new StringBuilder("计算机").append("技术").toString();
System.out.println(s1 == s1.intern());
}
}| JDK 6 | JDK 7+ |
|---|---|
false | true |
解析:
s1 = "计算机技术"(new StringBuilder 生成的新 String 对象)s1.intern()在 JDK 6 中把「计算机技术」复制到 PermGen StringTable,返回 PermGen 中的引用 ≠ s1- JDK 7+ 把「计算机技术」的引用存入 StringTable,返回的就是 s1 本身 →
s1 == s1.intern()=true
题目二
java
public class InternInterview2 {
public static void main(String[] args) {
String s1 = new String("ja") + new String("va");
System.out.println(s1 == s1.intern());
}
}| JDK 6 | JDK 7+ |
|---|---|
false | true |
解析:同上。"java" 是 new StringBuilder 拼接的结果,intern() 后在 JDK 7+ 中返回的就是 s1 本身。
题目三
java
public class InternInterview3 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s2); // false
System.out.println(s1 == s2.intern()); // true
}
}解析:
"abc"字面量在编译时就进入了 StringTables1指向 StringTable 中的对象new String("abc")在堆中创建新 String 对象,s2指向它s2.intern()返回 StringTable 中的标准引用,与s1相同
intern() 的使用场景
场景一:减少字符串对象数量
java
public class InternSaveMemory {
public static void main(String[] args) {
// 场景:从网络读取大量重复的 JSON 字段
String[] fields = new String[1_000_000];
for (int i = 0; i < 1_000_000; i++) {
String field = readFromNetwork(); // 返回大量重复的字符串
// 不 intern:1_000_000 个 String 对象
// intern:只有 StringTable 中有 "status" 等几个字符串
fields[i] = field.intern();
}
// fields 数组中的字符串共享 StringTable 中的引用
// 节省了大量堆内存
}
}场景二:保证字符串唯一性
java
public class InternUnique {
private static final String POOL_USER = "user";
private static final String POOL_ADMIN = "admin";
public static void main(String[] args) {
// 使用 intern 保证全局只有一份
String input = "user";
boolean isUser = input == POOL_USER;
boolean isAdmin = input == POOL_ADMIN;
}
}场景三:类加载器隔离的字符串比较
在需要区分不同类加载器加载的同名类时,可以用 String.intern() 保证类名字符串的一致性:
java
public class ClassNameComparison {
public static void main(String[] args) {
ClassLoader loader1 = new CustomLoader();
ClassLoader loader2 = new CustomLoader();
String name1 = loader1.loadClass("com.example.MyClass").getName();
String name2 = loader2.loadClass("com.example.MyClass").getName();
// 即使两个类由不同加载器加载,类名相同
// intern 后可以比较字符串
System.out.println(name1.intern() == name2.intern()); // true
}
}intern() 的代价
intern() 不是免费的午餐:
| 代价 | 说明 |
|---|---|
| 查询开销 | 每次调用都要在 StringTable 中查找 |
| StringTable 膨胀 | 大量 intern 会撑大 StringTable |
| PermGen OOM(JDK 6) | intern 的字符串进入 PermGen,容易 OOM |
| 启动变慢 | StringTable 越大,查找越慢 |
过度 intern 的问题
java
public class InternOveruse {
public static void main(String[] args) {
// 模拟过度 intern
for (int i = 0; i < 10_000_000; i++) {
// ❌ 不要 intern 随机生成的字符串
String random = UUID.randomUUID().toString();
random.intern(); // UUID 数量无限,StringTable 会爆炸
}
}
}正确做法:只 intern 少量固定的字符串(如配置值、枚举值)。
intern() 与 JIT 编译器
JIT 编译器对 intern() 做了特殊优化:
- 如果 JIT 确认某个字符串一定会调用
intern(),可能会在编译时就直接指向 StringTable 中的对象 - 这种优化叫 String Constant Folding
java
public class InternJIT {
public static void main(String[] args) {
String s = new String("hello").intern();
// JIT 可能优化成 String s = "hello";
// 因为 "hello" 字面量已经在 StringTable 中
}
}本节小结
intern() 方法的核心要点:
| 关键点 | 说明 |
|---|---|
| 作用 | 把字符串加入 StringTable,返回 StringTable 中的引用 |
| JDK 6 行为 | 复制字符串到 PermGen StringTable |
| JDK 7+ 行为 | 把字符串引用添加到堆中的 StringTable |
| 使用场景 | 减少重复字符串、保证字符串唯一性 |
| 注意事项 | 不要过度 intern,随机字符串不要 intern |
理解 intern() 的关键是记住:JDK 6 和 JDK 7+ 的区别是复制 vs 引用。
下一节,我们来看 StringTable 垃圾回收/G1 去重。
