通过一道面试题来学习StringTable
先看下下面这道面试题
1 | String s1 = "a"; |
常量池和串池之间的关系
首先我们先通过一个Demo案例来分析.
1 | public class Demo02 { |
然后利用javap命令反编译class
1 | javap -v Demo02.class |
反编译后样式
1 | Classfile /E:/workplace/abc/jvmStudy/out/production/jvmStudy/demo1/Demo02.class |
然后找到main方法部分,我们来查看字节码
1 | public static void main(java.lang.String[]); |
其中 ldc #2 表示去常量池中的2号位置加载数据(“a”).
astore_a 表示把加载好的数据存入一个1号局部变量.
LocalVariableTable 表示方法的局部变量表,对应的1号局部变量就是
1 | 3 7 1 s1 Ljava/lang/String; |
同理可理解全部main方法.
Constant pool 指的是运行时常量池.
通过反编译来查看字符串拼接
现在我们来新增一行代码
1 | String s4 = s1 + s2; |
再重新编译Demo02,然后使用javap进行反编译,得到如下内容:
1 | Classfile /E:/workplace/abc/jvmStudy/out/production/jvmStudy/demo1/Demo02.class |
我们来查看新增的第四行代码反编译的结果
- 9: new #5 // class java/lang/StringBuilder
先通过new关键字创建了一个StringBuilder对象 - 13: invokespecial #6 // Method java/lang/StringBuilder.”
“:()V
调用StringBuilder的构造方法,通过()V可以看出是一个无参构造 - 16: aload_1
将局部变量表中的1号局部变量加入栈顶供方法使用 - 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
调用StringBuilder的append方法. - 20: aload_2
加载S2入栈 - 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
调用append方法 - invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
调用StringBuilder中的toString方法 - 27: astore 4
添加到4号局部变量
然后我们看一下StringBuilder中的toString()方法.
1 | @Override |
他就是通过StringBuilder中的value值,创建了一个新的值为”ab”字符串对象.
所以
1 | System.out.println(s3 == s4); |
的结果就很明显了,s3是串池中的对象,而s4是一个新new的对象.
编译器优化
我们在定义一个变量s5
1 | String s5 = "a" + "b"; |
然后再进行反编译
反编译后我们继续查看main方法的字节码
1 | public static void main(java.lang.String[]); |
我们找到s5的执行代码
29: ldc #4 // String ab
31: astore 5
可以看出是直接在常量池中找到”ab”对象,然后存到局部变量表中
我们可以对比下 String s3 = “ab”; 反编译的字节码
6: ldc #4 // String ab
他们都是从常量池中找到”ab”对象
所以
1 | System.out.println(s3 == s5);//返回true |
返回true.
这是因为javac在编译期间做的优化,因为”a” ,”b” 都是常量,在编译期间做拼接,结果是固定的,不可能是其他结果,所以直接优化成”ab”.
而s1 +s2 因为都是变量,无法在编译期间确定值,变量的值是可能改变的,所以在编译期间无法确定其值,所以需要通过StringBuilder来拼接字符串.
字符串加载到串池中的时间
我们新建一个Demo03类,代码如下
1 | public class Demo03 { |
然后分别在两个 System.out.println(“1”); 处打断点.
然后通过IDea中的Debug memory工具来查看对象个数.
点击load classes 查看运行到当前代码处,每个对象的实例个数.
其中 String对象为2252.
然后我们单步执行一下,2254(在System.out.print创建类名字符串),再执行一下,变成了2255.
走到下一个断点处,String对象变为2262个,继续往下走,字符串个数不变.
这个现象,充分证明了两个信息
- 串池不是在编译期立即生成对象,而是在使用的时候才延迟加载
- 如果串池中有对应的字符串对象,则从串池中获取,不再新创建对象.(“a”格式,如果使用new String(“a”) 则正常新建对象)
StringTable特性总结
- 常量池中的字符串仅仅是符号,只有在第一次用到时才会变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder.append
- 字符串常量拼接的原理是编译期优化
- 可以使用intern方法,主动将串池中还没有的字符串放入串池.
java8中的intern()方法
我们新创建一个测试类Demo04,代码如下
1 | public class Demo04 { |
我们逐步分析
- 在执行 String s = new String(“a”) + new String(“b”);时
串池中存在 “a” 对象和 “b” 对象
堆中存在 new String(“a”) 和 new String(“b”),以及拼接得来的 new String(“ab”) - String s2 = s.intern();
intern()方法的作用是:// 将这个字符串对象放入串池,如果有则不会放入,会把串池中的对象返回
这时,串池中多了一个”ab”对象,且改对象与拼接来的 new String(“ab”)为同一个对象
所以 两个判断相等的结果都为true
假设 我们在执行 s.intern(); 方法前面加上如下代码String x = “ab”; ,我们再来逐步分析
在执行 s.intern() 时,串池中已经有了”ab”对象,则s2直接饮用串池中的”ab”对象,所以两个判断的结果应为false,true .
java1.6中的intern()方法
在java1.6中,intern()方法将这个字符串尝试放入串池,如果有则不会放入,如果没有,则会将现有对象复制一份放入串池,也就是串池中的对象,并不是当前执行intern()的对象.
这个就不具体演示了(偷懒…)
面试题结果
1 | public class Demo01 { |