Java-“手撕”Class文件结构
前言
在上一章节 Java-JVM类文件结构 中描述了Class文件的组成,为了加深影响,这章将进行手动实践,编写一个Java示例文件,对编译生成后的Class文件进行一个一个字节的分析。
Java文件
以下是示例文件的Java代码:
1 | package com.itwray.study.advance.jvm; |
Class文件
通过 javac Main.java
命令生成Class文件,文件内容如下(本文使用Sublime Text):
1 | cafe babe 0000 0034 003e 0a00 0f00 2507 |
魔数
1 | u4 magic; //Class 文件的标志 |
从魔数开始,魔数是u4的无符号数,对应字节为 cafe babe
。
版本号
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
Class的小版本号是u2的无符号数,对应字节为 0000
,表示小版本号为0。
Class的大版本号是u2的无符号数,对应字节为 0034
,表示大版本号为52,即Java 8版本。
常量池
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
常量池的数量是u2的无符号数,对应字节为 003e
,对应十进制为62,表示常量池中有61个常量。
因此,在003e
后面的cp_info对应有61个常量,按照常量特性,第一个u1无符号数表示标志位,用于确定常量类型,接下来一个一个的列举出来。
-
0a
对应十进制为10,代表 CONSTANT_Methodref_info ,表示类中方法的符号引用,它对应的结构定义为如下:那么后面对应的值就是
000f
和0025
,十进制为15和37,分别表示方法返回类型的Class常量索引为15,方法的名称和描述符的常量索引为37。1
#1 = Methodref #15.#37 // java/lang/Object."<init>":()V
-
07
对应十进制为7,代表 CONSTANT_Class_info ,表示类或接口的符号引用,它对应的结构定义如下:后面对应的值就是
0026
,十进制为38,表示这个类的全限定名的常量索引为38。1
#2 = Class #38 // com/itwray/study/advance/jvm/Main
可以发现,每个常量的名称,无论是全限定名还是简单名称,到最后都会指向 CONSTANT_Utf8_info 常量,表示字符串的意思。
-
0a
同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为0002
和0025
,十进制为2和37,分别表示方法返回类型的Class常量索引为2,方法的名称和描述符的常量索引为37。1
#3 = Methodref #2.#37 // com/itwray/study/advance/jvm/Main."<init>":()V
通过第一个常量和第三个常量,可以发现它们的名称和描述符指向了同一个常量,说明Class文件中允许多个不同的方法有相同的名称和描述符,只要返回值不同,也是可以在一个Class文件中共存,这点与Java代码的重载(Overload)有点不同。
-
09
对应十进制为9,代表 CONSTANT_Fieldref_info ,表示字段的符号引用,它对应的结构定义如下:那么后面对应的值就是
0002
和0027
,十进制为2和39,分别表示字段所在的Class常量索引为2,字段的名称和描述符的常量索引为39。1
#4 = Fieldref #2.#39 // com/itwray/study/advance/jvm/Main.num:I
-
07
同第2个常量的类型一样,代表 CONSTANT_Class_info ,对应的值是0028
,十进制为40,表示这个类的全限定名的常量索引为40。1
#5 = Class #40 // java/lang/StringBuilder
-
0a
同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为0005
和0025
,十进制为5和37,分别表示方法返回类型的Class常量索引为5,方法的名称和描述符的常量索引为37。1
#6 = Methodref #5.#37 // java/lang/StringBuilder."<init>":()V
-
08
对应十进制为8,代表 CONSTANT_String_info ,表示字符串类型字面量,它的结构定义如下:对应的值就是
0029
,十进制为41,表示这个字符串对应的 CONSTANT_Utf8_info 索引为41。1
#7 = String #41 // wray
-
0a
同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为0005
和002a
,十进制为5和42,分别表示方法返回类型的Class常量索引为5,方法的名称和描述符的常量索引为42。1
#8 = Methodref #5.#42 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
-
0a
同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为0005
和002b
,十进制为5和43,分别表示方法返回类型的Class常量索引为5,方法的名称和描述符的常量索引为43。1
#9 = Methodref #5.#43 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
-
0a
同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为0005
和002c
,十进制为5和44,分别表示方法返回类型的Class常量索引为5,方法的名称和描述符的常量索引为44。
1 | #10 = Methodref #5.#44 // java/lang/StringBuilder.toString:()Ljava/lang/String; |
-
0a
同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为0002
和002d
,十进制为2和45,分别表示方法返回类型的Class常量索引为2,方法的名称和描述符的常量索引为45。1
#11 = Methodref #2.#45 // com/itwray/study/advance/jvm/Main.print:(Ljava/lang/String;)V
-
09
同第4个常量的类型一样,代表 CONSTANT_Fieldref_info ,对应为002e
和002f
,十进制为46和47,分别表示字段所在的Class常量索引为46,字段的名称和描述符的常量索引为47。1
#12 = Fieldref #46.#47 // java/lang/System.out:Ljava/io/PrintStream;
-
08
同第7个常量的类型一样,代表 CONSTANT_String_info ,对应值为0030
,十进制为48,表示这个字符串对应的 CONSTANT_Utf8_info 索引为48。1
#13 = String #48 // Hello
-
0a
同第1个常量的类型一样,代表 CONSTANT_Methodref_info ,对应为0031
和0032
,十进制为49和50,分别表示方法返回类型的Class常量索引为49,方法的名称和描述符的常量索引为50。1
#14 = Methodref #49.#50 // java/io/PrintStream.println:(Ljava/lang/String;)V
-
07
同第2个常量的类型一样,代表 CONSTANT_Class_info ,对应的值是0033
,十进制为51,表示这个类的全限定名的常量索引为51。1
#15 = Class #51 // java/lang/Object
-
01
对应十进制为1,代表 CONSTANT_Utf8_info,表示UTF-8编码的字符串,它的结构定义如下:对应的length选项值为
0003
,对应十进制为3,说明bytes选项的无符号数长度为3,即6e756d
,对应的字符串为num
。1
#16 = Utf8 num
-
从第17个常量到第36个常量,以及索引为38、40、41、48、51~61的常量,同第16个常量的类型一样,代表 CONSTANT_Utf8_info,它们对应的字符串如下:
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#17 = Utf8 I
#18 = Utf8 name
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 ConstantValue
#21 = Utf8 <init>
#22 = Utf8 ()V
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 LocalVariableTable
#26 = Utf8 this
#27 = Utf8 Lcom/itwray/study/advance/jvm/Main;
#28 = Utf8 main
#29 = Utf8 ([Ljava/lang/String;)V
#30 = Utf8 args
#31 = Utf8 [Ljava/lang/String;
#32 = Utf8 print
#33 = Utf8 (Ljava/lang/String;)V
#34 = Utf8 arg
#35 = Utf8 SourceFile
#36 = Utf8 Main.java
#38 = Utf8 com/itwray/study/advance/jvm/Main
#40 = Utf8 java/lang/StringBuilder
#41 = Utf8 wray
#48 = Utf8 Hello
#51 = Utf8 java/lang/Object
#52 = Utf8 append
#53 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#54 = Utf8 (I)Ljava/lang/StringBuilder;
#55 = Utf8 toString
#56 = Utf8 ()Ljava/lang/String;
#57 = Utf8 java/lang/System
#58 = Utf8 out
#59 = Utf8 Ljava/io/PrintStream;
#60 = Utf8 java/io/PrintStream
#61 = Utf8 println -
第37个常量,对应的字节码位置如下:
0c
的十进制为12,代表 CONSTANT_NameAndType_info ,表示字段或方法的部分符号引用,它的结构定义如下:对应的值是
0015
和0016
,十进制为21和22,分别表示对应的 CONSTANT_Utf8_info 常量的索引为21和22。1
#37 = NameAndType #21:#22 // "<init>":()V
关于如何在字节码文件中快速定位数据所处的字节码位置:在已知该项数据的上一个数据的值的情况下,可以根据数据的类型和值 反算出16进制编码,然后在编辑器中Ctrl + F搜索即可。(最好结合javap -verbose Xxx命令判断,避免找错)
-
常量索引为39、42、43、44、45、47、50的常量,均同第37个常量的类型一样,代表 CONSTANT_NameAndType_info ,它们对应的数据如下:
1
2
3
4
5
6
7#39 = NameAndType #16:#17 // num:I
#42 = NameAndType #52:#53 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#43 = NameAndType #52:#54 // append:(I)Ljava/lang/StringBuilder;
#44 = NameAndType #55:#56 // toString:()Ljava/lang/String;
#45 = NameAndType #32:#33 // print:(Ljava/lang/String;)V
#47 = NameAndType #58:#59 // out:Ljava/io/PrintStream;
#50 = NameAndType #61:#33 // println:(Ljava/lang/String;)V -
第46个常量同第2个常量的类型一样,代表 CONSTANT_Class_info ,其对应的16进制字节码位置可以按照前面的搜索逻辑查询,16进制结果为39,十进制为57,表示这个类的全限定名的常量索引为57。
1
#46 = Class #57 // java/lang/System
-
第49个常量同第2个常量的类型一样,代表 CONSTANT_Class_info ,16进制结果为3c,十进制为60,表示这个类的全限定名的常量索引为60。
1
#49 = Class #60 // java/io/PrintStream
至此,61个常量就分析完了。确定常量池最后在字节码文件中结束的位置如下:
01 0007 7072 696e 746c6e
是最后一个 CONSTANT_Utf8_info 常量的字节码内容,对应的字符串为println
。
访问标志
1 | u2 access_flags;//Class 的访问标记 |
访问标志是一个u2无符号数,对应的字节码是0021
,通过标志位表查询的结果为:ACC_PUBLIC、ACC_SUPER。
类索引、父类索引、接口索引集合
u2 this_class;//类索引
u2 super_class;//父类索引
u2 interfaces_count;//接口数量
u2 interfaces[interfaces_count];//一个类可以实现多个接口
类索引是u2无符号数,对应字节码为0002
,十进制为2,表示当前类文件的类对应为常量池中索引为2的常量。
1 | #2 = Class #38 // com/itwray/study/advance/jvm/Main |
父类索引是u2无符号数,对应字节码为000f
,十进制为15,表示当前类文件的父类对应为常量池中索引为15的常量。
1 | #15 = Class #51 // java/lang/Object |
接口索引集合的结构如下:
1 | u2 interfaces_count;//接口数量 |
对应字节码为0000
,十进制为0,表示当前类文件没有接口。
字段表集合
u2 fields_count;//字段数量
field_info fields[fields_count];//一个类可以有多个字段
字段表是先以一个u2无符号数表示字段数量,对应字节码为0002
,十进制为2,表示类中有2个字段。
字段的结构定义固定如下:
access_flags的标志字典如下:
接下来一个个字段的分析:
-
access_flags对应字节码为
0002
,表示ACC_PRIVATE;name_index对应字节码为
0010
,十进制为16,表示字段的简单名称对应常量池的索引为16,即\#16 = Utf8 num
;descriptor_index对应字节码为
0011
,十进制为17,表示字段的描述符对应常量池的索引为17,即\#17 = Utf8 I
;attributes_count对应字节码为
0000
,说明该字段没有属性信息,即没有attributes_info。通过匹配常量池的索引,该字段为:
private int num
。 -
access_flags对应字节码为
001a
,0010
表示ACC_FINAL,000a
则是0002
和0008
的按位或运算结果,所以还表示ACC_PRIVATE、ACC_STATIC。name_index对应字节码为
0012
,十进制为18,表示字段的简单名称对应常量池的索引为18,即\#18 = Utf8 name
;descriptor_index对应字节码为
0013
,十进制为19,表示字段的描述符对应常量池的索引为19,即\#19 = Utf8 Ljava/lang/String;
;attributes_count对应字节码为
0001
,说明该字段有1个属性信息,根据属性表的结构定义,开始是一个u2无符号数,表示属性名称在常量池中的索引,对应字节码为0014
,十进制为20,对应常量池的\#20 = Utf8 ConstantValue
。ConstantValue
的属性结构如下:根据属性结构得出紧接着后面是一个u4无符号数,表述属性的长度,对应字节码
0000 0002
,表示该属性的长度是2个u1(与属性结构中的第三个u2刚好对应上)。对应的属性长度的字节码为0007
,通过ConstantValue
标志表示这个字段是一个常量属性,然后通过属性长度的字段找到常量池中索引为7的常量,内容为\#7 = String #41 // wray
,说明这个常量字段的值是一个字符串,字符串内容对应常量池中索引41,即\#41 = Utf8 wray
。最后通过匹配常量池的索引,该字段为:
private static final String name = "wray"
。
方法表集合
1 | u2 methods_count;//方法数量 |
方法表同字段表的结构几乎一样,先是以一个u2无符号数表示方法数量,对应字节码为0003
,表示当前类有3个方法。方法表的结构定义与字段表一样:
只是access_flags访问标志有一点区别,访问标志字典如下:
接下来一个个方法的分析:
-
access_flags对应字节码为
0001
,表示ACC_PUBLIC;name_index对应字节码为
0015
,十进制为21,表示字段的简单名称对应常量池的索引为21,即\#21 = Utf8 <init>
;descriptor_index对应字节码为
0016
,十进制为22,表示字段的描述符对应常量池的索引为22,即\#22 = Utf8 ()V
;从上面三个标志可知,这是当前类无参构造函数。
attributes_count对应字节码为
0001
,说明该字段有1个属性信息,根据属性表的结构定义,开始是一个u2无符号数,表示属性名称在常量池中的索引,对应字节码为0017
,十进制为23,对应常量池的\#23 = Utf8 Code
。Code
属性的结构定义如下:Code
属性较为复杂,接下来一个个类型再分析:- attribute_name_index 在
Code
属性中肯定对应的是常量池中的Code
常量,也就是属性最开始的u2无符号数,即0017
。 - attribute_lenth表示属性值的长度,由于属性名称索引与属性长度一共为6个字节,所以属性值的长度固定为整个属性表长度减去6个字节。对应字节码为
0000 002f
,十进制为47,所以属性值长度为41。 - max_stack代表操作数栈深度的最大值,
- 在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。对应字节码为
0001
。 - max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。对应字节码为
0001
。
对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放。
注意,并不是在方法中用了多少个局部变量,就把这些局部变量所占变量槽数量之和作为max_locals的值,操作数栈和局部变量表直接决定一个该方法的栈帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费。Java虚拟机的做法是将局部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出max_locals的大小。
-
code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。
code_lenth对应字节码为
0000 0005
,说明code有5个u1,那么code对应字节码为2ab7 0001 b1
。每一个u1对应一个字节码指令,具体指令可以参考《深入理解Java虚拟机》附录C“虚拟机字节码指令表”。 -
exception_table_length和exception_table表示方法的异常表,异常表有自己的表结构定义(从上到下、从左到右):
exception_table_length对应字节码为
0000
,表示没有异常表,所以exception_table为空。 -
attributes_count和attributes则表示属性表(attribute_info),说明
Code
属性内部可以包含其他属性,例如LineNumberTable
和LocalVariableTable
等子属性。attributes_count对应字节码为0002
,表示有两个属性表,属性表结构如下:接下来一个个属性分析:
-
attribute_name_index对应字节码为
0018
,十进制为24,说明该属性的类型在常量池索引为24中,即\#24 = Utf8 LineNumberTable
。LineNumberTable
属性的结构这里就不在过多分析了。!!!只需要记住,在属性表结构中通过attribute_lenth确定了长度,attribute_length个u1就是这个属性的总长度,无论这个属性的内部结构怎么变,最后的总长度就是 u2 + u4 + attribute_length个u1。
再看attribute_lenth对应字节码为
0000 0006
,表示info的长度为6,对应字节码为0001 0000 0009
。 -
attribute_name_index对应字节码为
0019
,十进制为25,说明该属性的类型在常量池索引为25中,即\#25 = Utf8 LocalVariableTable
。attribute_lenth对应字节码为0000 000c
,表示info的长度为12,对应字节码为0001 0000 0005 001a 001b 0000
。
-
- attribute_name_index 在
-
接下来是第二个方法,access_flags对应字节码为
0009
,对应的字节码标志值为0001
和0008
,即表示ACC_PUBLIC、ACC_STATIC;name_index对应字节码为
001c
,十进制为28,表示字段的简单名称对应常量池的索引为28,即\#28 = Utf8 main
;descriptor_index对应字节码为
001d
,十进制为29,表示字段的描述符对应常量池的索引为29,即\#29 = Utf8 ([Ljava/lang/String;)V
;从上面三个标志可知,该方法的定义为:
public static void main(String[] arg0)
。其中
arg0
参数名称是不被虚拟机关注的,可以通过方法对应的属性表找到LocalVariableTable
属性以确定实际代码中的参数名称。attributes_count对应字节码为
0001
,说明该字段有1个属性信息,根据属性表的结构定义,开始是一个u2无符号数,表示属性名称在常量池中的索引,对应字节码为0017
,十进制为23,对应常量池的\#23 = Utf8 Code
。因为与第一个方法属性一样,就不再展开分析了,按照属性表的通用结构定义直接分析字节码,再次展示一遍属性表的结构定义:attribute_length对应字节码为
0000 006d
,十进制为109,说明info有109个u1无符号数,对应字节码如下(灰色选中区域): -
第三个方法,access_flags对应字节码为
0002
,表示ACC_PRIVATE;name_index对应字节码为
0020
,十进制为32,表示字段的简单名称对应常量池的索引为32,即\#32 = Utf8 print
;descriptor_index对应字节码为
0021
,十进制为33,表示字段的描述符对应常量池的索引为33,即\#33 = Utf8 (Ljava/lang/String;)V
;从上面三个标志可知,该方法的定义为:
private void print(String arg0)
。attributes_count对应字节码为
0001
,说明该字段有1个属性信息,根据属性表的结构定义,开始是一个u2无符号数,表示属性名称在常量池中的索引,对应字节码为0017
,十进制为23,对应常量池的\#23 = Utf8 Code
。同前两个方法一样,直接看attribute_length对应字节码为00 0000 52
,十进制为82,info对应的82个字节码如下(灰色选中区域):
属性表集合
1 | u2 attributes_count;//此类的属性表中的属性数 |
首先第一个u2无符号数表示属性表的属性数,对应字节码为0001
,表示有一个属性。
同分析方法表中的属性一样,再次根据attribute_info(属性表)的结构定义分析:
attribute_name_index对应字节码为0023
,十进制为35,对应常量池中索引为35的常量,即\#35 = Utf8 SourceFile
。
接下来是attribute_length,对应字节码为00 0000 02
,表示info的长度为2。
info对应的字节码为00 24
。
至此,这个Class文件分析完毕(完结撒花~)。
总结
从魔数到最后的属性表集合,一个个u1无符号数分析下来,不得不感慨Class文件结构的紧凑,因为它真的没有任何一个分隔符,但即使文件结构紧凑,仍然提供了很多可扩展的特性,并且通过《Java虚拟机规范》实现了平台无关性、语言无关性。
而且,这还是从Java初版到至今,仍然维持Class文件结构几乎不变,且功能稳定实用。
再次佩服Java语言设计者们的智慧。