- 实例
- 常量池
- u2 access_flags
- u2 this_class
- u2 super_class
- interfaces_count 、 interfaces[interfaces_count]
- fields_count、field_info
- TestClass字段(fields_count)的数量
- int staticVar = 0
- methods_count、method_info
- TestClass第1个方法
- TestClass第2个方法
- Class文件的属性
实例
首先有一个TestClass类,代码如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14package com.ejushang.TestClass;
public class TestClass implements Super {
private static final int staticVar = 0;
private int instanceVar = 0;
public int instanceMethod(int param) {
return param + 1;
}
}
interface Super {
}
通过jdk1.6.0_37的javac编译后的TestClass.java对应的TestClass.class的二进制结构如下。
TestClass.class
用WinHex工具查看字节码。
魔数
从Class的文件结构我们知道,刚开始的4个字节是魔数,上图中从地址00000000h-00000003h
的内容就是魔数,从上图可知Class的文件的魔数是0xCAFEBABE
。
主次版本号
接下来的4个字节是主次版本号,由上图可知从00000004h-00000005h
对应的是0x0000
,因此Class的minor_version
为0x0000
,从00000006h-00000007h
对应的内容为0x0032
,因此Class文件的major_version
版本为0x0032
,这正好就是jdk1.6.0
不带target
参数编译后的Class对应的主次版本。
常量池的数量
接下来的2个字节从00000008h-00000009h
表示常量池的数量,由上图可以知道其值为0x0018,十进制为24个,但是对于常量池的数量需要明确一点,常量池的数量是constant_pool_count-1
,为什么减一,是因为索引0表示class中的数据项不引用任何常量池中的常量。
常量池
我们上面说了常量池中有不同类型的常量,下面就来看看TestClass.class的第一个常量,我们知道每个常量都有一个u1
类型的tag
标识来表示常量的类型,上图中0000000ah
处的内容为0x0A
,转换成二级制是10,由上面的关于常量类型的描述可知tag
为10的常量是Constant_Methodref_info
,而Constant_Methodref_info
的结够如下图(常量池结构)。
class_index
指向常量池中类型为CONSTANT_Class_info
的常量,从TestClass的二进制文件结构中可以看出class_index
的值为0x0004
(地址为0000000bh-0000000ch
),也就是说指向第四个常量。
name_and_type_index
指向常量池中类型为CONSTANT_NameAndType_info
常量。从上图可以看出name_and_type_index
的值为0x0013
,表示指向常量池中的第19个常量.接下来又可以通过同样的方法来找到常量池中的所有常量。
不过JDK提供了一个方便的工具可以让我们查看常量池中所包含的常量。通过javap -verbose TestClass
即可得到所有常量池中的常量,截图如下。
TestClass中常量池有24个常量,不要忘记了第0个常量,因为第0个常量被用来表示Class中的数据项不引用任何常量池中的常量。从上面的分析中我们得知TestClass的第一个常量表示方法,其中class_index指向的第四个常量为java/lang/Object
,name_and_type_index
指向的第19个常量值为:()V
,从这里可以看出第一个表示方法的常量表示的是Java编译器生成的实例构造器方法。
通过同样的方法可以分析常量池的其它常量。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
230A 0004 0013
09 0003 0014 int instanceVar
07 0015 utf-8
07 0016 utf-8
07 0017 utf-8
01 0009 737461746 963566172
01 0001 49
01 000D 436F6E7374616 E7456 616C7 565
03 00000000
01 000B 696E7 37461 6E636 5566172
01 0006 3C696E 69743E
01 0003 282956
01 0004 436F6465
01 000F 4C696E654E 756D626572 5461626C65
01 000E 696E737461 6E63654D65 74686F64
01 0004 28492949
01 000A 536F757263 6546696C65
01 000E 5465737443 6C6173732E 6A617661
0C 000B 000C
0C 000A 0007
01 0020 636F6D2F65 6A75736861 6E672F5465 7374436C61 73732F5465 7374436C61 7373
01 0010 6A6176612F 6C616E672F 4F626A6563 74
01 001C 636F6D2F65 6A75736861 6E672F5465 7374436C61 73732F5375 706572
注:utf-8(string)= 1个英文字符占1个byte,1个中文字符占2个byte,长度=byte长度。
u2 access_flags
表示类或者接口方面的访问信息,比如Class表示的是类还是接口,是否为public
,static
,final
等。具体访问标示的含义之前已经说过了,下面我们就来看看TestClass的访问标示。Class的访问标示是从00000100d-0000010e
,其值为0x0021
,根据前面说的各种访问标示的标志位,我们可以知道0x0021=0x0001|0x0020
,也即ACC_PUBLIC
和ACC_SUPER
为真,其中ACC_PUBLIC
大家好理解,ACC_SUPER
是jdk1.2之后编译的类都会带有的标志。
u2 this_class
表示类的索引值,用来表示类的全限定名称,类的索引值如下图。
类索引值为0x0003
,对应常量池的第三个常量,通过javap
的结果,我们知道第三个常量为CONSTANT_Class_info
类型的常量,通过它可以知道类的全限定名称为com/ejushang/TestClass/TestClass
。
u2 super_class
表示当前类的父类的索引值,索引值指向常量池中类型为CONSTANT_Class_info
的常量,父类的索引值如下图所示,其值为0x0004
,查看常量池的第四个常量,可知TestClass的父类的全限定名称为java/lang/Object
。
interfaces_count 、 interfaces[interfaces_count]
表示接口数量以及具体的每一个接口,TestClass的接口数量以及接口如下图所示,其中0x0001
表示接口数量为1,而0x0005
表示接口在常量池的索引值,找到常量池的第五个常量,其类型为CONSTANT_Class_info
,其值为com/ejushang/TestClass/Supe
。
fields_count、field_info
fields_count
表示类中field_info
表的数量,而field_info
表示类的实例变量和类变量,这里需要注意的是field_info
不包含从父类继承过来的字段,field_info
的结构如下图。
access_flags
表示字段的访问标识,比如public
,private
,protected
,static
。final
等,access_flags
的取值如下图。
name_index
常量池的索引值,表示字段的名称。
descriptor_index
常量池的索引值,表示字段的描述符。
其实在JVM规范中,对于字段的描述符规定如下图。
关注上图最后一行,它表示的是对一维数组的描述符,对于数组类型,每一维度将使用一个前置的[
字符来描述,String[][]
的描述符将是[[Ljava/lang/String
,而对于int[][]
的描述符为[[I
。
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述。参数列表按照严格的顺序放在一组小括号()
内。
如方法void inc()
的描述符为()V
,方法java.lang.String.toString()
的描述符为()Ljava/lang/String;
,方法int indexOf(char[] source
,int offset,int count,char[] target
,int tOffset
,int tCount
,int fromIndex
的描述符为([CII[CIII)I
。
attributes_count
表示属性表的数量。
attribute_info
表示属性表的属性表。
TestClass字段(fields_count)的数量
fields_count = 2
,TestClass有两个字段,查看TestClass的源代码可知,确实也只有两个字段,接下来我们看看第一个字段,我们知道第一个字段应该为private int staticVar
,它在Class文件中的二进制表示如下图。
fields_info
access_flags
0x001A
表示访问标示,通过查看access_flags
表可知,其为ACC_PRIVATE,ACC_STATIC
,ACC_FINAL
。
name_index
0x0006
表示常量池中第6常量,通过查看常量池可知,其值为staticVar
,其中staticVar
为字段名称。
descriptor_index
0x0007
表示常量第7个常量,通过查看常量池可知,其值为I
,而I为字段的描述符,通过上面对描述符的解释,I
所描述的是int
类型的变量。
attributes_count
0x0001
表示staticVar
这个字段表中的属性表的数量,从上图可以staticVar
字段对应的属性表有1个。
attributes[attributes_count]
0x0008
表示属性表集合,常量池中的第8个常量,查看常量池可以得知此属性为ConstantValue
属性。ConstantValue
属性的格式如下图。
attribute_name_index
0x0008
表述属性名的常量池索引,本例中为ConstantValue
。
attribute_length
0x00000002
固定长度为2。
constantValue_index
表示常量池中的引用,本例中,其中为0x0009
,查看第9个常量可以知道,它表示一个类型为CONSTANT_Integer_info
的常量,其值为0。
int staticVar = 0
private static final int staticVar = 0
instanceVar
的二进制表示如下图。
其中0x0002
表示访问标示为ACC_PRIVATE
,0x000A
表示字段的名称,它指向常量池中的第10个常量,查看常量池可以知道字段名称为instanceVar
,而0x0007
表示字段的描述符,它指向常量池中的第7个常量,查看常量池可以知道第7个常量为I
,表示类型为instanceVar
的类型为I
,最后0x0000
表示属性表的数量为0。
methods_count、method_info
方法表和字段表field_info
虽然都有属性数量和属性表,但是它们里面所包含的属性是不同。methods_count
表示方法的数量,而method_info
表示的方法表,其中方法表的结构如下图。
可以看出method_info
和field_info
的结构是很类似的,由于ACC_VOLATILE
标志和ACC_TRANSIENT
标志不能修饰方法,所以access_flags
中不包含这两项,同时增加ACC_SYNCHRONIZED
标志、ACC_NATIVE
标志、ACC_STRICTFP
标志和ACC_ABSTRACT
标志 ,方法表的access_flag
的所有标志位以及取值如下图。
标志名称 | 标志值 | 含义
—–|—–|—
ACC_PUBLIC | 0x0001 | 字段是否为public
ACC_PRIVATE | 0x0002 | 字段是否为private
ACC_PROTECTED | 0x0004 | 字段是否为protected
ACC_STATIC | 0x0008 | 字段是否为static
ACC_FINAL | 0x0010 | 字段是否为final
ACC_SYNCHRONIZED | 0x0020 | 字段是否为synchronized
ACC_BRIDGE | 0x0040 | 方法是否是由编译器产生的桥接方法
ACC_VARARGS | 0x0080 | 方法是否接受不定参数
ACC_NATIVE | 0x0100 | 字段是否为native
ACC_ABSTRACT | 0x0400 | 字段是否为abstract
ACC_STRICTFP | 0x0800 | 字段是否为strictfp
ACC_SYNTHETIC | 0x1000 | 字段是否为编译器自动产生
name_index
表示的是方法的名称,指向常量池的索引。
descriptor_index
表示的是方法的描述符,指向常量池的索引。
方法的描述符的结构为(参数列表)返回值,比如public int instanceMethod(int param)
的描述符为(I)I
,表示带有一个int类型参数且返回值也为int
类型的方法。
attributes_count
属性数量。
attributes
属性表。
方法表的二进制。
TestClass第1个方法
方法表的数量为0x0002
表示有两个方法,分析第1个方法,首先来看一下TestClass的第1个方法的access_flag
,name_index
,descriptor_index
。
access_flag
0x0001
表示访问标示,通过查看access_flags
表可知,其为ACC_PUBLIC
。
name_index
0x000B
表示方法的名字,查看常量池中的第11个常量。
descriptor_index
0x000C
表示descriptor_index
表示常量池中的第12常量,其值为()V
,表示方法没有参数和返回值,其实这是编译器自动生成的实例构造器方法。
attributes_count
0x0001表示方法的方法表有1个属性。
属性表图。0x000D
对应的常量池中的常量为Code
,表示的方法的Code
属性,所以到这里大家应该明白:方法的那些代码是存储在Class文件方法表中的属性表中的Code
属性中。接下来我们在分析一下Code
属性,Code
属性的结构如下图。
attribute_name_index
指向常量池中值为Code
的常量。
attribute_length
的长度表示Code
属性表的长度(这里需要注意的时候长度不包括attribute_name_index
和attribute_length
的6个字节的长度)。
max_stack
表示最大栈深度,虚拟机在运行时根据这个值来分配栈帧中操作数的深度。
max_locals
代表了局部变量表的存储空间,它的单位为slot
,slot
是虚拟机为局部变量分配内存的最小单元,在运行时,对于不超过32位类型的数据类型,比如byte
,char
,int
等占用1个slot
,而double
和Long
这种64位的数据类型则需要分配2个slot
,另外max_locals
的值并不是所有局部变量所需要的内存数量之和,因为slot
是可以重用的,当局部变量超过了它的作用域以后,局部变量所占用的slot
就会被重用。
code_length
代表了字节码指令的数量,而code
表示的时候字节码指令,从上图可以知道code
的类型为u1
,一个u1
类型的取值为0x00-0xFF
,对应的十进制为0-255
,目前虚拟机规范已经定义了200多条指令。
exception_table_length\exception_table
分别代表方法对应的异常信息。
attributes_count
表示Code
属性中的属性数量。
attribute_info
表示Code
属性中的属性表,从这里可以看出Class的文件结构中,属性表是很灵活的,它可以存在于Class文件,方法表,字段表以及Code
属性中。
继续以上面的图来分析,从上面init()
的Code
属性开始。
attribute_length
属性表的长度为0x00000026
。
max_stack
0x0002。
max_locals
0x0001。
code_length
0x0000000A。
code
00000149h-00000152h
为字节码的值;字节码的长度是以code_length
值为准。
exception_table_length
0x0000。
attribute_count
0x0001。
attribute_info
00000157h-00000158h
的值为0x000E
,它表示常量池中属性的名称,查看常量池得知第14个常量的值为LineNumberTable
,LineNumberTable
用于描述Java源代码的行号和字节码行号的对应关系,它不是运行时必需的属性。
LineNumberTable
如果通过
-g:none
的编译器参数来取消生成LineNumberTable的话,最大的影响就是异常发生的时候,堆栈中不能显示出出错的行号,调试的时候也不能按照源代码来设置断点。
attribute_name_index
表示常量池的索引。
attribute_length
表示属性长度。
line_number_table_length
表示行号属性数目。
start_pc
表示字节码的行号。
line_number
表示源代码的行号。LineNumberTable
属性的字节流如下图。
attribute_name_index
0x000E
,表示属性集的名字。
attribute_length
0x0000A
,表示属性的长度(行号长度+start_pc+line_number
的长度)。
line_number_table_length
0x0002
,行号的长度=2。
start_pc
0x0000
,0x0004
,2次行号。
line_number
0x0003
,0x0005
。
TestClass第2个方法
access_flags
0x0001
。
name_index
0x000F
。
descriptor_index
0x0010
,通过查看常量池可以知道此方法为public int instanceMethod(int param)
。
通过和上面类似的方法,instanceMethod
的Code
属性为下图。
attribute_name_index
0x000D
attribute_length
0X0000001C
max_stack
0x0002
max_locals
0x0002
code_length
0x00000004
code
0x1B0460AC
exception_table_length\exception_table
0x0000
attributes_count
0x0001
attribute_info数组
attribute_name_index
0x000E
,表示属性集的名字
attribute_length
0x000006
,表示属性的长度(行号长度+start_pc+line_number
的长度)
line_number_table_length
0x0001
start_pc
0x0000
line_number
0x0008
Class文件的属性
Class文件的属性,从00000191h-00000199h
为Class文件中的属性表,其中0x0011
表示属性的名称,查看常量池可以知道属性名称为SourceFile
,我们再来看看SourceFile
的结构如下图。
attribute_length
属性的长度。
sourcefile_index
指向常量池中值为源代码文件名称的常量,在本例中SourceFile
属性截图。0x0001
表示sourceFile
以指向常量池中CONSTANT_Utf8_info
类型常量的索引,utf8-info
是0x01。
attribute_length
0x00000002
表示长度为2个字节。
soucefile_index
值为0x0012,查看常量池的第18个常量可以知道源代码文件的名称为TestClass.java。