一、常量池与串池StringTable的关系
public class StringTableStudy {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = "b";
String s5 = "ab";
}
}
将上述代码先编译成字节码,再反编译,得到如下虚拟机指令(部分截图):
常量池:
方法内的指令:
局部变量表:
StringTable在数据结构上,是一个哈希表,下面介绍一下指令执行时,运行时常量池和StringTable的工作关系:
- 由前面的内容可以知道,常量池存在于字节码文件,当该类被加载,它的常量池信息就会放入运行时常量池。此时a b ab仍旧是运行常量池种的符号,还没变为java字符串对象。
- 程序计数器记下0,开始执行code下的指令" ldc #2",执行到 ldc #2时,会根据#2去运行常量池中找到a,并创建出a的字符串对象,然后存入StingTable(串池)。【此时StringTable内有[“a”]】
- 程序计数器记下2,开始执行“astore_1”,将a字符串对象存入LocalVariableTable(方法栈内局部变量)的s1对象。【此时LocalVariableTable内有[s1]】
- 同理的创建s2,s3对象时也是走2、3步骤一样。【此时,StringTable内有[“a”,“b”,“ab”],LocalVariableTable内有[s1,s2,s3]】
- 执行到“ 9: ldc #3”创建s4对象时,先根据#3找到b,然后发现StringTable已经存在"a"的字符串对象,无需再创建,就直接将s4引用到其上面。同理,s5与s4步骤一样。
常量池与串池StringTable的关系:在常量池中的字符串常量,会被创建并存入StringTable内,若StringTable内已经存在了,就直接使用无需再重新创建。
注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。
二、拼接字符串变量对象
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
//结果为false,因为s3是存在于串池之中,s4是由StringBuffer的toString方法所返回的一个对象,存在于堆内存之中
}
}
输出结果为:false
用反编译来解释:
通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()
最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中.
三、拼接字符串常量对象
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
}
}
输出结果为:true
反编译解释:
- 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以s5直接从串池中获取值,所以进行的操作和 s3= “ab” 一致。
- 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建
四、intern方法 JDK1.8
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
-
如果串池中没有该字符串对象,则放入成功
-
如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
例子1:
例子2:
public class HelloWorld {
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
}
}
五、intern方法 JDK1.6
调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
- 如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中
- 如果有该字符串对象,则放入失败
无论放入是否成功,都会返回串池中的字符串对象
六、面试题 JDK1.8
package com.itcast.itheima.xpp;
public class main {
public static void main(String[] args) {
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);//false
System.out.println(s3==s5);//true
System.out.println(s3==s6);//true
String x2=new String("c")+new String("d");
String x1="cd";
x2.intern();
System.out.println(x1==x2);//false
String x4=new String("e")+new String("f");
x4.intern();
String x3="ef";
System.out.println(x3==x4);//true
}
}
七、 StringTable 位置
JDK1.6 时,StringTable是属于常量池的一部分。
JDK1.8 以后,StringTable是放在堆中的。
验证JDK1.8的StringTable:
/**
* 演示 StringTable 位置
* 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit
* 在jdk6下设置 -XX:MaxPermSize=10m
*/
public class Demo1_6 {
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<String>();
int i = 0;
try {
for (int j = 0; j < 260000; j++) {
list.add(String.valueOf(j).intern());
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
验证JDK1.6的StringTable内存:
八、StringTable 垃圾回收
StringTable在内存紧张时,会发生垃圾回收。
演示案例:
/**
* 演示 StringTable 垃圾回收
* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
*/
public class Demo1_7 {
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 100000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
设置虚拟机参数:-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
堆内存为10M,输出StringTable信息,打印GC信息。
设置i为100,输出结果:
没有发生GC,且StringTable statistics这栏显示着Stringtable的信息。
设置i为10000,输出结果:
发生了GC,而且Stringtabl的信息没有增加,说明达到了最大值。
九、StringTable 性能调优
调优的两个方案:
- 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
- 考虑是否将字符串对象入池,存在堆中的字符串对象可以重复的,但可以通过intern方法入池减少重复,保证相同的地址在StringTable中只存储一份
9.1 演示增加桶的个数提升性能
/**
* 演示串池大小对性能的影响
* -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009
*/
public class Demo1_24 {
public static void main(String[] args) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if (line == null) {
break;
}
line.intern();
}
System.out.println("cost:" + (System.nanoTime() - start) / 1000000);
}
}
}
linux.words文件含很多字符串,程序将读取这些字符串,并存入串池中,最后打印需要的时间。
(1)设置虚拟机参数:堆大小为500M,且打印StringTable信息
-Xmx:设置JVM最大可用内存为500M。
-Xms:设置JVM促使内存为500M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
运行结果:
未指定StringTable大小时,StringTable的桶个数默认为60013,入池的总时间为300。
(2)追加虚拟机参数,指定StringTable的大小为1009
输出结果:
入池时间耗费8453,桶个数1009。
显然,Stringtable越小,入池时间越慢,反之越大。
9.2 演示通过入池减少内存消耗
/**
* 演示 intern 减少内存占用
* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
* -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000
*/
public class Demo1_25 {
public static void main(String[] args) throws IOException {
List<String> address = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = reader.readLine();
if(line == null) {
break;
}
address.add(line);//强引用,防止垃圾回收
}
System.out.println("cost:" +(System.nanoTime()-start)/1000000);
}
}
System.in.read();
}
}
由代码可以见,重复读取文件内容。
运行代码后,通过 jvisualvm工具查看内存占用情况如下:
修改代码,做入池操作
address.add(line);//强引用,防止垃圾回收
重新运行,再查看内存情况:
显然,添加了入池操作,能减少内存空间消耗。因为重复的字符串对象不会重新保存到串池中。