String的intern方法
一、 核心概念:字符串常量池 (String Pool)
为了提升性能和减少内存开销,JVM 在内存堆中开辟了一块特殊的区域,称为字符串常量池。
作用:缓存所有通过字面量(如
"hello"
)创建的字符串对象,以及显式调用intern()
方法的字符串。目的:实现具有相同值的字符串只有一个副本,从而实现字符串的复用,节省内存。
位置:
JDK 1.6及之前:常量池位于方法区(永久代 PermGen)。
JDK 1.7及之后:常量池被移到了 Java 堆(Heap) 中。这个改动的好处是常量池中的字符串也能被垃圾回收器管理,避免了在永久代中可能发生的内存溢出(OutOfMemoryError)。
2. intern()
方法的作用
intern()
是一个本地方法(Native Method),它的行为非常明确:
当一个字符串调用 intern()
方法时:
检查常量池:JVM 会检查字符串常量池中是否已经存在一个与该字符串值相等(通过
equals(Object)
方法判断)的字符串对象。存在则返回引用:如果存在,则直接返回常量池中那个字符串的引用。
不存在则添加并返回:如果不存在,JDK 版本不同,行为有差异:
在 JDK 1.6 及之前:会在常量池中创建一份新的副本,然后返回这个新副本的引用。
在 JDK 1.7 及之后:不会复制整个字符串,而是在常量池中记录首次遇到的该字符串的引用,并返回这个引用。这意味着常量池中存储的实际上是堆中对象的引用。
简单来说:intern()
是一个主动将字符串放入常量池并获取其引用的操作。它的最终目的是保证任何内容相同的字符串,在常量池中有且只有一个引用。
3. 代码示例
通过几段代码来彻底理解它。
示例 1:字面变量会放入当常量池,但是new String()会创建新对象,重新开辟地址
String s1 = "hello"; // 字面量创建:编译器会确保"hello"被放入常量池
String s2 = new String("hello"); // new创建:在堆中创建一个新的String对象,内容也是"hello"
String s3 = s2.intern(); // 调用intern,尝试将s2代表的字符串放入常量池System.out.println(s1 == s2); // false -> s1在池中,s2在堆中,是两个不同的对象
System.out.println(s1 == s3); // true -> s3得到的是常量池中"hello"的引用,也就是s1的引用
图解(JDK 1.7+):
堆 (Heap) 字符串常量池 (String Pool)
+------------------+ +---------------------------+
| String对象 (s2) | | 记录引用: ----> "hello" <----+----> (s1, s3)
| value -> "hello"| +---------------------------+
+------------------+ ^| |+-----------------------+(s2.intern() 返回了这个堆对象的引用给s3,并记录在池中)
字面量
"hello"
在编译期就已进入常量池(s1
)。new String("hello")
会在堆中创建一个新对象(s2
)。s2.intern()
发现常量池中已有内容为"hello"
的字符串(即s1
),所以直接返回s1
的引用给s3
。
示例 2:JDK 1.7+ 的优化行为
String s4 = new String("world"); // 此时常量池中已有"world"(因为字面量)
String s5 = new StringBuilder().append("ja").append("va").toString();
// s5的内容是"java",但它是一个new出来的新对象String s6 = s5.intern(); // 在JDK7+中,常量池会记录s5的引用并返回
String s7 = "java"; // 字面量创建,现在会直接返回常量池中记录的引用,即s5的引用System.out.println(s5 == s6); // true in JDK7+! 因为s6拿到的就是s5本身的引用
System.out.println(s5 == s7); // true in JDK7+! 因为s7拿到的也是s5的引用
在 JDK 1.7+ 中,s5.intern()
发现常量池中没有 "java"
,它没有创建副本,而是将s5
这个堆对象的引用登记到了常量池中。所以后续任何获取 "java"
的操作,得到的都是堆中s5
这个对象的引用。
示例 3:动态拼接的字符串,运行时拼接,会在堆中创建一个新的String对象 。
String s8 = "he";
String s9 = "llo";
String s10 = s8 + s9; // 运行时拼接,会在堆中创建一个新的String对象 "hello"
String s11 = "hello"; // 字面量,来自常量池System.out.println(s10 == s11); // falseString s12 = s10.intern(); // 将s10代表的"hello"尝试放入常量池,但池中已存在(s11),返回s11的引用
System.out.println(s10 == s12); // false
System.out.println(s11 == s12); // true
4. intern()
的用途与陷阱
用途:
节省内存:这是最主要的目的。如果你的程序中有大量重复的字符串(例如从文件中读取的重复数据、网络协议中的重复命令字),使用
intern()
可以确保这些重复字符串在内存中只存在一份,极大地减少内存占用。这在处理大规模数据时非常有效。
陷阱与注意事项:
性能开销:
intern()
方法本身不是免费的。它需要检查常量池,这个过程可能涉及哈希计算和查找。如果对大量不同的字符串调用intern()
,反而会增加性能开销,并可能使常量池变得过大。JDK 版本差异:如前所述,JDK 1.6 和 JDK 1.7+ 的行为有重要区别(复制 vs. 记录引用),这在某些极端case下可能导致不同的结果。
不可控的常量池:在 JDK 1.6 中,常量池位于永久代,大小固定(通过
-XX:MaxPermSize
设置),如果intern()
过多字符串,容易导致java.lang.OutOfMemoryError: PermGen space
。虽然在 JDK 1.7+ 移到了堆中,但滥用依然可能引起堆内存溢出(Java heap space
)。intern()
的自动化:Java 编译器已经会对字面量字符串自动执行intern()
,所以通常我们不需要对直接用字面量创建的字符串再调用此方法。
5. 最佳实践与总结
谨慎使用:不要盲目地对所有字符串调用
intern()
。它应该被用作一种优化手段,而不是默认操作。适用场景:通常用于处理已知会有大量重复且生命周期较长的字符串。例如,数据库中的地区名称、枚举值、XML/JSON解析中的重复键名等。
优先使用字面量:直接使用字面量
String s = "abc"
是最佳方式,因为它由编译器自动处理并放入常量池。测试:如果你考虑使用
intern()
来优化内存,一定要进行充分的测试和性能剖析(Profiling),确保它确实带来了好处,而不是引入了新的问题。
总结
特性 | 描述 |
---|---|
方法签名 | public native String intern() |
核心作用 | 返回字符串在常量池中的唯一引用,实现字符串复用 |
JDK 1.6 | 常量池在永久代;intern() 会创建副本放入池中 |
JDK 1.7+ | 常量池在堆;intern() 会记录堆中对象的引用到池中 |
主要用途 | 节省内存,避免大量重复字符串对象的存在 |
主要风险 | 性能开销、可能引起内存溢出(尤其在JDK1.6)、需谨慎使用 |