一、StringTable
??StringTable也叫串池,听名字就可以知道它和String的存储有关。在1.6,它是存在于永久代中的,到了1.7之后,StringTable被放在了堆中。 ??为了说明StringTable,我使用了几个例子。
1.1 例一 字面量创建字符串
public static void main(String[] args){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
??在上面这段代码中,通过字符串字面量的方式新建了几个String。对于变量s1,s2,s3,我们都知道它们被存在了栈中。可是后面的字符串呢?它被存储在哪个地方呢? ??经过反编译,我们得到如下jvm指令。 ??这里的#2就是“a”,当类加载的时候,常量池中的信息会加载到运行时常量池中,此时的a,b,ab都还是符号,没有变成java对象。当运行此方法,执行到对应的代码时,才会将符号a变成“a”字符串对象,并将对象放入StringTable中。 需要注意的是,普通的java对象在类加载的时候就会生成并放入堆中,而这种方式生成的String不同,只有当执行到新建String的代码时才会生成字符串对象。 ??StringTable是一个哈希表,长度固定,“a”就是哈希表的key。一开始的时候,会根据“a”到串池中找其对象,一开始是没有的,所以就会创建一个并放入串池中。串池为 [“a”]。 ??执行到指令ldc #3时,会和上面一样,生成一个“b”对象并放入串池中,串池变为[“a”, “b”]。 ??同样地,后面会生成“ab”对象并放入串池中。串池变为[“a”, “b”, “ab”]。
??小结一下:字面量创建字符串对象是懒惰的,即只有执行到相应代码才会创建相应对象(和一般的类不同)并放入串池中。如果串池中已经有了,就直接使用串池中的对象(让引用变量指向已有的对象)。串池中的对象只会存在一份,也就是只会有一个“a”对象。
1.2 例二 字符串变量拼接
??观察下面的代码,请问输出结果是什么?
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
??一样,先拿到反编译的jvm指令。 ??前面的指令我们已经很熟悉,观察行号为9的指令,这里是个new。这就说明s4的创建方式和s1、s2、s3不同,它是在堆里新建了一个对象,前面根据字面量创建的则是在串池中生成了字符串对象。 ??观察行号9的指令后面的注释,可以知道这里是new了一个StringBuilder对象。接着看17,21,可以发现“s1 + s2”的方式是通过StringBuilder对象调用append方法实现的。 ??最后看24,最后是调用了toString方法生成了新的字符串对象。
public String toString(){
return new String(value, 0, count);
}
??以上分析就想要说明:即当两个字符串变量拼接时,jvm会创建一个StringBuilder对象,利用其append方法实现变量的拼接。最后再通过其toString方法生成一个新的String对象。 ??最后我们看输出结果,发现s3不等于s4,这说明s3指向串池中的“ab”对象,s4指向堆中的“ab”对象。这是两个不同的对象。
1.3 例三 字符串常量拼接
??观察下面的代码,请问输出结果是什么?
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
System.out.println(s4 == s5);
??直接反编译: ??可以看到s5的创建和s3一样。这也就是说,即两个字符串常量拼接时,最后的结果和直接用一个字符串字面量新建一个String一样,都是在串池里创建对应的对象。所以最后s4也是不等于s5的。 ??至于为什么会以这种方式创建s5,这其实是编译期的优化。编译期间,编译器发现这是两个常量相加,结果是确定的,所以就直接让s5等于“ab”。
1.4 例四 intern方法
??接下来,我们来聊聊String的intern方法。
String s = new String("a") + new String("b");
String s2 = s.intern();
System.out.println(s1 == "ab");
System.out.println(s == "ab");
??首先反编译。 ??反编译的结果我们也很熟悉,也是新建了一个StringBuilder实现字符串的拼接与创建。最终的结果就是在堆中创建了一个“ab”字符串对象。并且在串池中加入“a”,“b”对象。 ??intern方法的作用就是在尝试把堆中对象放入串池中。如果串池中已有,会返回串池中的对象。并且s调用intern方法后依旧指向堆中的对象。如果串池中没有,会在串池中创建一个“ab”对象并返回,并且会让s指向串池中的“ab”对象。 ??注意上面是jdk1.7之后的做法,在jdk1.6,当一个String调用intern方法时,如果串池中没有,会将堆中的字符串对象复制一份放到串池中,最后返回StringTable中刚加入的对象。
1.5 小结
- 懒加载。当用字符串常量创建字符串时,在执行到对应的代码之前,该常量只是符号,只有执行到该代码时,才会创建相应的字符串对象并放入串池中。
- 可以利用串池的机制,来避免重复创建字符串的对象。
- 字符串变量的拼接的原理时StringBuilder (1.8)。
- 字符串常量拼接的原理时编译期优化。
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池( s.intern() )。
二、StringTable调优
??1)StringTable是一个哈希表,所以它的性能就和它的大小密切相关,所以StringTable调优其实就是调桶的个数。桶的数量越大,就越不容易产生哈希碰撞,效率就越好。可以通过 -XX:StringTableSize = …进行设置。 ??2)考虑将字符串对象是否入池。如果要存的字符串过多并且很多重复,可以通过intern方法,把字符串从堆中入池,就可以减少字符串对象的个数,节约堆内存。
|