StringTable
先看几个面试题
解析String s1 = "a";
使用javap -v 程序.class
反编译当前的程序,查看字节码文件
对应虚拟机指令:
ldc #2 // 加载常量池中 符号地址为#2 的数据(其实就是字符串 “a”)
astore_1 // 将加载的数据,保存到方法的本地变量表LocalVariableTable中
当程序真正运行的时候,常量池中的数据,全都被加载到运行时常量池中,并且出现了一个串池StringTable,用来记录字符串对象。
StringTable [ ]
“a”在常量池中并不是一个对象,当程序运行到main方法的第一行,执行了上面提到的两条虚指令,执行ldc命令的时候,才会将“a”变成对象。这时,会先查看StringTable中是否存在 “a” 对象,第一次执行,所以没有,就把 “a” 对象放入其中。
StringTable [ "a" ]
这种直到运行到某行,才将字符串转换为对象的做法,属于一种懒加载。
后面两行代码的执行过程也是一样。
解析 String s4 = s1 + s2;
使用命令javap -v 程序.class
反编译程序,找到这行代码对应的虚拟机指令:
可以看到虚拟机指令做了这些动作
9:创建一个StringBuilder类对象
12:
13:执行特殊方法,StringBuilder类的构造器方法
16:加载LocalVariableTable 中卡槽1的数据(也就是“a”对象)
17:执行特殊方法,StringBuilder类的append方法
20:加载LocalVariableTable 中卡槽2的数据(也就是“b”对象)
21:执行特殊方法,StringBuilder类的append方法
24:执行特殊方法,StringBuilder类的toString方法
25:将结果保存到LocalVariableTable 中卡槽4中
也就是
new StringBuilder().append("a").append("b").toString()
而查看StringBuilder类的toString方法,会发现,执行了一个 new 操作。这会创建一个新对象,新对象的地址和原先不同。
所以 s3 == s4
的结果是 false
解析String s5 = "a" + "b";
使用命令javap -v 程序.class
反编译程序,找到这行代码对应的虚拟机指令:
可以看到
29:加载LocalVariableTable 中卡槽4的数据(也就是“ab”对象)
17:将结果保存到LocalVariableTable 中卡槽5
只是做了一个从常量池加载 “ab”对象的动作。
这是因为javac 对编译进行了优化,这里“a”,"b"都是确定的结果,可以在编译期间确定,变量为“ab”,所以可以直接加载StringTable 中的 “ab”对象。
因此 s3 == s5
答案为true
字符串延迟加载
StringTable 是位于堆的的一个保存运行时字符串对象的HashTable,并且其中的字符串对象,只有在第一次被使用的时候才会被加载,这就是字符串延迟加载。
下面演示一下,使用Idea的Memory功能,可以看到堆中对象的个数:
刚开始字符串类型对象,有2275个
执行到第一个断点,由于每个被执行到的字符串都是第一次被使用,所以每个字符串都被创建了一个新的对象,并放在StringTable 中。堆中字符串对象变为2285个。
再次使用和上面相同的字符串字面量,这时由于StringTable中已经存在这样的字符串字面量的对象,就可以直接复用。字符串对象不变,仍未2285个。
StringTable 的 intern 方法
String s1 = new String("a") + new String("b");
String s2 = s1.intern();
boolean b2 = s2 == "ab";
boolean b1 = s1 == "ab";
编译之后
常量池:“a”,“b”
运行期间:
代码执行到第一行,new String(“a”),首先将常量池中的 “a” 字符串生成对象放到串池中,并且使用这个串的值创建了一个新的对象放在堆中
StringTable:[ “a” ]
堆:new String(“a”)
new String(“b”) 同理
StringTable:[ “a” ,“b”]
堆:new String(“a”) new String(“b”)
两者相加,由于都是动态变量,在编译期间无法直接确定,所以不会放到常量池,在运行期间也不会放到 StringTable 中。而是会先创建一个StringBuilder对象,将堆中的new String(“a”) new String(“b”)两个字符串对象拼接起来,获得 new StringBuilder(“ab”),再toString(),生成对象 new String(“ab”) 在堆中。这是变量对象,所以不会放到StringTable中。
StringTable:[ “a” ,“b”]
堆:new String(“a”) new String(“b”) new String(“ab”)
执行到 String s2 = s1.intern();
将 s1 字符串对象放入 StringTable(若StringTable 中已经存在 “ab” 字符串对象就不会再放入),然后返回StringTable 中 “ab” 字符串对象的地址。
执行到 boolean b2 = s2 == “ab”;
“ab” 就会到 StringTable 中查找值为 “ab” 的字符串对象,获得该地址。
因此s2,和“ab” 其实都指向StringTable 的 同一个“ab” 字符串对象,也地址相等。
执行到 boolean b1 = s1 == “ab”;
此时StringTable 中的字符串对象,正是刚刚放进去的 s1 对象,所以这两个也是地址相等的。
String x = "ab";
String s1 = new String("a") + new String("b");
String s2 = s1.intern();
boolean b2 = s2 == "ab";
boolean b1 = s1 == "ab";
编译之后
常量池:“ab”,“a”,“b”
运行期间:
代码执行到第一行,String x = “ab”;,首先将常量池中的 “ab” 字符串生成对象放到串池中
StringTable:[ “ab” ]
堆:
代码执行 new String(“a”),首先将常量池中的 “a” 字符串生成对象放到串池中,并且使用这个串的值创建了一个新的对象放在堆中
StringTable:[ “ab”,“a” ]
堆:new String(“a”)
new String(“b”) 同理
StringTable:[ “ab”,“a” ,“b”]
堆:new String(“a”) new String(“b”)
两者相加,由于都是动态变量,在编译期间无法直接确定,所以不会放到常量池,在运行期间也不会放到 StringTable 中。而是会先创建一个StringBuilder对象,将堆中的new String(“a”) new String(“b”)两个字符串对象拼接起来,获得 new StringBuilder(“ab”),再toString(),生成对象 new String(“ab”) 在堆中。这是变量对象,所以不会放到StringTable中。
StringTable:[ “ab”,“a” ,“b”]
堆:new String(“a”) new String(“b”) new String(“ab”)
执行到 String s2 = s1.intern();
将 s1 字符串对象放入 StringTable,StringTable 中已经存在 “ab” 字符串对象就不放入,然后返回StringTable 中 “ab” 字符串对象的地址。
执行到 boolean b2 = s2 == “ab”;
“ab” 就会到 StringTable 中查找值为 “ab” 的字符串对象,获得该地址。
因此s2,和“ab” 其实都指向StringTable 的 同一个“ab” 字符串对象,也地址相等。
执行到 boolean b1 = s1 == “ab”;
刚刚s1 并没有放到 StringTable 中,所以此时StringTable 中的字符串对象与它并不相等
StringTable的特性
由上面,可以得出StringTable 的特性
- 常量池中的字符串仅是符号,只有第一次使用后才会变成对象,并保存到StringTable中
- 字符串转换为对象的时候,会尽量复用StringTable中已有的对象,避免每次重新新建对象
- 编译期间能确定的字符串常量,会被优化,直接加载拼接结果,如果StringTable中已经存在,就会复用StringTable 中已有的对象
- 只有编译期间能确定的字符串常量,才能直接使用StringTable中已有的对象,如果是字符串变量的拼接,编译期间无法预知结果,就会使用StringBuilder新建对象
- 可以使用instern方法,主动将串池中还没有的字符串对象放入串池
1) 1.8版本:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
2)1.6 版本:将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份(对象地址改变),放入串池,会把串池中的对象返回