运行时常量池
一个二进制字节码文件包括了三个部分:类的基本信息、常量池、类方法定义(包括了虚拟机指令)。如果想查看一个类的二进制字节码的信息,可以使用Javap -v 字节码文件 来查看相应的信息。下面就是一个简单的Helloword的Java Demo对应的二进制字节码信息。
Classfile /F:/JavaProject/day_12/src/_01Calender/SayHello.class
Last modified 2020-2-23; size 421 bytes
MD5 checksum e5bd88fa5c10f13df7396dc70d7759f7
Compiled from "SayHello.java"
public class SayHello
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello world
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // SayHello
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 SayHello.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello world
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 SayHello
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public SayHello();
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 2: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
}
SourceFile: "SayHello.java"
在整个解析的最后,有关于main方法的一个说明,包括参数、访问类型,以及最终的虚拟机指令。首先看虚拟机指令的第一行“getstatic #2”,解释器在将二进制字节码进行解释的时候其实也只是认这一个内容,#2代表要去常量池Costant pool中的#2去查找,对应的是一个FieldRef,也就是变量引用,又对应#16和#17的信息,然后又通过#16和#17后面的内容确定最终要拿到PrintStrem这个变量。然后再来执行第二行虚拟机指令——“ldc #3”,在Constant Pool找到#3 看到是一个String 类型的数据对应#18的内容,找到#18行然后发现是字符串“Hello World”。
所谓常量池,就是一张表,虚拟机指令根据这张表去根据这张常量表找到要执行的类名、方法名、参数类型、字面量(就像上面的Hello world字符串、整数、布尔类型)的信息。
运行时常量池,常量池是*.class文件,当该类被加载,它的常量池信息就会被加载到运行时常量池,并把里面的符号地址变成真实地址。
常量池与StringTable(串池)
首先看一个简单字符串的二进制文件。
通过Javap得到的二进制文件为:
常量池中的内容,在运行时都会被加载到运行时常量池,这时a,b,ab都是常量池的符号,还不是Java字符串对象,此时它们就像#2、#3、#4这种符号。
执行到“ldc #2”时,会把a符号变成“a”字符串对象,在变为“a”字符串对象好之后,它会准备好一块空间,这块空间就叫做StringTable,也就是串池,刚开始它是空的,没有任何内容。在变为“a”字符串对象好之后,会把“a”做为key,去StringTable找有没有相同取值的Key(StringTable的数据结构其实就是hash表,且长度是固定,也不能扩容的),如果没有找到,就会把刚刚生成的“a”字符串对象放入串池,如果有就直接使用串池中的对象。总之,串池中的字符串对象只能存在一份。
我们经常会碰到下面的一段代码:
虽然s4和s5的内容是一致的,都是“ab”,但是具体的位置却不相同,通过Javap反编译就可以明白,其反编译的结果如下:
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: return
上面的二进制文件中从第9到第27都是String s4=s1+s2的二进制文件的内容,通过分析这句话其实是先找到StringBuilder类,创建了一个StringBuilder对象,调用了两次Append对象,然后又调用StringBuilder的ToString(),通过查看源码发现StringBuidler中的ToString()方法其实是new String(value, 0, count)即创建了一个对象,在前面我们了解到,通过new关键字创建的都是放在了堆中,所以就这一行,其实是做了很多事(new StringBuilder().append("a").append("b").toString() => new String("ab"))。从29到31其实就是String s5="a"+"b",其实是JavaC在编译期间的优化,结果在编译期间已经确定为了“ab”,都是常量,在编译期间都知道是常量“ab”,而上面的s1和s2是变量,在编译期间并不能确定。所以两行虽然值都是一样,但是最终的来源是不一样的。