先来几道题,看看大家是否都能答对
代码块1
String s1 = new String("小爽帅到拖网速");
String intern = s1.intern();
String s2 = "小爽帅到拖网速";
System.out.println(intern == s1); // false
System.out.println(intern==s2); // true
如果上面的题你都能答对,那下面的题呢
代码块2
String a = "aa";
String b = "bb";
String ab = a + b;
String intern1 = ab.intern();
String c = "aabb";
System.out.println(intern1 == ab); // true
System.out.println(intern1 == c); // true
为什么ab都能通过intern()方法主动放入StringTable中,而s1却不能呢?下面我将通过字节码的角度为大家分析
首先我们先看一下代码块1的字节码
-
首先在main方法中执行到String s1 = new String(“小爽帅到拖网速”);对应的字节码如下
0: new #2 // class java/lang/String 堆内存中分配字符串内存 3: dup // 复制一份引用保存在操作数栈中 4: ldc #3 // String 小爽帅到拖网速 去运行时常量池中查找对应的值 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V 调用构造方法 9: astore_1 // 把刚才dup复制指向堆内存的引用存入局部变量表中
这里的ldc指的是去常量池中查找对应的值,而常量池的值是字面值,只有字符串或者大于215的数值会被当做字面值放入常量池中,小于215会使用jvm指令i_const、bipush、sipush进行加载;
JVM规范里Class文件的常量池项的类型,有两种东西:
CONSTANT_Utf8_info // 维护两个字节大小的字节数组 CONSTANT_String_info // 简单了解一下,是String常量类型,但是并不直接持有String常量内容,而是有一个index指向另一个常量池为CONSTANT_Utf_info类型的常量,它才是真正保存字符串的内容
使用ldc加载字面量,会涉及的一个懒加载的操作,jvm走ldc命令时会去判断该常量是否已经resolve(没有被resolve会打上JVM_CONSTANT_UnresolvedString标记),在resolve过程中如果发现StringTable已经有了内容匹配的String引用,则直接返回引用,如果没有,则会在堆中创建该对象,并且该对象引用放在StringTable中,并返回引用
这里我们注意一个问题,String s1 = new String(“小爽帅到拖网速”);到底创建了几个对象
答案是一个或者两个都有可能,这是为什么呢?
在加载类的时候,字符串的字面量会进入到当前类的运行时常量池中,不会直接进入全局的字符串常量池(StringTable中并没有这个引用,在堆内也没有对应的对象产生),而java是懒加载的,等到运行到new String(“小爽帅到拖网速”);执行ldc指令才会去resolve,如果发现StringTable中已经有了匹配的内容,则只会通过new 在堆中创建新的实例;如果发现StringTable中没有匹配的对象,除了通过new在堆中创建新的对象,还会额外在堆里创建新的对象并且把引用放入StringTable,这就是两个对象了
总之ldc是否创建对象,全看这个字面值是否能在StringTable中匹配到对应值,在接着判断是否创建对象实例,但是如果使用到了new就一定会在堆内创建新的对象
-
s1.intern()
intern()在jdk8中,调用者的值如果已经被放入StringTable中,则会返回在StringTable的引用;如果在StringTable匹配不到对应的值则会将调用者的引用存入StringTable,并返回该引用;
在这里s1.intern()是放不进去的,应为在new String(“小爽帅到拖网速”);就已经创建了对象并把引用放入到了StringTable
-
String s2 = “小爽帅到拖网速”;
15: ldc #3 // String 小爽帅到拖网速 7: astore_3
同样使用ldc,同样会去resolve,并且在StringTable 中匹配到值,会把StringTable中的引用返回,不会创建新的对象
-
System.out.println(intern == s1); // false
String intern = s1.intern(); s1并没有把该引用放入到StringTable中,intern()返回的是new String(“小爽帅到拖网速”); 执行的ldc命令所创建的对象的引用,即intern 跟 s1 都是指向堆内不同的对象
-
System.out.println(intern==s2); // true
创建s2的执行的ldc返回的是StringTable中对应字面量的引用,所以这里自然是true
看完代码块1之后我们再来看看代码块2
- String a = “aa”;
String b = “bb”;
对应的字节码如下
0: ldc #2 // String aa
2: astore_1
3: ldc #3 // String bb
5: astore_2
两次ldc都在StringTable没有匹配到对应的字面量,所以都会在堆内创建对象,并且把引用放入StringTable中,并且通过astore_x 字节码指令存入到main方法对应栈帧的局部变量的对应槽位
- String ab = a + b;
在通过javac编译成.class文件时,会做编译时的优化成(new StringBuilder()).append(a).append(b).toString();
查看对应的字节码
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_1 // 对应astore_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17:astore_1 // 对应astore_2
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
通过字节码我们发现,如果直接将a1+a2相加, 会使用StringBuilder的append方法进行追加,最后使用toString()方法,本质也是使用了new String()的方式,但是拼接后的字面量并不是一开始就存在常量池当中的,自然不会走ldc指令,也就没有进入StringTable的机会了,只会在堆里边创建了一个对象
注意:创建什么样的字符串实例才会去走ldc指令,我们发现有些字符串创建会走ldc,而有些不会走ldc,这里涉及到类加载的一些知识
- 编译后的.class文件被类加载器加载到jvm时,会在方法区的元空间构建instanceKlass,并且把常量池的符号引用转存到运行时常量池,在类加载的第二阶段链接中的验证、准备阶段后,根据该符号引用对应的字面值在堆中生成驻留字符串的实例对象(java是懒加载的,只有运行到这行代码时才会执行该操作)然后将这个对象的引用存到StringTable中,最后在链接的第三个阶段解析,把运行时常量池中的符号引用替换成指向堆内存的直接引用(这个直接引用指向了堆内存中的实例对象,其中实例对象的对象头保存了java_mirror.class对象,该对象指向了方法区元空间中的instanceKlass,在调用这个实例的方法时都需要去instanceKlass中查方法表拿到对应的字节码地址去调用执行对应的方法),保证StringTable里的引用与运行时常量池中的引用值一致,整个过程大概就是这样
- 走ldc的都是在.class中已经留有的字面量,其中只会有字符串和大于2^15的数值(int,float,double),可能这么说有点牵强,我觉得还可以理解为走ldc是保留在常量池的静态字面量,在编译阶段就可以确定的,而String ab = a+b,包括一些字符串的操作都是运行时动态确定的,所以不会被常量池收留
-
String intern1 = ab.intern();
执行String ab = a + b; 并没有把“aabb”放入到StringTable中,所以主动调用intern()方法是能够把ab引用放入到StringTable中
-
String c = “aabb”;
31: ldc #9 // String aabb 33: astore 5 LocalVariableTable: Start Length Slot Name Signature 35 36 5 c Ljava/lang/String;
观察字节码可以发现执行的还是ldc命令,去resolve,并且在StringTable 中匹配到值,会把StringTable中的引用返回,不会创建新的对象
-
System.out.println(intern1 == ab); // true
System.out.println(intern1 == c); // true从上面可知,intern1的引用是ab调用intern()主动放进StringTable中的,所以intern1==ab为true,而c是从StringTable拿到的自然也是true
看完以上的分析,相信你已经对字符串对象进入StringTable的实际、堆中创建对象的个数有了一定的了解,那么下面这道题再试试看
String a = "aa";
String b = "bb";
String c = "aa"+"bb";
String d = new String("aabb");
String intern = d.intern();
System.out.println(intern==c); // true
System.out.println(intern==d); // false
这次不会再逐句分析,直接贴出字节码
-
变量c在编译阶段就把字面量放入常量池,使用ldc发现StringTable中没有,就在堆内创建后把引用放入了StringTable,有没有小伙伴好奇为什么在编译阶段aabb会被放入常量池中,这是java编译时的优化,可以看看反编译后的字节码
public class TestInstance { public TestInstance() { } public static void main(String[] args) { String a = "aa"; String b = "bb"; String c = "aabb"; String d = new String("aabb"); String intern = d.intern(); System.out.println(intern == c); System.out.println(intern == d); } }
-
创建变量d的时候按照我们之前的流程,StringTable中匹配到对应字面量,不会在将新创建的引用放入了StringTable,且主动调用intern()也是不行的,只会返回c 在ldc时放入StringTable中的引用
Constant pool:
#1 = Methodref #11.#35 // java/lang/Object."<init>":()V
#2 = String #36 // aa
#3 = String #37 // bb
#4 = String #38 // aabb
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=6, args_size=1
0: ldc #2 // String aa
2: astore_1
3: ldc #3 // String bb
5: astore_2
6: ldc #4 // String aabb
8: astore_3
9: new #5 // class java/lang/String
12: dup
13: ldc #4 // String aabb
15: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V
18: astore 4
20: aload 4
22: invokevirtual #7 // Method java/lang/String.intern:()Ljava/lang/String;
25: astore 5
27: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
30: aload 5
32: aload_3
33: if_acmpne 40
36: iconst_1
37: goto 41
40: iconst_0
41: invokevirtual #9 // Method java/io/PrintStream.println:(Z)V
44: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
47: aload 5
49: aload 4
51: if_acmpne 58
54: iconst_1
55: goto 59
58: iconst_0
59: invokevirtual #9 // Method java/io/PrintStream.println:(Z)V
62: return
LocalVariableTable:
Start Length Slot Name Signature
0 63 0 args [Ljava/lang/String;
3 60 1 a Ljava/lang/String;
6 57 2 b Ljava/lang/String;
9 54 3 c Ljava/lang/String;
20 43 4 d Ljava/lang/String;
27 36 5 intern Ljava/lang/String;