JVM学习-StringTable字符串常量池

StringTable

  • 1.StringTable的特性
    • 1.1.面试题
    • 1.2.常量池与串池的关系
    • 1.3.字符串的拼接
    • 1.4.编译器优化
    • 1.5.intern方法
      • 1.5.1.intern方法(1.8)
      • 1.5.2.intern方法(1.6)
    • 1.6.StringTable的特性总结
  • 2.StringTable位置
  • 3.StringTable垃圾回收
  • 4.StringTable性能调优

1.StringTable的特性

1.1.面试题

先看几道面试题:

String s1 = "a"; 
String s2 = "b"; 
String s3 = "a" + "b"; 
String s4 = s1 + s2; 
String s5 = "ab"; 
String s6 = s4.intern();
// 问 
System.out.println(s3 == s4); 
System.out.println(s3 == s5); 
System.out.println(s3 == s6);
String x2 = new String("c") + new String("d"); 
String x1 = "cd"; x2.intern();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢 
System.out.println(x1 == x2);

以上代码的运行结果如何,如果尚有疑惑,请看下面的分析

1.2.常量池与串池的关系

public static void main(String[] args) {
     
    String s1 = "a"; // 懒惰的
    String s2 = "b";
    String s3 = "ab";
}

反编译Demo1.class(之前需要运行将.java文件生成.class文件)
在这里插入图片描述

F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\Demo1.class

常量池中信息

Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."":()V
   #2 = String             #25            // a
   #3 = String             #26            // b
   #4 = String             #27            // ab
   #5 = Class              #28            // Demo1
   #6 = Class              #29            // java/lang/Object
   ......

main方法字节码信息

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: return
      LineNumberTable:
        line 9: 0
        line 10: 3
        line 11: 6
        line 14: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
            3       7     1    s1   Ljava/lang/String;
            6       4     2    s2   Ljava/lang/String;
            9       1     3    s3   Ljava/lang/String;

可以看到ldc #2,也就是到常量池中2号位置加载信息,这个例子中2号位置对应字符串对象String a(注释中),然后astore_1也就是把加载好的a字符串对象存入1号的局部变量。低下的 LocalVariableTable:也就是main方法栈帧运行时局部变量表中的变量,编号是1.同理,ldc #3,String b存入到局部变量表中2号位置去,ab字符串存入到3中去。

看完上面代码之后,我们要理清楚常量池与串池的关系。
编译后常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
当具体执行到引用它的代码上,就会变成java字符串对象
ldc #2 会把 a 符号变为 “a” 字符串对象
在变为a字符串对象之后,还要准备好一块空间,即StringTable字符串常量池,刚开始里面是空的,将a变为字符串对象之后,就会去StringTable中找,看有没有相同的key,在数据结构上是一个hash表,长度固定,不能扩容。如果没有,就会把a放入串池
只有执行到用到它的代码,才开始创建字符串对象,放入到常量池中
它们在行为上是懒惰的

如果串池中有了,就会使用串池中的对象
ldc #3 会把 b 符号变为 “b” 字符串对象
ldc #4 会把 ab 符号变为 “ab” 字符串对象
最终
StringTable [ “a”, “b” ,“ab” ] hashtable 结构,不能扩容

1.3.字符串的拼接

public static void main(String[] args) {
     
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
    }

将上述代码反编译,生成反编译结果,对于String s4 = s1 + s2;部分的代码如下:

  		9: new           #5                  // class java/lang/StringBuilder
        12: dup
        13: invokespecial #6                  // Method java/lang/StringBuilder."":()V
        16: aload_1
        17: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        20: aload_2
        21: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        27: astore        4

它先创建了StringBuilder()对象,然后调用了init()构造方法。aload_1把s1这个参数加载进来,即从局部变量表中拿到s1参数a,与astore1相反。接下来调用了append方法,s1作为参数。aload2又把s2拿到了,即字符串b,作为append方法。最后使用了一个toString方法,最后astore_4,即把toString转换后的结果存入到4号的局部变量中。
我们可以看看StringBuilder的toString原码

public String toString() {
     
        // Create a copy, don't share the array
        return new String(value, 0, count);
}

即创建了一个新的值为ab的对象,存入到s4这个对象中。

String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
System.out.println(s3==s4);

结果是false

虽然它们值是相同的,但是s3是在串池中,而s4是new出来的字符串对象,是在堆里面的。它们是两个对象。所以打印false

1.4.编译器优化

public static void main(String[] args) {
     
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
    }

反编译
生成的最后一行代码的字节码如下:

29: ldc           #4                  // String ab
31: astore        5

我们可以看到,它不是先找a再找b最后拼接到一起,而是直接找到的就是直接拼接好的ab这个符号,并且存入到局部变量里面,下标为5的位置。

而这是String s3 = “ab”;的字节码

6: ldc           #4                  // String ab
8: astore_3

