首先看这样一个面试题
// StringTable [ "a", "b" ,"ab" ] hashtable 结构,不能扩容 public class Demo1_22 { // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象 // ldc #2 会把 a 符号变为 "a" 字符串对象 // ldc #3 会把 b 符号变为 "b" 字符串对象 // ldc #4 会把 ab 符号变为 "ab" 字符串对象 public static void main(String[] args) { String s1 = "a"; // 懒惰的 String s2 = "b"; String s3 = "ab"; String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab") String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab System.out.println(s3 == s4); // false System.out.println(s3 == s5); // true } }
我们从字节码的角度来分析结果
首先反编译该类代字节码,输入命令:
javap -v Demo1_22.class // -v显示详细信息
得到结果:
Classfile /D:/work_folder/java_studying/idea_workspace/jvm/out/production/jvm/cn/itcast/jvm/t1/stringtable/Demo1_22.class Last modified 2019-12-8; size 1039 bytes MD5 checksum 9efed67948c7dacf9ddb854ca18f8c2b Compiled from "Demo1_22.java" public class cn.itcast.jvm.t1.stringtable.Demo1_22 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #12.#36 // java/lang/Object."<init>":()V #2 = String #37 // a #3 = String #38 // b #4 = String #39 // ab #5 = Class #40 // java/lang/StringBuilder #6 = Methodref #5.#36 // java/lang/StringBuilder."<init>":()V #7 = Methodref #5.#41 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #8 = Methodref #5.#42 // java/lang/StringBuilder.toString:()Ljava/lang/String; #9 = Fieldref #43.#44 // java/lang/System.out:Ljava/io/PrintStream; #10 = Methodref #45.#46 // java/io/PrintStream.println:(Z)V #11 = Class #47 // cn/itcast/jvm/t1/stringtable/Demo1_22 #12 = Class #48 // java/lang/Object #13 = Utf8 <init> #14 = Utf8 ()V #15 = Utf8 Code #16 = Utf8 LineNumberTable #17 = Utf8 LocalVariableTable #18 = Utf8 this #19 = Utf8 Lcn/itcast/jvm/t1/stringtable/Demo1_22; #20 = Utf8 main #21 = Utf8 ([Ljava/lang/String;)V #22 = Utf8 args #23 = Utf8 [Ljava/lang/String; #24 = Utf8 s1 #25 = Utf8 Ljava/lang/String; #26 = Utf8 s2 #27 = Utf8 s3 #28 = Utf8 s4 #29 = Utf8 s5 #30 = Utf8 StackMapTable #31 = Class #23 // "[Ljava/lang/String;" #32 = Class #49 // java/lang/String #33 = Class #50 // java/io/PrintStream #34 = Utf8 SourceFile #35 = Utf8 Demo1_22.java #36 = NameAndType #13:#14 // "<init>":()V #37 = Utf8 a #38 = Utf8 b #39 = Utf8 ab #40 = Utf8 java/lang/StringBuilder #41 = NameAndType #51:#52 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #42 = NameAndType #53:#54 // toString:()Ljava/lang/String; #43 = Class #55 // java/lang/System #44 = NameAndType #56:#57 // out:Ljava/io/PrintStream; #45 = Class #50 // java/io/PrintStream #46 = NameAndType #58:#59 // println:(Z)V #47 = Utf8 cn/itcast/jvm/t1/stringtable/Demo1_22 #48 = Utf8 java/lang/Object #49 = Utf8 java/lang/String #50 = Utf8 java/io/PrintStream #51 = Utf8 append #52 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #53 = Utf8 toString #54 = Utf8 ()Ljava/lang/String; #55 = Utf8 java/lang/System #56 = Utf8 out #57 = Utf8 Ljava/io/PrintStream; #58 = Utf8 println #59 = Utf8 (Z)V { public cn.itcast.jvm.t1.stringtable.Demo1_22(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 4: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcn/itcast/jvm/t1/stringtable/Demo1_22; 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 a 2: astore_1 3: ldc #3 // String b 5: astore_2 6: ldc #4 // String ab 8: astore_3 9: new #5 // class java/lang/StringBuilder 12: dup 13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 16: aload_1 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 20: aload_2 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 27: astore 4 29: ldc #4 // String ab 31: astore 5 33: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload_3 37: aload 4 39: if_acmpne 46 42: iconst_1 43: goto 47 46: iconst_0 47: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V 50: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 53: aload_3 54: aload 5 56: if_acmpne 63 59: iconst_1 60: goto 64 63: iconst_0 64: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V 67: return LineNumberTable: line 11: 0 line 12: 3 line 13: 6 line 14: 9 line 15: 29 line 16: 33 line 17: 50 line 18: 67 LocalVariableTable: Start Length Slot Name Signature 0 68 0 args [Ljava/lang/String; 3 65 1 s1 Ljava/lang/String; 6 62 2 s2 Ljava/lang/String; 9 59 3 s3 Ljava/lang/String; 29 39 4 s4 Ljava/lang/String; 33 35 5 s5 Ljava/lang/String; StackMapTable: number_of_entries = 4 frame_type = 255 /* full_frame */ offset_delta = 46 locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, c lass java/lang/String ] stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, c lass java/lang/String ] stack = [ class java/io/PrintStream, int ] frame_type = 79 /* same_locals_1_stack_item */ stack = [ class java/io/PrintStream ] frame_type = 255 /* full_frame */ offset_delta = 0 locals = [ class "[Ljava/lang/String;", class java/lang/String, class java/lang/String, class java/lang/String, class java/lang/String, c lass java/lang/String ] stack = [ class java/io/PrintStream, int ] } SourceFile: "Demo1_22.java"
我们从中摘取最重要的一段:
Code: stack=3, locals=6, args_size=1 0: ldc #2 // String a 2: astore_1 3: ldc #3 // String b 5: astore_2 6: ldc #4 // String ab 8: astore_3 9: new #5 // class java/lang/StringBuilder 12: dup 13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 16: aload_1 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 20: aload_2 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 27: astore 4 29: ldc #4 // String ab 31: astore 5
上面每句的意思可参照jvm指令手册去进行阅读
0: ldc #2 // String a ldc 把常量池中的项压入栈,即在Constant pool中找到#2,然后将其压入栈。也就是String a
astore_1 // astore_1 将引用类型或returnAddress类型值存入局部变量1 也就是LocalVariableTable中的这一行 3 65 1 s1 Ljava/lang/String;
其他类似,
在索引9的位置:
9: new #5 // class java/lang/StringBuilder
这 就是创建了一个StringBuilder对象,
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
"
即
invokespecial 根据编译时类型来调用实例方法
16: aload_1 // aload_1 从局部变量1中装载引用类型值,就是之前的String a
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
调用了StringBuilder对象的append方法,
后面几句类似。
接着:
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
调用了StringBuilder对象的toString方法。
综合下来:
这一句虚拟机底层就干了这个事,
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()
我们来看看StringBuilder对象的toString方法。
@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
可以发现是创建了一个字符串对象:new String("ab")
而这一行:
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
对应:
29: ldc #4 // String ab 31: astore 5
即直接拿到了#4,可在常量池中对应找下。也就是"ab"v这个字符串。
所以:
new StringBuilder().append("a").append("b").toString() new String("ab")自然s3和s5都是从常量池中拿的"ab" 而s4是通过new StringBuilder().append("a").append("b").toString() // new String("ab") 即在堆中的对象
可参照jvm指令手册进行理解,随手百度即可。
总结:
常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
字符串常量拼接的原理是编译期优化