• 近期将进行后台系统升级,如有访问不畅,请稍后再试!
  • 极客文库-知识库上线!
  • 极客文库小编@勤劳的小蚂蚁,为您推荐每日资讯,欢迎关注!
  • 每日更新优质编程文章!
  • 更多功能模块开发中。。。

浅谈String的intern

先来看一道题,谁能猜出它的输出结果?
  1. publicclassStringTest{
  2.    publicstaticvoid main(String[] args){
  3.        String s1 =newStringBuilder().append("String").append("Test").toString();
  4.        System.out.println(s1.intern()== s1);
  5.        String s2 =newStringBuilder().append("ja").append("va").toString();
  6.        System.out.println(s2.intern()== s2);
  7.    }
  8. }
答案:JDK6 下输出 false false,而 JDK6 以后输出 true false。
what?为什么这种结果?我第一次看就被吓了一跳。要说原因还得从常量池的内存结构以及 intern 方法来说起。
我们先来说说 intern 方法,这个方法很有意思,我们先看看 jdk 对它的描述
  1. /**
  2. * 返回字符串对象的规范表示形式。
  3. * 字符串常量池,初始为空,由 String 类来维护
  4. * 当 intern 方法被调用时,如果池中已经有一个字符串和传入的字符串相等(equals),
  5. * 返回池中的字符串,否则,将这个 String 对象添加到池中并返回这个 String 对象的引用。
  6. *
  7. * 因此,对于任意两个字符串 s 和 t,如果 str1.equals(str2)则有 str1.intern() == str2.intern()。
  8. */
  9. publicnativeString intern();
jdk 的描述很清晰,但是可能有人不知道字符串常量池是什么,我们先来看看什么是常量池。

1、什么是常量池

在 Java 的内存分配中,总共 3 种常量池:
  1. 字符串常量池
  2. Class 常量池
  3. 运行时常量池

1.1、字符串常量池

字符串常量池在 Java 内存区域的哪个位置?

  • 在 JDK6 及之前的版本,字符串常量池是放在 Perm Gen(也就是方法区)中。
  • 在 JDK7 版本,字符串常量池被移到了堆中了。至于为什么移到堆内,大概是由于方法区的内存空间太小了。

字符串常量池是什么?

  • 在 HotSpot VM 里实现的 string pool 功能的是一个 StringTable 类,它是一个 Hash 表,默认值大小长度是 1009;这个 StringTable 在每个 HotSpot VM 的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了 StringTable 上。
  • 在 JDK6.0 中,StringTable 的长度是固定的,长度就是 1009,因此如果放入 String Pool 中的 String 非常多,就会造成 hash 冲突,导致链表过长,当调用 String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
  • 在 JDK7.0 中,StringTable 的长度可以通过参数指定:-XX:StringTableSize=66666

字符串常量池放的是什么?

  • 在 JDK6.0 及之前版本中,String Pool 里放的都是字符串常量;
  • 在 JDK7.0 中,由于 String 的 intern()发生了改变,因此 String Pool 中也可以存放放于堆内的字符串对象的引用。
需要说明的是:字符串常量池中的字符串只存在一份!
  1. String s1 ="hello,world!";
  2. String s2 ="hello,world!";
上面的代码执行完第一行代码后,常量池中已存在 “hello,world!”,那么 s2 不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给 s2。

1.2、Class 常量池

  • 我们写的每一个 Java 类被编译后,就会形成一份 class 文件;class 文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);
  • 每个 class 文件都有一个 class 常量池。
这里有人可能要问了,什么是字面量和符号引用?
字面量包括:
  1. 文本字符串
  2. 八种基本类型的值
  3. 被声明为 final 的常量等
符号引用包括:
  1. 类和方法的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符
我们可以很容易的看到 Class 常量池的内容(执行 javap-verboseStringTest):
  1. publicclassStringTest{
  2.    publicstaticvoid main(String[] args){
  3.        String s ="hello world";
  4.    }
  5. }
下面是输出的常量池内容:
  1. Constant pool:
  2.   #1=Methodref          #4.#13         // java/lang/Object."<init>":()V
  3.   #2=String             #14            // hello world
  4.   #3=Class              #15            // com/github/StringTest
  5.   #4=Class              #16            // java/lang/Object
  6.   #5=Utf8               <init>
  7.   #6=Utf8               ()V
  8.   #7=Utf8               Code
  9.   #8=Utf8               LineNumberTable
  10.   #9=Utf8               main
  11.  #10=Utf8               ([Ljava/lang/String;)V
  12.  #11=Utf8               SourceFile
  13.  #12=Utf8               StringTest.java
  14.  #13=NameAndType        #5:#6          // "<init>":()V
  15.  #14=Utf8               hello world
  16.  #15=Utf8               com/github/StringTest
  17.  #16=Utf8               java/lang/Object
