Java Class文件的结构(实践篇)

实例

首先有一个TestClass类,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package 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_version0x0000,从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/Objectname_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
23
0A 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表示的是类还是接口,是否为publicstaticfinal等。具体访问标示的含义之前已经说过了,下面我们就来看看TestClass的访问标示。Class的访问标示是从00000100d-0000010e,其值为0x0021,根据前面说的各种访问标示的标志位,我们可以知道0x0021=0x0001|0x0020,也即ACC_PUBLICACC_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

表示字段的访问标识,比如publicprivateprotectedstaticfinal等,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[] sourceint offset,int count,char[] targetint tOffsetint tCountint 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_STATICACC_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_PRIVATE0x000A表示字段的名称,它指向常量池中的第10个常量,查看常量池可以知道字段名称为instanceVar,而0x0007表示字段的描述符,它指向常量池中的第7个常量,查看常量池可以知道第7个常量为I,表示类型为instanceVar的类型为I,最后0x0000表示属性表的数量为0。

methods_count、method_info

方法表和字段表field_info虽然都有属性数量和属性表,但是它们里面所包含的属性是不同。
methods_count表示方法的数量,而method_info表示的方法表,其中方法表的结构如下图。

可以看出method_infofield_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_flagname_indexdescriptor_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_indexattribute_length的6个字节的长度)。

max_stack

表示最大栈深度,虚拟机在运行时根据这个值来分配栈帧中操作数的深度。

max_locals

代表了局部变量表的存储空间,它的单位为slotslot是虚拟机为局部变量分配内存的最小单元,在运行时,对于不超过32位类型的数据类型,比如bytecharint等占用1个slot,而doubleLong这种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个常量的值为LineNumberTableLineNumberTable用于描述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

0x00000x0004,2次行号。

line_number

0x00030x0005

TestClass第2个方法

access_flags

0x0001

name_index

0x000F

descriptor_index

0x0010,通过查看常量池可以知道此方法为public int instanceMethod(int param)
通过和上面类似的方法,instanceMethodCode属性为下图。

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。

# Java

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×