JVM 中的StringTable

是什么

字符串常量池是 JVM 中的一个重要结构,用于存储JVM运行时产生的字符串。在JDK7以前在方法区中,存储的是字符串常量。而字符串常量池在 JDK7 开始移入堆中,随之而来的是除了存储字符串常量外,还能够存储字符串引用(由于在堆中,引用堆中的字符串常量很方便,因此能够存储引用)。这使得不少字符串的操做在 JDK7 中和在以前的版本中执行是不一样的结果。这也是为何字符串相关的问题是如此具备迷惑性的缘由之一。数组

 

底层

String:在 JDK9 以前,String 底层是使用 char 数组来存储字符串数据的,而在 JDK9 开始,使用 byte 数组+编码来代替 char 数组,这是为了节省空间,由于不一样编码的数据占空间不同,不少单位数据只须要一个 byte(8字节) 就能够存储,而使用 char(16字节)就会浪费多余的空间。app

字符串常量池:底层使用 HashTable 来存储字符串,在 JDK6 HashTable 的数组长度是1006,JDK7 开始变成了 60013,这是为了不存储字符串过多致使链表长度过长从而查询效率下降。可使用参数 -XX:StringTableSize= 来设置 StringTable 数组的长度。优化

 

常见问题

字符串相加

一、对于字符串常量相加,编译器会优化成直接相加。ui

如 String ss = "a" + "b",在编译器的优化下,实际上只会建立一个 "ab" 字符串。编码

而 final String s1 = "a"; String s2 = s1+"b",除了建立字符串 "a" 外,只会建立 "ab"。spa

操做相关字符串以下:3d

能够看到只对字符串 "a"、"ab" 进行了入池操做(ldc)code

二、对于包含字符串变量的相加,不会在字符串常量池中建立对应的字符串。
如 String s1 = "a"; String s2 = s1 + "b",执行完后字符串常量池中只会包含 "a"、"b" 字符串。
对象

对于 s1 + "b",下面是其字节码操做blog

能够看到,相加操做其实是调用 StringBuilder 的append 方法进行字符串拼接,而后调用它的 toString 方法获取返回值保存输出,期间并无入池操做(ldc)。

 

由此得出的优化建议:由于每次执行一次包含很是量的字符串相加时,都进行了一次 StringBuilder 对象的建立,因此若是须要屡次链接,能够直接建立 StringBuilder 对象,使用一个 StringBuilder 对象进行字符串拼接,避免建立多个对象下降效率。


对象建立数量

对象,包括 new 的对象以及字符串对象。

一、对于String ss = new String ("ab"),这个过程首先会在会在字符串常量池中建立一个 "ab" 字符串常量,而后再在堆上建立一个 new String() 的对象,在这个对象中会保存常量池中 "ab" 的地址信息,最后在栈上建立一个局部变量 ss ,保存堆中建立的对象地址。因此全程建立了堆中的一个对象和字符串常量池中的一个对象。

二、new String("a") + new String("b")。严格来看,建立了六个对象。

首先new String("a") 和 new String("b") ,分为建立了两个对象。二者相加时,会建立一个 StringBuilder 对象,而在 StringBuilder.toString()方法中,也会建立一个 String 对象

 三、String s1 = "a", String s2 = "b",  String s3 = "a" + "b" + s1 + "c" + s2; 对应的字节码以下:

字符串常量池中会有四个字符串对象,分别是 "a"、"b"、"ab"、"c"。在开始由于 s一、s2 的赋值,会将 "a"、"b" 分别加入字符串常量池,而后执行第三步,运行顺序是从左到右,首先执行 "a" + "b" ,由于两个都是常量,因此会由于编译器的优化直接返回 "ab",而且由于计算的两个参数都是常量,因此直接加入字符串常量池,随后由于与变量 s1 相加,因此调用 StringBuilder的append 方法,获得的结果保存到局部变量表中,因此引入常量 "c",由于是常量,因此仍是会引入字符串常量池,而后与前面拼接获得的结果再次拼接,最后再与变量 s2 相加,由于不是常量因此仍是不会将结果加入字符串常量池。

 

除此以外,还须要注意,上面三种状况是在初始状况下,也就是字符串常量池中没有要加入的字符串时的场景,若是字符串常量池中预先就包含要加入的字符串,那么就会直接将常量池中的对应的字符串地址返回给调用方。好比 String s1 = "a",在常量池中没有 "a" 时,建立的对象是 1个,而若是常量池中已经存在,那么就会将其地址直接返回赋给 s1。那么建立的对象就是 0个了。

 

intern() 与字符串相等判断

intern() 方法是 String 类的一个native方法,做用是尝试将调用这个方法的字符串对象加入字符串常量池中,而后返回常量池中存储的值。在开头说过,在 JDK7 开始字符串常量池能够存储字符串引用,致使字符串操做的过程可能会以前不同,从而获得不一样的结果。

intern() 方法的执行:

1.6 及以前:尝试将当前字符串常量加入常量池,若是常量池存在就返回地址值;若是不存在就先加入常量池,而后再返回加入位置的地址值。

1.7开始:尝试将当前字符串常量加入常量池,若是存在就将返回地址值;若是不存在就存入当前 String 字符串的地址值。

下面以一个例子来解释一下,在JDK7和JDK7以前下面代码执行分别是什么结果。

 1     @Test
 2     public void test1(){
 3         String s = new String("1");
 4         s.intern();
 5         String s2 = "1";
 6         System.out.println(s == s2);
 7 
 8 
 9         String s3 = new String("1") + new String("1");
10         s3.intern();
11         String s4 = "11";
12         System.out.println(s3 == s4);
13     }    

先说结论:

JDK7 以前: false、false。

JDK7 及以后:false、true。

缘由:

一、首先先看上面 3------6 行的,首先,第三行会在字符串常量池中添加 "1" ,而后在堆中建立一个对象,保存 "1" 在常量池中的地址,再在局部变量表中添加一个 s 保存堆中对象的地址。随后执行第四行,此时 s 指向的字符串已经在常量池中了,因此这一步无效,第五行由于常量池已经存在 "1" ,因此 JDK7或以前执行的逻辑是同样的,直接将 "1" 在常量池中的地址返回给 s2。而后判断,s 指向的是堆中的对象,而 s2 指向的是常量池中的字符串常量,因此不管是 JDK7 仍是以前的都是 false。

二、而后再看下面 9-----12 行。由于前面已经在常量池中添加 "1",因此第9行会直接返回地址,而后执行添加操做,建立字符串 "11",此时并无添加到常量池,而后执行第10行,由于常量池不存在 "11",因此 JDK7 以前直接加入常量池,JDK7 及之后则直接将 "11" 的地址存入常量池,而 s3 则不变,仍是保存的是常量池外的那个 "11" 的地址值。而后执行 11 行,由于常量池已存在 "11",因此 s4 就是返回 "11" 的地址值,不一样的是在 JDK7 以前由于常量池保存的是 "11" 常量,因此返回的是常量池中的地址值;而 JDK7 及之后常量池保存的是常量池外的 "11" 的地址值,因此返回的是池外的地址值。因此最后判断在 JDK7 以前是 false,而在 JDK7 开始是 true。

相关文章
相关标签/搜索