我们可以看到字符串 hello world 在#14 常量池中的定义。
在 main 方法的字节码指令中, Strings="hello world"; 由两部分组成:
  1. publicstaticvoid main(java.lang.String[]);
  2.    descriptor:([Ljava/lang/String;)V
  3.    flags: ACC_PUBLIC, ACC_STATIC
  4.    Code:
  5.      stack=1, locals=2, args_size=1
  6.         0: ldc           #2                  // String hello world
  7.         2: astore_1
  8.         3:return
  1. 当 StringTest 类加载到虚拟机时,”hello world”字符串在 Constant pool 中使用符号引用 symbol 表示,当调用 ldc#2 指令时,如果 Constant pool 中索引 #2 的 symbol 还未解析,则调用 C++底层的 StringTable::intern 方法生成 char 数组,并将引用保存在 StringTable 和常量池中,当下次调用 ldc#2 时,可以直接从 Constant pool 根据索引 #2 获取 “test” 字符串的引用,避免再次到 StringTable 中查找。(这个在上面字符串常量池中已经说过了)
  2. astore_1 指令将”hello world”字符串的引用保存在局部变量表中。

1.3、运行时常量池

运行时常量池存在于内存中,也就是 class 常量池被加载到内存之后的版本,不同之处是:它的字面量可以动态的添加(String#intern()),符号引用可以被解析为直接引用。
JVM 在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm 就会将 class 常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的 StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

2、原理分析

介绍完了常量池的概念以及不同版本的内存结构,我们再去看一开始的例子就能理解为什么 JDK6 下输出 false false,而 JDK6 以后输出 true false 了吧。
在 JDK6 中,常量池在永久代分配内存,永久代和 Java 堆的内存是物理隔离的,执行 intern 方法时,如果常量池不存在该字符串,虚拟机会在常量池中复制该字符串,并返回引用,所以需要谨慎使用 intern 方法,避免常量池中字符串过多,导致性能变慢,甚至发生 PermGen 内存溢出。
在 JDK7 中,常量池已经在Java堆上分配内存,执行 intern 方法时,如果常量池已经存在该字符串,则直接返回字符串引用,否则复制该字符串对象的引用到常量池中并返回,所以在 JDK7 中,可以重新考虑使用 intern 方法,减少 String 对象所占的内存空间。
对于变量 s1,常量池中没有 “StringTest” 字符串,s1.intern() 和 s1 都是指向 Java 对象上的 String 对象。
 对于变量 s2,常量池中一开始就已经存在 “java” 字符串,所以 s2.intern() 返回常量池中 “java” 字符串的引用。(因为像”java”这样出现率高的字符串,在虚拟机启动的时候,肯定已经使用过了)
对于这个问题的原理,早在很多年前 R 大就分析过了,请自行百度。

3、活学活用

偶然在知乎上发现一个问题,正好验证一下上面学的是否牢固。
在 jdk7 中,有以下代码:
  1. publicstaticvoid pushPool(){
  2.    String a ="a";
  3.    String param ="b"+ a;
  4.    //这里的"ba"为字面量不应该在类加载后就进入常量池了吗
  5.    //(查看字节码也可以看到它被放到了 constant pool),那么 param 应该不会放到 pool 中啊
  6.    System.out.println(param.intern()=="ba");
  7.    System.out.println(param =="ba");
  8. }
这里的返回是两个 true
  1. publicstaticvoid pushPool(){
  2.    String a ="a";
  3.    String param ="b"+ a;
  4.    System.out.println("ba"== param.intern());
  5.    System.out.println(param =="ba");
  6. }
这里的返回一个 true 一个 false
注意:下面有一个重要的字节码指令 ldc,ldc 字节码在下面的执行语义是:到当前类的运行时常量池(runtime constant pool,HotSpot VM 里是 ConstantPool + ConstantPoolCache)去查找该 index 对应的项,如果该项尚未 resolve 则 resolve 之,并返回 resolve 后的内容。
分析过程如下。
下面是第一个方法的字节码。
  1. publicstaticvoid main(java.lang.String[]);
  2.    descriptor:([Ljava/lang/String;)V
  3.    flags: ACC_PUBLIC, ACC_STATIC
  4.    Code:
  5.      stack=3, locals=3, args_size=1
  6.         0: ldc           #2                  // String a
  7.         2: astore_1
  8.         3:new           #3                  // class java/lang/StringBuilder
  9.         6: dup
  10.         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
  11.        10: ldc           #5                  // String b
  12.        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  13.        15: aload_1
  14.        16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  15.        19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  16.        22: astore_2
  17.        23: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
  18.        26: aload_2
  19.        27: invokevirtual #9                  // Method java/lang/String.intern:()Ljava/lang/String;
  20.        30: ldc           #10                 // String ba
  21.        32: if_acmpne     39
  22.        35: iconst_1
  23.        36:goto          40
  24.        39: iconst_0
  25.        40: invokevirtual #11                 // Method java/io/PrintStream.println:(Z)V
  26.        43: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
  27.        46: aload_2
  28.        47: ldc           #10                 // String ba
  29.        49: if_acmpne     56
  30.        52: iconst_1
  31.        53:goto          57
  32.        56: iconst_0
  33.        57: invokevirtual #11                 // Method java/io/PrintStream.println:(Z)V
  34.        60:return
  1. 【0-2】先用 ldc 把”a”送到栈顶,换句话说,会创建对象,并且会保存引用到字符串常量池中。
  2. 【3-22】new 一个 StringBuilder 对象把“b”和“a”进行拼接,接着用 ldc 把“b”送到栈顶,创建”b”的对象,并把引用保存到字符串常量池中。接着一路 append,最后调用 StringBuilder 对象的 toString 方法得到一个 String 对象(内容是 ba,注意这个 toString 方法会 new 一个 String 对象),并把它赋值给 param。注意,这里没有把 ba 的引用放入字符串常量池。
  3. 【23-40】接着调用 intern 去字符串常量池找有没有“ba”的引用,发现没有就会把“ba”对象的引用保存到字符串常量池,接着 ldc 去字符串常量池获取到刚刚保存完的地址,所以这个判断肯定是 true。
  4. 【43-57】接着获取到 param 的引用,ldc 去字符串常量池找“ba”的引用,也就是上面 param 指向的地址,所以这个判断肯定也是 true。
我们再来看看第二个方法的字节码。
  1. publicstaticvoid main(java.lang.String[]);
  2.    descriptor:([Ljava/lang/String;)V
  3.    flags: ACC_PUBLIC, ACC_STATIC
  4.    Code:
  5.      stack=3, locals=3, args_size=1
  6.         0: ldc           #2                  // String a
  7.         2: astore_1
  8.         3:new           #3                  // class java/lang/StringBuilder
  9.         6: dup
  10.         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
  11.        10: ldc           #5                  // String b
  12.        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  13.        15: aload_1
  14.        16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  15.        19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  16.        22: astore_2
  17.        23: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
  18.        26: ldc           #9                  // String ba
  19.        28: aload_2
  20.        29: invokevirtual #10                 // Method java/lang/String.intern:()Ljava/lang/String;
  21.        32: if_acmpne     39
  22.        35: iconst_1
  23.        36:goto          40
  24.        39: iconst_0
  25.        40: invokevirtual #11                 // Method java/io/PrintStream.println:(Z)V
  26.        43: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
  27.        46: aload_2
  28.        47: ldc           #9                  // String ba
  29.        49: if_acmpne     56
  30.        52: iconst_1
  31.        53:goto          57
  32.        56: iconst_0
  33.        57: invokevirtual #11                 // Method java/io/PrintStream.println:(Z)V
  34.        60:return
  1. 【0-2】同上。
  2. 【3-22】同上。
  3. 【23-40】这个时候和上面的不同了。它会用 ldc 把”ba”送到栈顶,在堆中创建一个对象,并且会保存引用到字符串常量池中。接着 param 的 intern 发现字符串常量池中已经有“ba”的引用了,就直接返回已存在的引用,但是这个引用和 param 指向的地址是不同的。不过这里比较的是“ba”和 param.intern(),所以返回 true。
  4. 【43-57】上面说了“ba”的引用和 param 指向的地址是不同的,所以这里返回 false。

丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:浅谈 String 的 intern
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

欢迎 注册账号 登录 发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00