Java字节码实例探究

深入理解java虚拟机第三版读书笔记06中介绍了class文件结构,这里我们动手实践,编译一个类查看一下它的字节码。

java源码:

public class Main {
    private int i = 10;
    private static int j = 40000;
    public static String str = "Hello World!";
    public static void main(String[] args){
        System.out.println(str);
    }
}

这个类有方法有实例变量有静态变量,在JDK8环境下编译后的字节码开头部分如下图:

该文件开头部分

我们来逐字节分析:

魔数、次版本号、主版本号

00~03:CA FE BA BE,魔数。

04\~05:00 00 Minor Version,次版本号
06\~07:00 34 十进制52,Major Version,主版本号,两者对应JDK8

常量池

08~09:00 2F 十进制47,代表常量池内项的数量。

10开始为常量池中的各表:

#1
0A:0A 十进制10,代表CONSTANT_Methodref_info类型,
0B~0E:00 0A 00 1E #10,#30

#2
0F:09 十进制9,代表CONSTANT_Fieldref_info类型,
10~13:00 09 00 1F #9,#31

#3
14:09 十进制9,代表CONSTANT_Fieldref_info类型,
15~18:00 20 00 21 #32,#33

#4
19:09 十进制9,代表CONSTANT_Fieldref_info类型,
1A~1D:00 09 00 22 #9,#34

#5
1E:0A 十进制9,代表CONSTANT_Methodref_info类型,
1F~22:00 23 00 24 #35,#36

#6
1E:03 十进制3,代表CONSTANT_Integer_info类型,
15~18:00 00 9C 40 40000

#7
28:09 十进制9,代表CONSTANT_Fieldref_info类型,
29~2C:00 09 00 25 #9,#37

#8
2D:08 十进制8,代表CONSTANT_String_info类型,
2E~2F:00 26 #38

#9
30:07 十进制7,代表CONSTANT_Class_info类型,
31~32:00 27 #39

#10
33:07 十进制7,代表CONSTANT_Class_info类型,
34~35:00 28 #40

#11
36:01 十进制1,代表CONSTANT_Utf8_info类型,
37~38:00 01代表长度为1,39:69 代表字符'i'

#12
3A:01 十进制1,代表CONSTANT_Utf8_info类型,
3B~3C:00 01代表长度为1,3D:49 代表字符'I'

剩余常量池的项我们用javap得到(其实累了):

#13 = Utf8               j
#14 = Utf8               str
#15 = Utf8               Ljava/lang/String;
#16 = Utf8               <init>
#17 = Utf8               ()V
#18 = Utf8               Code
#19 = Utf8               LineNumberTable
#20 = Utf8               LocalVariableTable
#21 = Utf8               this
#22 = Utf8               LMain;
#23 = Utf8               main
#24 = Utf8               ([Ljava/lang/String;)V
#25 = Utf8               args
#26 = Utf8               [Ljava/lang/String;
#27 = Utf8               <clinit>
#28 = Utf8               SourceFile
#29 = Utf8               Main.java
#30 = NameAndType        #16:#17        // "<init>":()V
#31 = NameAndType        #11:#12        // i:I
#32 = Class              #41            // java/lang/System
#33 = NameAndType        #42:#43        // out:Ljava/io/PrintStream;
#34 = NameAndType        #14:#15        // str:Ljava/lang/String;
#35 = Class              #44            // java/io/PrintStream
#36 = NameAndType        #45:#46        // println:(Ljava/lang/String;)V
#37 = NameAndType        #13:#12        // j:I
#38 = Utf8               Hello World!
#39 = Utf8               Main
#40 = Utf8               java/lang/Object
#41 = Utf8               java/lang/System
#42 = Utf8               out
#43 = Utf8               Ljava/io/PrintStream;
#44 = Utf8               java/io/PrintStream
#45 = Utf8               println
#46 = Utf8               (Ljava/lang/String;)V

