目录
1.JVM更新:
JDK1.8将String Table从方法区的常量池移至了堆中。
1.1 String Table移至Heap中的验证
List<String> list =new ArrayList<>();
try {
//不断往串池中添加字符串数据
for (int j = 0; j < 200000; j++) {
list.add(String.valueOf(j).intern());
}
}catch (Throwable e){
e.printStackTrace();
}
//-Xms10m -Xmx10m
报错:java.lang.OutOfMemoryError: Java heap space
D:\Application\JDK13\bin\java.exe -Xms10m -Xmx10m "-javaagent:D:\Application\IDEAC\IntelliJ IDEA Community Edition 2019.3.4\lib\idea_rt.jar=63616:D:\Application\IDEAC\IntelliJ IDEA Community Edition 2019.3.4\bin" -Dfile.encoding=UTF-8 -classpath D:\Study\Javacode\out\production\Javacode JvmTest.test1
java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3721)
at java.base/java.util.Arrays.copyOf(Arrays.java:3690)
at java.base/java.util.ArrayList.grow(ArrayList.java:235)
at java.base/java.util.ArrayList.grow(ArrayList.java:242)
at java.base/java.util.ArrayList.add(ArrayList.java:452)
at java.base/java.util.ArrayList.add(ArrayList.java:465)
at JvmTest.test1.main(test1.java:24)
2. String Table
常量池中的字符串仅仅是字面符号,只有当执行到对应的JVM指令时,才会生成对象,利用串池避免重复创建字符串对象,JDK1.8串池从常量池中移动到Heap中。String table是一个hashtable结构,所以不可能出现两个相同的值,其不可以扩容。
2.1 字节码解释
2.1.1 测试代码
public class StringTableTest {
public static void main(String[] args) {
String s1="a";
String s2="b";
String s3="ab";
String s4=s1+s2;
String s5="a"+"b";
String s6=s4.intern();
//判断输出结果?
System.out.println(s3==s4);
System.out.println(s3==s5);
System.out.println(s3==s6);
}
}
2.1.2 字节码文件
在IDEA的Terminal窗口输入命令: javap -verbose StringTableTest.class
得到字节码文件,截取其中的重要部分:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=7, args_size=1
0: ldc #7 // String a
2: astore_1
3: ldc #9 // String b
5: astore_2
6: ldc #11 // String ab
8: astore_3
9: aload_1
10: aload_2
11: invokedynamic #13, 0 // InvokeDynamic #0:makeConcatWithC
onstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
16: astore 4
18: ldc #11 // String ab
20: astore 5
22: aload 4
24: invokevirtual #17 // Method java/lang/String.intern:(
)Ljava/lang/String;
27: astore 6
29: getstatic #23 // Field java/lang/System.out:Ljava
/io/PrintStream;
32: aload_3
33: aload 4
35: if_acmpne 42
38: iconst_1
39: goto 43
42: iconst_0
43: invokevirtual #29 // Method java/io/PrintStream.print
ln:(Z)V
46: getstatic #23 // Field java/lang/System.out:Ljava
/io/PrintStream;
49: aload_3
50: aload 5
52: if_acmpne 59
55: iconst_1
56: goto 60
59: iconst_0
60: invokevirtual #29 // Method java/io/PrintStream.print
ln:(Z)V
63: getstatic #23 // Field java/lang/System.out:Ljava
/io/PrintStream;
66: aload_3
67: aload 6
69: if_acmpne 76
72: iconst_1
73: goto 77
76: iconst_0
77: invokevirtual #29 // Method java/io/PrintStream.print
ln:(Z)V
80: return
LineNumberTable:
line 7: 0
line 8: 3
line 9: 6
line 10: 9
line 11: 18
line 12: 22
line 14: 29
line 15: 46
line 16: 63
line 18: 80
LocalVariableTable:
Start Length Slot Name Signature
0 81 0 args [Ljava/lang/String;
3 78 1 s1 Ljava/lang/String;
6 75 2 s2 Ljava/lang/String;
9 72 3 s3 Ljava/lang/String;
18 63 4 s4 Ljava/lang/String;
22 59 5 s5 Ljava/lang/String;
29 52 6 s6 Ljava/lang/String;
2.1.3 根据字节码文件理解JVM中StringTable的实现
public static void main(String[] args) {
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
String s1="a";
/* 0: ldc #7 // String a
2: astore_1
// a 符号变为 "a" 字符串对象,并存入局部变量表中Slot=1,变量名为s1的位置
以"a"作为键值去StringTable中做查询操作,StringTable中还未存有"a",
于是将"a"存入StringTable中==》StringTable ["a"]
*/
String s2="b";
/* 3: ldc #9 // String b
5: astore_2
// b 符号变为 "b" 字符串对象,并存入局部变量表中Slot=2,变量名为s2的位置
以"b"作为键值去StringTable中做查询操作,StringTable中还未存有"b",
于是将"b"存入StringTable中==》StringTable ["a","b"]
*/
String s3="ab";
/* 6: ldc #11 // String ab
8: astore_3
// ab 符号变为 "ab" 字符串对象,并存入局部变量表中Slot=3,变量名为s3的位置
同理 ==》StringTable ["a","b","ab"]
*/
String s4=s1+s2;
/* 9: aload_1
10: aload_2
11: invokedynamic #13, 0 // InvokeDynamic #0:makeConcatWithC
onstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
16: astore 4
//先加载变量aload_1 ("a" )和aload_2(" b "),再拼接
//这一步实际等效于:new StringBuilder().append("a").append("b").toString()
//此处是重新生成了一个新的对象
*/
String s5="a"+"b";
/*18: ldc #11 // String ab
20: astore 5
//此处可以发现s5的生成并没有像变量s4一样先加载变量s1和s2
而是和 String s3="ab" 对应的JVM指令一致,
所以s5和s3指向同一对象
*/
String s6=s4.intern();
//调用intern()方法,先在String table中查找是否存在与该对象相同的字符串,
// 若存在直接返回String table中字符串的引用,若不存在则将当前对象的引用放到String Table中,并且返回的是当前对象的引用
//所以此处"ab"在StringTable中已存在,s6获得的是与s3相同的引用地址
//运行结果:
System.out.println(s3==s4);//false
System.out.println(s3==s5);//true
System.out.println(s3==s6);//true
}
2.1.4 String Table编译器调优
String s4=s1+s2;//StringBulider
String s5="a"+"b";//Javac 编译期直接确定
变量拼接是动态的,因为变量的指向在运行期是可以改变的,所以变量的拼接是通过new 一个新的对象实现的。
而常量是确定不变的,所以javac进行了优化,在编译期就确定了常量拼接的结果。
注意:常量的拼接结果会自动存入String Table,而变量的拼接结果需要使用String类的intern()方法存入String Table中。
2.2 Debug测试:对利用串池避免重复创建字符串对象的验证
public static void main(String[] args) {
String s1="a";
String s2="b";
String s3="c";
String s4="d";
String s5="e";
String s6="a";
String s7="b";
String s8="c";
String s9="d";
String s10="e";
}
首先设置三个断点,进行Debug测试,查看Memory窗口Java,lang.String的数量变化
运行至断点1时,得到初始String的数量:
运行至断点2时,String如期望增加了5:
运行至断点3时,String的数量不变,说明没有重复生成"a",“b”,“c”,“d”,“e”:
2.3 String Table中存储的是String对象还是引用地址?
此部分对于网上查询到的资料有点蒙圈,个人总结如下:JDK1.8开始String Table中存储的既可以是String对象也可以是引用地址(如之后发现此结论有错,再做笔记更改)
2.3.1 Java中创建字符串对象的情况分析:
- 采用字面值的方式创建一个字符串时,JVM首先去串池中寻找是否存在该字符串,如果不存在,则在串池中创建该字符串,然后返回该字符串对象的引用地址;如果存在,则不创建任何新的对象,直接返回该字符串对象的引用地址。
- 采用new String(“abc”)方式创建字符串时,JVM首先去串池中寻找是否存在该字符串,如果不存在,则首先在串池中创建该字符串,然后再在堆中也创建一个该字符串对象,返回该字符串对象在堆中的引用地址,这种情况实际生成了两个String对象;如果存在,则只在堆中创建新的对象,返回该字符串对象在堆中的引用地址。
2.3.2 代码验证
Debug测试下观察Memory窗口中String数量的变化
断点1执行之后,String增量为1:
断点2执行之后,较上一行代码String增量为2:
2.3.3 运行结果分析:
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3=new String("asd");//串池和堆中都会生成一个"asd"字符串对象,而s3指向的是堆中的字符串对象
String s4=s3.intern();
//"asd"在串池中已经存在,所以直接返回串池中该字符串对象的引用,而s3指向的是堆中的"asd"
System.out.println(s4==s3);//false 该结果验证了以上分析
String s5=s3+s1;//StringBuilder方式创建的拼接对象只在堆中存在
String s6=s5.intern();//串池中不存在该字符串对象,此时存入的只是s5所指对象的引用地址,所以s6和s5指向的是同一地址
System.out.println(s5==s6);//true 该结果验证了以上分析
}