JVM系列之String
程序员文章站
2022-05-09 22:18:15
...
一、常量池
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进去方法区的运行时常量池中存放。Java语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,比如String类的intern()方法。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
二、JVM处理String
编译:javac JvmTest.java
反编译:javap -v JvmTest
8-39行:常量池
41-54:默认构造方法
46: aload_0 指令是把0索引的局部变量进栈 this
47: invokespecial调用父类构造方法
48: ldc指令格式:ldc,index
ldc指令过程:要执行ldc指令,JVM首先查找index所指定的常量池入口,在index指向的常量池入口,JVM将会查找CONSTANT_Integer_info,CONSTANT_Float_info和CONSTANT_String_info入口。如果还没有这些入口,JVM会解析它们。而对于上面的haha,JVM在运行时环境中解析ldc指令时,会找到CONSTANT_String_info入口,同时,将把指向被拘留字符串对象(由解析该入口的进程产生)的引用压入操作数栈。
50: putfield给实例变量c赋值
61-62: 把常量池中的haha字符串压入操作数栈,然后出栈将该值存入由索引1指定的局部变量中。
62: astore_1指令格式:astore_1
astore_1指令过程: 要执行astore_1指令,JVM从操作数栈顶部弹出一个引用类型或者returnAddress类型值,然后将该值存入由索引1指定的局部变量中,即将引用类型或者returnAddress类型值存入局部变量1。
63: new指令格式:new indexbyte1,indexbyte2
new指令过程:要执行new指令,Jvm通过计算(indextype1<<8)|indextype2生成一个指向常量池的无符号16位索引。然后JVM根据计算出的索引查找常量池入口。该索引所指向的常量池入口必须为CONSTANT_Class_info。如果该入口尚不存在,那么JVM将解析这个常量池入口,该入口类型必须是类。JVM从堆中为新对象映像分配足够大的空间,并将对象的实例变量设为默认值。最后JVM将指向新对象的引用objectref压入操作数栈。
64: dup指令格式:dup
dup指令过程:要执行dup指令,JVM复制了操作数栈顶部一个字长的内容,然后再将复制内容压入栈。本指令能够从操作数栈顶部复制任何单位字长的值。但绝对不要使用它来复制操作数栈顶部任何两个字长(long型或double型)中的一个字长。上面例中,即复制引用objectref,这时在操作数栈存在2个引用。
66: invokespecial调用String构造方法
80: putstatic赋值静态变量d
总结:实例变量在构造方法中初始化。静态变量在静态代码块中初始化。
三、案例分析
案例一
分析:
1. “a”和1都是常量,在编译期间JVM就将常量字符串的"+"连接优化为连接后的值,拿"a" + 1来说,经编译器优化后在class中就已经是a1。故第一个是true。
2. 由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + b无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给bb。所以上面程序的结果也就为false。
3. 对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"a" + cc和"a" + "b"效果是一样的。故上面程序的结果为true。
案例二
分析:可以看出,[2]的流程其实等价于下面JAVA代码:
String c = new StringBuilder().append("a").append(b).toString();
案例三
分析:
1. a引用的“ab”在常量池,s是StringBuilder.toString()中新new了一个String,在堆中分配对象。故第一个判断的结果为false。
2. 当调用 intern 方法时,如果常量池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。故第二个判断结果为true。
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进去方法区的运行时常量池中存放。Java语言并不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中,比如String类的intern()方法。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
二、JVM处理String
public class JvmTest { private String c = "hello"; private static String d = "hello"; public static void main(String[] args) { String a = "haha"; String b = new String("haha"); } }
编译:javac JvmTest.java
反编译:javap -v JvmTest
public class spring.proxy.JvmTest minor version: 0 //class 次版本号 major version: 53 //class 主版本号 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #8 // spring/proxy/JvmTest super_class: #9 // java/lang/Object interfaces: 0, fields: 2, methods: 3, attributes: 1 Constant pool: #1 = Methodref #9.#22 // java/lang/Object."<init>":()V #2 = String #23 // hello #3 = Fieldref #8.#24 // spring/proxy/JvmTest.c:Ljava/lang/String; #4 = String #25 // haha #5 = Class #26 // java/lang/String #6 = Methodref #5.#27 // java/lang/String."<init>":(Ljava/lang/String;)V #7 = Fieldref #8.#28 // spring/proxy/JvmTest.d:Ljava/lang/String; #8 = Class #29 // spring/proxy/JvmTest #9 = Class #30 // java/lang/Object #10 = Utf8 c #11 = Utf8 Ljava/lang/String; #12 = Utf8 d #13 = Utf8 <init> #14 = Utf8 ()V #15 = Utf8 Code #16 = Utf8 LineNumberTable #17 = Utf8 main #18 = Utf8 ([Ljava/lang/String;)V #19 = Utf8 <clinit> #20 = Utf8 SourceFile #21 = Utf8 JvmTest.java #22 = NameAndType #13:#14 // "<init>":()V #23 = Utf8 hello #24 = NameAndType #10:#11 // c:Ljava/lang/String; #25 = Utf8 haha #26 = Utf8 java/lang/String #27 = NameAndType #13:#31 // "<init>":(Ljava/lang/String;)V #28 = NameAndType #12:#11 // d:Ljava/lang/String; #29 = Utf8 spring/proxy/JvmTest #30 = Utf8 java/lang/Object #31 = Utf8 (Ljava/lang/String;)V { public spring.proxy.JvmTest(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String hello 7: putfield #3 // Field c:Ljava/lang/String; 10: return LineNumberTable: line 8: 0 line 10: 4 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=3, args_size=1 0: ldc #4 // String haha 2: astore_1 3: new #5 // class java/lang/String 6: dup 7: ldc #4 // String haha 9: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V 12: astore_2 13: return LineNumberTable: line 15: 0 line 16: 3 line 17: 13 static {}; descriptor: ()V flags: (0x0008) ACC_STATIC Code: stack=1, locals=0, args_size=0 0: ldc #2 // String hello 2: putstatic #7 // Field d:Ljava/lang/String; 5: return LineNumberTable: line 12: 0 }
8-39行:常量池
41-54:默认构造方法
46: aload_0 指令是把0索引的局部变量进栈 this
47: invokespecial调用父类构造方法
48: ldc指令格式:ldc,index
ldc指令过程:要执行ldc指令,JVM首先查找index所指定的常量池入口,在index指向的常量池入口,JVM将会查找CONSTANT_Integer_info,CONSTANT_Float_info和CONSTANT_String_info入口。如果还没有这些入口,JVM会解析它们。而对于上面的haha,JVM在运行时环境中解析ldc指令时,会找到CONSTANT_String_info入口,同时,将把指向被拘留字符串对象(由解析该入口的进程产生)的引用压入操作数栈。
50: putfield给实例变量c赋值
61-62: 把常量池中的haha字符串压入操作数栈,然后出栈将该值存入由索引1指定的局部变量中。
62: astore_1指令格式:astore_1
astore_1指令过程: 要执行astore_1指令,JVM从操作数栈顶部弹出一个引用类型或者returnAddress类型值,然后将该值存入由索引1指定的局部变量中,即将引用类型或者returnAddress类型值存入局部变量1。
63: new指令格式:new indexbyte1,indexbyte2
new指令过程:要执行new指令,Jvm通过计算(indextype1<<8)|indextype2生成一个指向常量池的无符号16位索引。然后JVM根据计算出的索引查找常量池入口。该索引所指向的常量池入口必须为CONSTANT_Class_info。如果该入口尚不存在,那么JVM将解析这个常量池入口,该入口类型必须是类。JVM从堆中为新对象映像分配足够大的空间,并将对象的实例变量设为默认值。最后JVM将指向新对象的引用objectref压入操作数栈。
64: dup指令格式:dup
dup指令过程:要执行dup指令,JVM复制了操作数栈顶部一个字长的内容,然后再将复制内容压入栈。本指令能够从操作数栈顶部复制任何单位字长的值。但绝对不要使用它来复制操作数栈顶部任何两个字长(long型或double型)中的一个字长。上面例中,即复制引用objectref,这时在操作数栈存在2个引用。
66: invokespecial调用String构造方法
80: putstatic赋值静态变量d
总结:实例变量在构造方法中初始化。静态变量在静态代码块中初始化。
三、案例分析
案例一
String a = "a1"; String aa = "a" + 1; System.out.println((a == aa)); //result = true String ab = "ab"; String b = "b"; String bb = "a" + b; System.out.println((bb == ab)); //result = false String c = "ab"; final String cc = "b"; String ccc = "a" + cc; System.out.println((ccc == c)); //result = true
分析:
1. “a”和1都是常量,在编译期间JVM就将常量字符串的"+"连接优化为连接后的值,拿"a" + 1来说,经编译器优化后在class中就已经是a1。故第一个是true。
2. 由于在字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + b无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给bb。所以上面程序的结果也就为false。
3. 对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。所以此时的"a" + cc和"a" + "b"效果是一样的。故上面程序的结果为true。
案例二
String a = "a"; String b = "b"; String c = a + b;
0: ldc #16; //String a //将常量池中的a压入操作数栈 2: astore_1 //将引用a存放到1号局部变量中 3: ldc #18; //String b //将常量池中的b压入操作数栈 5: astore_2 //将引用b存放到2号局部变量中 6: new #20; //class java/lang/StringBuilder //检查到非常量的相加,这时创建 StringBuilder 对象,将引用放入栈 9: dup //复制刚放入的引用(这时存在着两个相同的引用) 10: aload_1 //从1号局部变量中加载数据引用a到操作数栈中 11: invokestatic #22; //Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String; //调用String类的valueOf方法 14: invokespecial #28; //Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V //对StringBulider对象进行一些初始化 17: aload_2 //从2号局部变量中加载数据引用b到操作数栈中 18: invokevirtual #31; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //调用 StringBuilder的append方法,把字符串b添加进去 21: invokevirtual #35; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; //调用 StringBuilder的toString方法 24: astore_3 //将toString的结果保存至3号局部变量 25: return //结束程序返回
分析:可以看出,[2]的流程其实等价于下面JAVA代码:
String c = new StringBuilder().append("a").append(b).toString();
案例三
String a = "ab"; String s1 = "a"; String s2 = "b"; String s = s1 + s2; System.out.println(s == a);//false System.out.println(s.intern() == a);//true
分析:
1. a引用的“ab”在常量池,s是StringBuilder.toString()中新new了一个String,在堆中分配对象。故第一个判断的结果为false。
2. 当调用 intern 方法时,如果常量池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。故第二个判断结果为true。