访问标志、类索引、父类索引、接口索引集合

跳过常量池,到了访问标志:

01C1~01C2:00 21,代表ACC_SUPER(0x0020)和ACC_PUBLIC(0x0001)。

接着是类索引:

01C3~01C4:00 09 代表常量池中#9,#9又指向#39,可以得知是Main,即类名

父类索引:

01C5~01C6:00 0A 代表常量池中#10,#10又指向#40,可以得知是java/lang/Object

接口索引集合:

01C7~01D8:00 00 代表接口索引集合中没有数据,长度是0

字段表集合

01C9~01CA: 00 03 十进制3,代表字段表中有三项数据:

第一个字段:

01CB\~01CC: 00 02 ,访问标志,代表ACC_PRIVATE(0x0002)
01CD\~01CE:00 0B ,name_index,指向常量池#11,#11代表'i'
01CF\~01D0:00 0C , discriptor_index,指向常量池#12,#12代表'I'(即int类型)
01D1\~01D2:00 00 , attributes_count,代表该字段无属性表。

第二个字段:

01D3\~01D4: 00 0A ,访问标志,代表ACC_STATIC(0x0008)和ACC_PRIVATE(0x0002)
01D5\~01D6:00 0D ,name_index,指向常量池#13,#13代表'j'
01D7\~01D8:00 0C , discriptor_index,指向常量池#12,#12代表'I'(即int类型)
01D9\~01DA:00 00 , attributes_count,代表该字段无属性表。

第三个字段:

01DB\~01DC: 00 09 ,访问标志,代表ACC_STATIC(0x0008)和ACC_PUBLIC(0x0001)
01DD\~01DE:00 0E ,name_index,指向常量池#14,#14代表"str"
01DF\~01E0:00 0F , discriptor_index,指向常量池#15,#15代表"Ljava/lang/String;"(即String类型)
01E1\~01E2:00 00 , attributes_count,代表该字段无属性表。

方法表集合

01E3~01E4:00 03 ,十进制3,代表方法表中有三项数据:

第一个方法:

01E5\~01E6: 00 01 ,访问标志,代表ACC_PUBLIC(0x0001)
01E7\~01E8:00 10 ,name_index,指向常量池#16,#16代表"<init>"(即对象构造器)
01E9\~01EA:00 11 , discriptor_index,指向常量池#17,#17代表"()V"(即无参、无返回值)
01EB\~01EC:00 01 , attributes_count,代表属性表中有一项数据。
01ED\~01EE: 00 12 ,attribute_name_index,指向常量池#18,#18代表"Code"(即Code属性)
01EF\~01F2:00 00 00 39,代表Code内容长度为57个字节。

接下来57个字节我们不查表逐一翻译,查看javap提供的内容:

01F3~022B

Code:
stack=2, locals=1, args_size=1
    0: aload_0
    1: invokespecial #1                  // Method java/lang/Object."<init>":()V
    4: aload_0
    5: bipush        10
    7: putfield      #2                  // Field i:I
    10: return
LineNumberTable:
    line 7: 0
    line 8: 4
LocalVariableTable:
    Start  Length   Slot    Name    Signature
    0      11       0       this    LMain;

我们来解释一下它的字节码指令:

aload_0

将局部变量表slot 0加载到操作数栈,那么局部变量表slot 0原来存放的是什么呢?非静态方法局部变量表0位置一开始都是存放的this,即调用方法的当前对象。这句话就是把this入操作数栈。

invokespecial #1

this调用#1代表的方法,我们查常量表#1,#1又指向#10和#30,#10指向#40,是java/lang/Object,#30是"<init>":()V,即调用父类Object的构造方法。

aload_0

再次加载this

bipush 10

将常量10压入操作数栈。

putfield #2

putfield是设置对象的字段值,通过查常量表,#2代表Main中的i:I,这句话就把栈里的两个操作数:10设置给this.i

