常量池最初存在于字节码文件中,在运行时被加载到运行时常量池中,其中a,b等就仅仅是常量池中的符号,还未变成Java中的字符串对象。等到执行到引用这个字符串的代码,才会变成字符串对象 。
执行 ldc #2 会把 a 符号变为“a”字符串对象
执行 ldc #3 会把 b 符号变为“b”字符串对象
创建的过程中,会先到StringTable中找,如果有就共用,没有再创建
StringTable是一个 HashTable 结构,不能扩容
public class StringTableStudy {
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "ab";
}
}
取出字符串a
存放到局部变量表1位置
取出字符串b
存放到局部变量表2位置
取出字符串ab
存放到局部变量表3位置
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
public class HelloWorld {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4=s1+s2;
//new StringBuilder().append("a").append("2").toString() new String("ab")
System.out.println(s3==s4);//false
}
}
反编译后的结果
9行,new了一个StringBuilder()对象,13,调用了一个<init>(),无参构造
16,加载局部量表中的1号变量。17,调用SB的append方法
20,加载局部变量表中2号变量。21,再次调用append方法
24,调用sb的toString方法,输出字符串
StringBuilder的toString方法,返回一个新的String对象。
因此,s3 == s4 为false,虽然他俩指向的均为ab,但是s3的ab是在串池中的,s4的是在堆中的
public class HelloWorld {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4=s1+s2;//new StringBuilder().a|ppend("a").append("2").toString() new String("ab")
String s5="a"+"b";
System.out.println(s5==s3);//true
}
}
编译后的结果
29行对应的s5 = "a" + "b"
6行对应的s3 = "ab"
javac在编译期间的优化,在编译期间就给你加好了。
String str = new String("a"),这行代码创建的对象个数因StringTable中有没有“a”对象而异,如果字符串池有“a”,则此时只会创建一个对象:也就是new的一个字符串对象,存放在堆中;如果没有就会创建两个对象,一个是new的对象存放在堆中,一个是“a”字符串常量对象,存放在StringTable中
StringTable 特性
- 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder
- 字符串常量拼接的原理是编译器优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
- 1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
- 所以1.6时,s对象不会被放入串池
public class Main{
public static void main(String[] args){
String s = new String("a") + new String("b");
s2 = s.intern();
System.out.println(s2 == "ab"); // true
System.out.println(s == "ab"); // true
}
}
a被放在串池中,b也被放入串池中
new String("a") 和new String("b")的对象在堆中,虽然值相同,但是地址不同。
因为此时的"ab"是利用StringBuilder拼接而成,并不是字面量,所以"ab"不会放入串池中,
s是由StringBuilder的toString创建的,是new String("ab"),只在堆中有,串池中没有ab这个值
s.intern(),将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入。并且将串池中的对象返回。
所以s2中存储的是串池中的ab,返回结果为true。
而且已经把s放入串池了,因此也为true。
public class Main{
public static void main(String[] args) {
String x = "ab";
String s = new String("a") + new String("b");
String s2 = s.intern();//将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,这两种情况都会把串池中的对象返回
System.out.println(s2 == x);//true
System.out.println(s == x);//false
}
}
再看这个代码
现在串池中已经有ab对象了,s2此时存储的是串池中的ab,而s存储的是堆中的。
下面换成1.6
StringTable 位置
1.7之前,StringTable跟随方法区位于永久代,之后就从永久代改为了堆中,因为永久代垃圾回收并不频繁,要等到FullGC才能被回收,即老年代满的时候才会触发,但是StringTable使用的相当频繁,因为有大量的字符串需要存储,如果他的回收效率不高,就会占用大量的内存,会造成永久代的内存不足,因此转移到堆中,仅仅minorGC就可以触发回收。
1.6方法区的内存溢出,说明字符串常量池位于永久代
这个错误是因为,如果一次GC,GC结束了,还有98%的内存被占用,就会被JVM视为无可救药了,直接抛异常了。
加入这个参数
发现堆空间不足,说明1.8使用的是堆空间
StringTable 垃圾回收
-Xmx10m 指定堆内存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次数,耗费时间等信息
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Code_05_StringTableTest {
public static void main(String[] args) {
int i = 0;
try {
}catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
字符串常量池的信息;
底层是用哈希表实现的,哈希表底层是数组加链表的结构,数组中的每个位置被成为桶,即buckets,entries即键值对的个数,literals即字符串常量的个数
我们修改代码
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Code_05_StringTableTest {
public static void main(String[] args) {
int i = 0;
try {
for(int j = 0; j < 10000; j++) { // j = 100, j = 10000
String.valueOf(j).intern();
i++;
}
}catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println(i);
}
}
}
在try块中加入一个循环,生成100个字符串,并加入到字符串常量池中。
我们发现确实加了100个字符串常量
我们把循环次数加到10000
发生了GC,且字符串常量变少了
StringTable 性能调优
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少发生Hash碰撞的次数,来增加HashTable的插入速度
我们设计一个方法,从文件中读取信息,并将信息存入常量池中,再计算一下一共花费了多长时间。
花费0.4秒,数组长度为20万
我们去掉这个参数
变成了0.6秒
执行下列程序:
因为没有出现字面量,所以其中的字符串均不会入池
我们观测一下内存中的数据
添加一个入池操作
内存占用显著变少了