1.常量池
常量池就是一张表,里面存储了运行所需要的类名、方法名、参数类型、字面量等信息,在JVM中虚拟机指令根据这张表来找到执行程序所需要的信息。当类还没有被加载到JVM中时,常量池就在每个类对应的class文件中,其中每条信息的地址还是符号地址,比如1、2、3...这样的序号;当类被加载时,这个类的常量池信息就会放入方法区中的运行时常量池,并把里面的符号地址变为真实存放的物理地址。但此时常量池中的那些字符串还是符号,并没有串池对象,只有当用到它时才会被创建成对象,如果在串池中搜索不到需要的对象就会将该对象放入串池中,如果有则不会重复放到串池,这也是串池的延时加载。常量池没有垃圾回收。
2.串池(String pool或String Table)
串池是针对字符串重用专门设置的一个缓存池,在串池中存放的是字符串的值而不是字符串对象,上文所说的串池对象就是串池中的一个个值。如果在创建一个String对象是采用的是直接赋值,比如String s="qqq",那么就会将"qqq"存入串池中,并将这个串池对象返回给s;如果是采用new创建的字符串对象,则是将对象创建到堆内存中,但指向的值还是串池中的值。
其次,串池是可以动态存放的而常量池是固定的,我们在创建字符串对象时如果串池中没有对应的值就会在串池中动态生成。
串池有垃圾回收,当串池中的对象过多导致空间不足时就会进行垃圾回收。串池在jdk1.6之前放在了永久代中,这时GC垃圾回收的效率较低,会等到FULLGC也就是老年代的空间不足时才会触发垃圾回收;在jdk1.7之后中放在了堆内存中,只需要等到minorGC就会触发垃圾回收,效率得到了提高。
串池的性能调优:
一方面,StringTable本质上是一个哈希表,所以当桶的个数较大时哈希碰撞的几率就小,查找的效率就会变高,反之桶的个数很少会导致哈希碰撞的几率增大,查找的效率就会变低,所以当存放的字符串很多时可以适当增大串池的桶的个数减少哈希冲突来提高查找效率。
另一方面,可以通过串池的特性减少内存的占用。在存储字符串时,很可能出现相同的字符串,如果重复存储会需要大量空间,而串池中重复的对象只会存储一个,所以可以将这些字符串放到串池中就只会生成一个串池对象,而intern方法就正好达到这个目的,所以在存储字符串时先调用intern方法再存储就会大大节省空间,就能达到表面上是多个字符串,但实际只存储一个对象的效果。
intern方法在jdk1.8和jdk1.6中有点差异,在jdk1.8中是将这个字符串本身尝试放入串池,如果有则不会放入,如果没有则会放入串池,两种情况都会返回串池中的对象;在jdk1.6中虽然也是将这个对象尝试放入串池,如果串池中有则不会放入,但如果串池中没有就会先拷贝一个对象并将拷贝后的对象放入串池,所以两种情况下原来的字符串都还是堆中的对象,但返回的都是串池中的对象。
由此衍生出一个问题,字符串的值能否改变?
答案是不能,一个字符串对应一个值。分两种情况讨论:
第一种是直接赋值然后修改。字符串在第一次赋值后串池会生成对应的对象,如果我们对字符串进行了修改,则串池会生成另一个串池对象并将其赋给局部变量,之前的串池对象依旧存在,只是没有局部变量使用了,所以这种情况下的修改本质上是生成另一个串池对象而不是修改原来的对象。
第二种是new一个字符串对象再修改。这实际上是重新创建了另一个字符串对象,然后串池也同时创建了一个新的串池对象,原来的字符串对象就会被回收,而串池中原来的对象依旧存在,所以也不是修改原来的对象,而是重新创建了一个对象。
由此可见如果对字符串频繁的进行修改会不断产生新的对象,一般不建议对String对象进行频繁修改。
3.练习
String s1="a";
String s2="b";
String s3="ab";
String s4=s1+s2;
String s5="a"+"b";
String s6=s4.intern();
判断s3==s4、s3==s5、s3==s6的结果。
解析:
首先"a"、"b"、"ab"都是常量,在串池中都搜索不到所以会将a、b、ab放入串池中,此时s1、s2、s3都是串池中的对象。
对于s4,s4是s1和s2拼接在一起的,而s1和s2是变量,本质上是通过new了一个StringBuilder对象,经过append方法拼接后再执行toString方法,而toString方法底层是又new了一个String对象,而new的对象是存放在堆中的,s3是串池中的,所以s3==s4的结果是false。
对于s5,s5是两个常量拼接的,由于javac在编译期间的优化,常量的拼接结果是确定的,
会直接从串池中查找是否有这个结果,此时串池中已经有ab对象,所以会将串池中的ab对象给s5,此时s5和s3都是串池中的ab对象,所以s3==s5的结果是true。
intern方法在jdk1.8中是将这个字符串尝试放入串池,如果有则不会放入,如果没有则会放入串池,两种情况都会返回串池中的对象,所以由于s3已经将ab放入了串池导致s4放入串池失败,此时s4仍是堆中的对象(如果先执行s4再执行s3那么由于串池中没有ab就会将s4放入串池,此时s4就和s3一样都是串中的对象了),但由于返回的是串中的对象所以s6是串中的对象,s6==s3的结果是true。在jdk1.6中虽然也是将这个对象尝试放入串池,如果串池中有则不会放入,但如果串池中没有就会先拷贝一个对象并将拷贝后的对象放入串池,所以两种情况下s4都还是堆中的对象,但返回的都是串中的对象,所以s6和s3还是相同的。