return

返回

第二个方法:

022C\~022D: 00 09 ,访问标志,代表ACC_PUBLIC(0x0001)和ACC_STATIC(0x0008)
022E\~022F:00 17 ,name_index,指向常量池#23,#23代表"main"
0230\~0231:00 18 , discriptor_index,指向常量池#24,#24代表"([Ljava/lang/String;)V"(即参数为String数组、无返回值)
0232\~0233:00 01 , attributes_count,代表属性表中有一项数据。
0234\~0235: 00 12 ,attribute_name_index,指向常量池#18,#18代表"Code"(即Code属性)
0236\~0239:00 00 00 38,代表Code内容长度为56个字节。

接下来56个字节我们不查表逐一翻译,查看javap提供的内容:

023A~0271

Code:
stack=2, locals=1, args_size=1
    0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
    3: getstatic     #4                  // Field str:Ljava/lang/String;
    6: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    9: return
LineNumberTable:
    line 12: 0
    line 13: 9
LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0      10     0  args   [Ljava/lang/String;

我们来解释一下它的字节码指令:

getstatic #3

获取一个类的静态字段,通过查常量表可知#3是java/lang/Systemout:Ljava/io/PrintStream;即获取System.out

getstatic #4

获取一个类的静态字段,通过查常量表可知#4是Mainstr:Ljava/lang/String;即获取str字段。

invokevirtual #5

调用#5方法,通过查常量表可知#5是java/io/PrintStreamprintln:(Ljava/lang/String;)V,即在操作栈的基础上调用System.out.println(str)

return

返回

第三个方法:

0272\~0273: 00 08 ,访问标志,代表ACC_STATIC(0x0008)
0274\~0275:00 1B ,name_index,指向常量池#27,#27代表"<clinit>"(即类构造器)
0276\~0277:00 11 , discriptor_index,指向常量池#17,#17代表"()V"(即无参无返回值)
0278\~0279:00 01 , attributes_count,代表属性表中有一项数据。
027A\~027B: 00 12 ,attribute_name_index,指向常量池#18,#18代表"Code"(即Code属性)
027C\~027F:00 00 00 27,代表Code内容长度为39个字节。

接下来39个字节我们不查表逐一翻译,查看javap提供的内容:

0280~02A6

Code:
stack=1, locals=0, args_size=0
    0: ldc           #6                  // int 40000
    2: putstatic     #7                  // Field j:I
    5: ldc           #8                  // String Hello World!
    7: putstatic     #4                  // Field str:Ljava/lang/String;
    10: return
LineNumberTable:
    line 9: 0
    line 10: 5

我们来解释一下它的字节码指令:

ldc #6

把一个常量#6加载到操作数栈,通过查常量表可知#6是40000。

putstatic #7

设置一个类的静态字段,通过查常量表可知#7是Mainj:I,即设置j的值为40000。(注意这里与第一个方法不同的是,设置小于等于short最大值的值的时候常数放在字节码中,而大于short最大值的常量放在常量表中)

ldc #8

把一个常量#8加载到操作数栈,通过查常量表可知#8是"Hello World!"

putstatic #4

设置一个类的静态字段,通过查常量表可知#4是Mainstr:Ljava/lang/String;,即将str的值设置为"Hello World!"

return

返回

属性表

02A7\~02A8:00 01 attributes_count,代表属性表中有一项数据
02A9\~02AA:00 1C attribute_name_index,指向常量池#28,#28代表"SourceFile"(记录源文件名称)
02AB\~02AE: 00 00 00 02 代表属性内容长度为2个字节。
02AF\~02B0: 00 1D 代表属性的值,指向常量池#29,#29代表"Main.java"

到此,该class文件的字节码全部分析完

该文件结尾部分

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/java%e5%ad%97%e8%8a%82%e7%a0%81%e5%ae%9e%e4%be%8b%e6%8e%a2%e7%a9%b6/