JVM Instruction Set Example 2
这次看一下String和常量池相关的操作。
ldc指令,把运行时常量池(runtime constant pool)中的元素压入操作栈。
压入元素的类型由常量池中元素类型决定:
- 如果常量池中的元素是int/float类型,那么压入的类型是float/int
- 如果是String类型的reference,那么压入其String对象的reference
- 如果是class类型的symbolic reference,那么压入其class对象的reference
- 如果是method type/handler的symbolic reference,那么压入其MethodType/MethodHandle对象的reference
可以看到压入的共两种类型:数值类型或reference类型
Example 2.1
又是一个经典笔试题:
1 2 3 4 5 6 7 8 9 10 | void f1() { String s1 = "abc"; String s2 = "abc"; String s3 = new String("abc"); String s4 = new String("abc"); System.out.println(s1 == s2); System.out.println(s3 == s4); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | void f1();
Code:
0: ldc #7 // String abc;常量池中取出其ref
2: astore_1 // (String s1 = "abc";)
3: ldc #7 // String abc;常量池中取出其ref
5: astore_2 // (String s2 = "abc";)
6: new #8 // class java/lang/String;创建新的对象并返回ref
9: dup
10: ldc #7 // String abc
12: invokespecial #9 // Method java/lang/String."<init>":(Ljava/lang/String;)V
15: astore_3 // (String s3 = new String("abc");)
16: new #8 // class java/lang/String;创建新的对象并返回ref
19: dup
20: ldc #7 // String abc
22: invokespecial #9 // Method java/lang/String."<init>":(Ljava/lang/String;)V
25: astore 4 // (String s4 = new String("abc"));
27: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
30: aload_1
31: aload_2
32: if_acmpne 39 // (s1 == s2;),比较了其引用值
35: iconst_1
36: goto 40
39: iconst_0
40: invokevirtual #11 // Method java/io/PrintStream.println:(Z)V
43: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
46: aload_3
47: aload 4
49: if_acmpne 56 // (s3 == s4;),比较了其引用值
52: iconst_1
53: goto 57
56: iconst_0
57: invokevirtual #11 // Method java/io/PrintStream.println:(Z)V
60: return
|
“123”和“456”作为当前类的常量池中成员,在class加载到JVM是已经有对象了。 因此s1,s2分别获得了它们的ref。
只要有new指令就会在heap中申请内存,从而有一个新的对象实例,新的ref。
因此,这段程序的输出是:
1 2 | true false |
Example 2.2
1 2 3 4 5 6 | void f2() { String s1 = "12" + "3"; String s2 = "456"; String s3 = s1 + s2; String s4 = s1 + s2 + s3; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | void f2();
Code:
0: ldc #12 // String 123
2: astore_1 // (String s1 = "12" + "3";),可以看出编译阶段可以优化字符串
3: ldc #13 // String 456
5: astore_2 // (String s1 = "456";)
6: new #14 // class java/lang/StringBuilder
9: dup
10: invokespecial #15 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3 // (String s3 = s1 + s2;)
25: new #14 // class java/lang/StringBuilder
28: dup
29: invokespecial #15 // Method java/lang/StringBuilder."<init>":()V
32: aload_1
33: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: aload_2
37: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
40: aload_3
41: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
44: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
47: astore 4 // (String s4 = s1 + s2 + s3;)
49: return
|
每个String对象保存的字符串是不会改变的。 改变String只会新建一个新的String对象。
上段代码可以看出,对于通过+来进行多个字符串的拼接操作,
Oracle JDK是通过StringBuilder来实现的,
这也是一种高效的做法。
相比于StringBuffer,StringBuilder也免去了sync的代价。
(StringBuffer只是给StringBuilder的绝大部份方法添加了synchronized)
但是这里并没有复用StringBuilder,
每个子符串连接的Java语句都要new一个新的StringBuilder对象,
这也是这种+操作耗时的地方。
Example 2.3
这个例子看看String.intern()。
1 2 3 4 5 6 7 8 9 10 | void f3() { String s1 = "123"; String s2 = new String("123"); System.out.println(s1 == s2); System.out.println(s1.intern() == s2.intern()); System.out.println(s1 == s1.intern()); System.out.println(s2 == s2.intern()); System.out.println(s1 == s2.intern()); } |
指令码就不贴了,和上面没什么区别。
String.intern()的实现通过JNI由本地代码实现的, 其功能是在字符串常量池中查找与当前字符串字面值相等的字符串, 返回其在字符串常量池中的引用。 如果不存在,则加入后返回。 因此调用String.intern()可能会扩充字符串常量池。
1 | public naive String intern();
|
字面值相同的字符串调用intern()都会获得同一字符串对象的引用。
这个页面里的测试说明intern()的调用是及其昂贵的, 大概是O(n)的算法复杂度,n是字符串常量池的大小。
这段代码的结果是:
1 2 3 4 5 | false true true false true |
s1开始就获得了字符串常量池中的对象,因此s1和s1.intern()都是同一对象的引用值, 结果相等。
Example 2.4
最后比较下StringBuilder复用和·+·直接拼接的效率:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public class TestStrBuilder { static String s1 = "1"; static String s2 = "2"; static String s3 = "3"; static String s4 = "4"; static String s5 = "5"; static String s6 = "6"; public static void main(String[] args) { f1(10000000); f2(10000000); f1(100000000); f2(100000000); } public static void f1(long time) { long cost = System.nanoTime(); for (long i = 0; i < time; i++) { String s = s1 + s2 + s3 + s4 + s5 + s6 + s5 + s4 + s3 + s2 + s1; } cost = System.nanoTime() - cost; System.out.println(cost / 1000000); } public static void f2(long time) { StringBuilder sb = new StringBuilder(); long cost = System.nanoTime(); for (long i = 0; i < time; i++) { sb.append(s1).append(s2).append(s3).append(s4).append(s5).append(s6); sb.append(s5).append(s4).append(s3).append(s2).append(s1); String s = sb.toString(); sb.delete(0, sb.length()); } cost = System.nanoTime() - cost; System.out.println(cost / 1000000); } } ` |
在OpenJDK 1.7.0_21 64bit下结果是:
1 2 3 4 | 1465 1230 13044 10681 |
大概复用StringBuilder时效率提高了17%。