关于 String.intern()

《深入理解java虚拟机》一书中对 String.intern() 方法是这样描述:

String.intern()方法是一个Native方法, 它的作用是:如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的string对象;否则,将此String对象包含的字符串添加到常量池中,并返回此String对象的引用。

先看测试案例:


public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {

        String str1 = new StringBuilder("计算机").append("软件").toString();

        System.out.println(str1.intern() == str1);

        String str2 = new StringBuilder("Ja").append("va").toString();

        System.out.println(str2.intern() == str2);

    }

}

运行结果:

JDK1.6以及以下:false false
JDK1.7以及以上:true  false 

JDK1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串的实例的引用,而StringBulder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。

在JDK1.7中,intern()的实现不会在复制实例,只是在常量池中记录首次出现的实例引用,因此返回的是引用和由StringBuilder.toString()创建的那个字符串实例是同一个。

str2的比较返回false因为"java"这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串是首次出现,因此返回true。

"java" 这个字符串不是首次出现,说明是在某一个地方系统已经使用过了。

这里可以翻看jdk源码查到,我们知道System类是由虚拟机自动调用的,在System类中的 initializeSystemClass() 方法中,发现调用了sun.misc.Version 类对象的init静态方法,而Version类里 laucher_name是私有静态字符串常量,“java”直接赋值给laucher_name。

                                         

因此sun.misc.Version 类会在JDK类库的初始化过程中被加载并初始化,而在初始化时它需要对静态常量字段根据指定的常量做默认初始化,此时被 sun.misc.Version.launcher 静态常量字段所引用的"java"字符串字面量就被intern到HotSpot VM的字符串常量池了。

                                       

JDK1.6及以前,常量池是放在方法区的永久代(PermGen Space)中,JDK1.7及以后常量池被放置在了堆空间,因此常量池位置的不同影响到了String的intern()方法的表现。

深入理解: 

先看一个例子(来源于参考博客):

String s = new String("1");
s.intern();
String s2 = "1";

System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);

运行结果:

JDK1.6以及以下:false false
JDK1.7以及以上:false true

分析:

                                        

                               注:图中绿色线条代表 string 对象的内容指向。 黑色线条代表地址指向。

JDK1.6以及以前版本中,常量池是放在 Perm 区(属于方法区)中的,Perm区和正常的Java堆区完全分开的。因此所有的输出结果都是 false。

如果是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在Java堆区域。两者的内存地址不相同的,即使调用了intern()方法也是没有任何关系的。

intern()方法在JDK1.6中的作用是:比如String s = new String("1"),再调用s.intern(),此时返回值还是字符串"1"。

JDK1.6:首先检查字符串常量池里是否存在"1"字符串,如果存在,就返回常量池中的字符串;如果不存在,该方法会把"1"复制到字符串池中,然后再返回它的引用。

JDK1.7以及以上的版本: 

在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError: PermGen space错误的。 所以在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。

                                    

为方便分析,复制上一段代码:

String s = new String("1");
s.intern();
String s2 = "1";

System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);

String s = newString("1"),生成了常量池中的“1” 和堆空间中的字符串对象。

s.intern(),s对象去常量池中寻找后发现"1"已经存在于常量池中了,返回常量池中的这个字符串。

String s2 = "1",s2的引用指向常量池中的“1”对象。而s指向堆空间中的 “1”。因此返回了false。


String s3 = new String("1") + newString("1"),在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。当然中间还有2个匿名的new String("1"),这里不讨论。此时常量池中没有 “11”对象。

 s3.intern(),将 s3中的“11”字符串放入 String 常量池中,此时常量池中不存在“11”字符串,JDK1.6直接将s3复制一份到常量池,生成"11" 的对象。

但是在JDK1.7中,常量池中不再复制实例,而是直接存储堆中的引用。这里引用直接指向 s3 引用的对象,当执行String s4 = "11"时,但是发现常量池中已经有这个对象了,直接返回这个引用,此时也就是指向 s3 引用对象的一个引用。因此s3 == s4返回了true。

然后将s3.intern();语句下调一行,放到String s4 = "11";后面;将s.intern(); 放到String s2 = "1";后面

String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);

String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);

运行结果:

JDK1.6以及以下:false false
JDK1.7以及以上:false false

JDK1.7及以上:

                                         

String s = newString("1"),生成了常量池中的“1” 和堆空间中的字符串对象。

String s2 = "1",生成一个s2的引用指向常量池中的“1”对象,已经存在了,直接指向常量池中的对象。

s.intern(),"1"已经存在,返回常量池中的对象。 因此s 和 s2 的引用地址不同,返回了false。

String s3 = new String("1") + newString("1"),在字符串常量池中生成“1” ,并在堆空间中生成s3引用指向的对象(内容为"11")。此时常量池中没有 “11”对象。

String s4 = "11", 在常量池中的生成 "11"。s3.intern(),这时"11"已经存在,返回常量池中的对象。结果就是 s3 和 s4 的引用地址不同。因此返回了false。

小结
从上述的例子代码可以看出 jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点:

  • 将String常量池 从 Perm 区移动到了 Java Heap区。
  • String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。

参考: 深入解析String#intern

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值