【String类】 intern的前因后果
引言
String字符串最为最高频的类型,它的操作性能会直接影响整个程序的性能。
Java中八种基本类型的包装类的大部分以及特殊类型String都实现了常量池技术,它们是Byte、Short、Integer、Long、Character、Boolean,另外两种浮点数类型的包装类(Float、Double)则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值在-128到127时才可使用对象池
常量池会保证:
- 一样的值只会在内存中保存一份
- 读取常量池的值更快速(主要指jdk6上)
- 避免重复创建对象的时间消耗
背景:String常量池
jdk 6中字符串常量池位于内存分区的方法区内, 而jdk 7中字符串常量池从方法区移至堆中。
字符串常量池本质上是一个Hashtable<String>,默认大小为1009。jdk 7 中支持使用-XX:StringTableSize参数来指定字符串常量池对应的HashTable的大小。
字符串放入常量池的时机
编译期:java代码中通过双引号直接声明的字符串(如:String s = "test"), 或者静态编译优化后的常量(如 String s = "test1"+"test2"), 或者JIT优化时产生的常量字符串都会在编译期放入字符串常量池。
运行期:运行期无法将字符串新增至字符串常量池,除非使用String.intern()方法。
intern源码(基于jdk8)
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
注意:
- 当常量池没有本字符串时,新增字符串到常量池,并返回常量池中新字符串对象的引用。
- 当常量池有本字符串时,直接返回常量池中字符串对象。
为什么要用intern
- 避免同一对象在内存中保存多份 ==> 节省内存空间
jdk7上常量池内存空间的变化及intern变化
jdk7在字符串常量池上的主要变化是,将其由方法区移至堆中。
相应的其String.intern方法的行为也有如下变化 :
jdk6的字符串常量池保存的是字符串对象,而jdk7中字符串常量池可以保存对于堆中原有字符串对象的** 引用** 。
实例说明
下面通过两组例子实际检验下对以上知识点的理解, 请分别写下jdk6和jdk7上你的答案,再查看正确答案
实例1
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class StringInternTest {
public static void main(String[] args) {
try {
String s = new String("1");
String t = s.intern();
long addressS = addressOf(s);
long addressT = addressOf(t);
String s2 = "1";
long addressS2 = addressOf(s2);
System.out.println("s: "+addressS);
System.out.println("s.intern: "+addressT);
System.out.println("s2 : "+addressS2);
System.out.println(s==s2);
System.out.println(t==s2);
String s3 = new String("1") + new String("1");
String t3 = s3.intern();
String s4 = "11";
long addressS3 = addressOf(s3);
long addressT3 = addressOf(t3);
long addressS4 = addressOf(s4);
System.out.println("s3: "+addressS3);
System.out.println("s3.intern: "+addressT3);
System.out.println("s4: "+addressS4);
System.out.println(s3 == s4);
System.out.println(t3 == s4);
}catch (Exception e){
System.out.println(e.getMessage());
}
}
private static Unsafe unsafe;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
}
public static long addressOf(Object o) throws Exception {
Object[] array = new Object[] { o };
long baseOffset = unsafe.arrayBaseOffset(Object[].class);
int addressSize = unsafe.addressSize();
long objectAddress;
switch (addressSize) {
case 4:
objectAddress = unsafe.getInt(array, baseOffset);
break;
case 8:
objectAddress = unsafe.getLong(array, baseOffset);
break;
default:
throw new Error("unsupported address size: " + addressSize);
}
return (objectAddress);
}
}
写好你的答案了么?来比较一下正确答案吧。
jdk6:
s: 4267994917 // String s = new String("1"); 编译期会向常量池中放入"1",运行期在堆中生成新的对象,引用常量池中的"1". s指向堆中新对象的地址
s.intern: 4284831160 //会检查“1”是否在常量池,如果不在会把“1”放入常量池,注意这里并未改变s的指向。与s = s.intern()操作不同。
s2 : 4284831160 //String s2 = "1"; s2指向常量池中的“1”
false // s==s2? 不正确,s指向堆中新对象,s2指向常量池中的“1”。
true // s.intern==s2? 正确,s.intern()不论“1”是否是常量池中己存在,返回的都是“1”在常量池中的对象地址,与s2一致。
s3: 4267995168 // String s3 = new String("1") + new String("1"); 编译期会将“1”放入常量池,静态编译时会把此语句转换为StringBuilder.append操作。 运行时在堆中生成两个临时对象和一个新的字符串对象,但是最终s3指向的是堆中新对象的地址。此时常量池中并没有“11”这个字符串。
s3.intern: 4284831191 //检查“11”是否存在于常量池,不存在则将其放入常量池,此时s3.intern()返回的是常量池中此对象的地址,而s3本身还是指向堆中新对象的地址。
s4: 4284831191 // String s4 = "11"; 指向常量池中“11”对象。与s3.intern()一致。
false // s3==s4? s3指向堆中新对象地址,s4指向常量池中地址。
true //s3.intern()==s4? s3.intern()返回的是常量池中字符串所在地址,s4也是返回常量池中字符串所在地址,因此一致。
jdk7:
s: 4071646725 // String s = new String("1"); 编译期会向常量池中放入"1",运行期在堆中生成新的对象,引用常量池中的"1". s指向堆中新对象的地址
s.intern: 4071646728 //会检查“1”是否在常量池,如果不在会把“1”放入常量池,注意这里并未改变s的指向。与s = s.intern()操作不同。
s2 : 4071646728 // String s2 = "1"; s2指向常量池中的“1”
false //与jdk6相同
true //与jdk6相同
s3: 4071647013 //String s3 = new String("1") + new String("1"); 编译期会将“1”放入常量池,静态编译时会把此语句转换为StringBuilder.append操作。 运行时在堆中生成两个临时对象和一个新的字符串对象,但是最终s3指向的是堆中新对象的地址。此时常量池中并没有“11”这个字符串。
s3.intern: 4071647013 //检查常量池中是否有“11”这个字符串,此时没有,则在常量池中新增对象,但是“11”这个字符串己经在堆中存在了,所以此处常量池保存的是对堆中己有“11”对象的引用。s3.intern()返回的也是这个堆中己有对象的地址。
s4: 4071647013 //String s4 = "11"; 指向常量池中“11”对象。与s3.intern()一致。都是堆中己有"11"对象地址.
true //s3,s3.intern,s4都指向堆中“11”字符串对象的地址。
true
回顾知识点:
- jdk7中字符串常量池不再只保持字符串对象,如果该字符串在堆中己经存在,则可以在常量池中保存对堆内己有对象的引用。
- new出来的字符串的intern方法返回的地址,与直接用双引号声明的字符串所对应的地址,永远是一致的,无论是jdk7还是jdk6. 参见上例s.intern==s2, s3.intern==s4
实例2
本例与上例的主要不同在于把intern操作的顺序后移:
public static void main(String[] args) {
try {
String s = new String("1");
String s2 = "1";
String t = s.intern();
long addressS = addressOf(s);
long addressT = addressOf(t);
long addressS2 = addressOf(s2);
System.out.println("s: "+addressS);
System.out.println("s.intern: "+addressT);
System.out.println("s2 : "+addressS2);
System.out.println(s==s2);
System.out.println(t==s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
String t3 = s3.intern();
long addressS3 = addressOf(s3);
long addressT3 = addressOf(t3);
long addressS4 = addressOf(s4);
System.out.println("s3: "+addressS3);
System.out.println("s3.intern: "+addressT3);
System.out.println("s4: "+addressS4);
System.out.println(s3 == s4);
System.out.println(t3 == s4);
}catch (Exception e){
System.out.println(e.getMessage());
}
}
写好你的答案了么?来比较一下正确答案吧。
jdk6:
s: 4071561516
s2 : 4071561519
s.intern: 4071561519
false
true //至此与上例一致,不过多解释
s3: 4071561804
s4: 4071561810
s3.intern: 4071561810
false
true
调整intern调用次序在jdk6上并不影响结果。
jdk7:
s: 4071561516
s2 : 4071561519
s.intern: 4071561519
false
true //至此与上例结果一致,不过多解释
s3: 4071561804 //String s3 = new String("1") + new String("1"); 编译期会将“1”放入常量池,静态编译时会把此语句转换为StringBuilder.append操作。 运行时在堆中生成两个临时对象和一个新的字符串对象,但是最终s3指向的是堆中新对象的地址。此时常量池中并没有“11”这个字符串?不对,因为整个java程序是一起编译的,编译期执行String s4="11"时,会把“11”放到常量池里。
s4: 4071561810 //String s4 = "11"; 检查常量池,没有“11”字符串,新增。此时s4指向常量池中的“11”。
s3.intern: 4071561810 //运行时执行,先去常量池找“11”,找到了,所以返回常量池中“11”的地址。
false // s3==s4? s3指向堆中新对象,s4指向常量池,因此不一致。
true // s3.intern()==s4 ? s3.intern指向常量池,与s4一致。
intern可能导致的问题
intern的设计本质上是以时间换空间的优化方法,因为在执行intern过程中首先要检查全局HashTable<String>,多线程的情况下涉及锁等资源的消耗。
而intern在使用时要特别注意以下两个问题:
- 性能问题:
常量池底层是靠Hashtable实现,hashtable中不可避免的一个问题就是hash值冲突的问题。而常量池所依赖的Hashtable跟Hashmap一样,是靠每个hash值对应一个单链表的方式(即为拉链法)来解决hash冲突。
拉链法在两种情况下性能下降非常快
-
扩容
-
保存的字符串过多,hash碰撞激烈,每个hash值存储的单链表过常。(查找时需要遍历整个单链表)
而Hashtable不存在扩容的情况,它的大小是jvm启动时指定的。jdk6上是默认的1009,jdk7上可以通过jvm参数指定。总结:
所以保存同样数量的字符串类型,jdk6上更可能遇到读取效率差的问题。而jdk7上可以通过适量增大字符串常量池大小来避免这个性能问题。
但总的来说,intern()并不适用于大量保存字符串信息的场景 。
而更适合可控数量字符串,并且这些字符串很高频的被重用的场景,如编码类型等。
-
内存问题:
jdk 6上使用intern可能遇到OutOfMemoryError: PermGen space 的问题。
这是因为java 6上字符串常量池在PermGen, 也就是永久代中(注意其实这种方法并不准确,在jvm spec上规定的这个区域叫方法区,只是HotSpot这种特定的虚拟机上选择把GC分代收集扩展到了方法区,或者说使用永久代来实现方法区而且。其他虚拟机如BEA JRockit/IBM J9等并不存在永久代的概念)。永久代主要用于存储静态的类信息和方法信息,静态方法和静态变量,以及final标注的常量信息等。PermGen的大小是相对固定的(JVM参数-XX:PermSize和-XX:MaxPermSize),而且这个内存空间虽然也会被GC管控,但是一个类要被回收掉,条件非常苛刻。
所以在jdk 6上不建议使用intern方法。
参考资源