在开始的时候,串池没有ab这个对象,s3创建出来了,就放入到串池,而s5再次引用ab这个字符串时候,就先会去字符串常量池中找,找到了,然后就会直接用串池中字符串对象。所以存储s3的对象和存储s5的对象,都是串池中的字符串对象。
最后发现下面结果输出为真。

String s3 = "ab";
String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
System.out.println(s3 == s5);

结果为true
javac 在编译期间的优化,结果已经在编译期确定为ab
而上面拼接的是变量,既然是变量,说明以后引用的时候可能会发生修改,所以值不能确定。所以在运行期间用stringbuilder的方式来拼接。

1.5.intern方法

1.5.1.intern方法(1.8)

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功
  • 如果有该字符串对象,则放入失败

无论放入是否成功,都会返回串池中的字符串对象

注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

我们有以下代码

String s=new String("a")+new String("b");

经过前面学习,我们知道了
常量a,b都被放入到了常量池中
new出来的两个对象String(“a”)和String(“b”)被放入到了堆中。他们值虽然相同,但是对象不同。
s又引用了对象,new String(“ab”),它只存在于堆中,不存在串池中。
即常量池中存在[“a”,“b”]
而堆中存在new String(“a”),new String(“b”),new String(“ab”)
我们能不能把ab存入到常量池中呢,可以通过intern方法
intern方法将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,会把串池中的对象返回。

public class Main {
     
	public static void main(String[] args) {
     
		//"a" "b" 被放入串池中,str则存在于堆内存之中
		String str = new String("a") + new String("b");
		//调用str的intern方法,这时串池中没有"ab",则会将该字符串对象放入到串池中,此时堆内存与串池中的"ab"是同一个对象
		String st2 = str.intern();
		//给str3赋值,因为此时串池中已有"ab",则直接将串池中的内容返回
		String str3 = "ab";
		//因为堆内存与串池中的"ab"是同一个对象,所以以下两条语句打印的都为true
		System.out.println(str == st2);
		System.out.println(str == str3);
	}
}

返回结果为true,true
如果将在最开始定义了String x="ab"呢

public class Main {
     
	public static void main(String[] args) {
     
        //此处创建字符串对象"ab",因为串池中还没有"ab",所以将其放入串池中
		String str3 = "ab";
        //"a" "b" 被放入串池中,str则存在于堆内存之中
		String str = new String("a") + new String("b");
        //此时因为在创建str3时,"ab"已存在与串池中,所以放入失败,但是会返回串池中的"ab"
		String str2 = str.intern();
        //false
		System.out.println(str == str2);
        //false
		System.out.println(str == str3);
        //true
		System.out.println(str2 == str3);
	}
}

1.5.2.intern方法(1.6)

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中

如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中
如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象

注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象

1.6.StringTable的特性总结

常量池中的字符串仅是符号,只有在被用到时才会转化为对象

利用串池的机制,来避免重复创建字符串对象

字符串变量拼接的原理是StringBuilder

字符串常量拼接的原理是编译器优化

可以使用intern方法,主动将串池中还没有的字符串对象放入串池中

注意:无论是串池还是堆里面的字符串,都是对象

用来放字符串对象且里面的元素不重复

面试结果如下:

public static void main(String[] args) {
     
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b"; // ab
        String s4 = s1 + s2;   // new String("ab")
        String s5 = "ab";
        String s6 = s4.intern();

// 问
        System.out.println(s3 == s4); // false
        System.out.println(s3 == s5); // true
        System.out.println(s3 == s6); // true

        String x2 = new String("c") + new String("d"); // new String("cd")
        x2.intern();
        String x1 = "cd";

// 问,如果调换了【最后两行代码】的位置呢
        System.out.println(x1 == x2);	//调换前true,调换后fals
    }

此时所有的问题是不是都迎刃而解了

2.StringTable位置

JVM学习-StringTable字符串常量池_第1张图片

在1.6中是常量池的一部分,随常量池存储在永久代中,1.7开始之后就放入到了堆中。因为永久代的内存回收效率很低,永久代很难触发垃圾回收,需要等到老年代空间不足才会回收,触发效率不高。Springtable是非常常用的,放入到常量池中容易导致永久代内存不足。而堆里面Springtable触发垃圾回收的情况比较简单。

3.StringTable垃圾回收

StringTable在内存紧张时,会发生垃圾回收。

4.StringTable性能调优

SpringTable底层是哈希表,哈希表性能与其大小有关,如果哈希表桶的个数比较多,元素分散,哈希碰撞的几率就比较小,查找效率较高。
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间

-XX:StringTableSize=xxxx

考虑是否需要将字符串对象入池
可以通过intern方法减少重复入池,相同的地址intern之后,在内存中只会存储一份,这样就能减少字符串对内存占用。

你可能感兴趣的:(JVM,Java,java,jvm)