在正式开始之前,先说下虚拟机提供的语言无关性
Java虚拟机只会解析字节码文件,至于上面到底采用的什么高级语言,他不会去关心。下面我们来看看class字节码文件的结构。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在class文件之中,中间没有任何分隔符。当遇到占用8位字节以上空间的数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。那么class文件的存储结构如何?你怎么区分一个数据项占用了几个字节呢?class文件采用一种类似于c语言结构体的伪结构来存储,这种伪结构只有两种数据类型:无符号数与表,表是由多个无符号数或其他表作为数据项构成的复合数据项,整个class文件本质上就是一张表,class文件格式如下:
类型 |
名称 |
数量 |
U4 |
Magic |
1 |
U2 |
Minor_version |
1 |
U2 |
Major_version |
1 |
U2 |
Constant_pool_count |
1 |
Cp_info |
Constant_pool |
Constant_pool_count-1 |
U2 |
Access_flags |
1 |
U2 |
This_class |
1 |
U2 |
Super_class |
1 |
U2 |
Interfaces_count |
1 |
U2 |
Interfaces |
Interfaces_count |
U2 |
Fields_count |
1 |
Field_info |
Fields |
Fields_count |
U2 |
Methods_count |
1 |
Method_info |
Methods |
Methods_count |
U2 |
Attributes_count |
1 |
Attribute_info |
attributes |
Attributes_count |
注:u1、u2、u4、u8代表1个字节、2个字节、4个字节和8个字节,以“_info”结尾表示是表。
1.魔数Magic
头4个字节,用于确定这个文件是否为一个能被虚拟机接受的class文件,该值为0xCAFEBABE,就能被虚拟机接受,否则不可以
2.次版本号Minor_version与主版本号Major_version
Java的版本号是从45开始的,JDK1.1能支持版本号为45.0~45.65535,JDK1.2能支持45.0~56.65535.下表列举了从JDK1.1到1.7之间,版本号的支持。
3.常量池
Constant_pool_count代表常量池容量计数值,如果Constant_pool_count=21,那么常量池 Constant_pool中就有20项常量,0项空出来。常量池中存放两大类常量:字面量和符号引用,字面量指文本字符串或final的常量值,符号引用指类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。常量池中的每一个常量都是一个表,共有11种结构各不相同的表结构,如下:
这11种常量类型均有自己的结构,如下:
(1)CONSTANT_Class_info
类型 |
名称 |
数量 |
U1 |
Tag |
1 |
U2 |
name_index |
1 |
Tag是标志位,name_index是一个索引值,指向常量池中一个CONSTANT_utf8_info类型的常量,指向的常量代表了类或接口的全限定名。
(2)CONSTANT_Utf8_info
类型 |
名称 |
数量 |
U1 |
Tag |
1 |
U2 |
Length |
1 |
U1 |
Bytes |
Length |
Length表示这个utf-8编码的字符串长度是多少字节,后面紧跟着的长度为Length字节的连续数据是一个使用utf-8编码的字符串。U2能表达的最大值是65535,因此java程序中如果定义超过64kB的英文字符的变量或方法名,将会无法编译。
(3)CONSTANT_Integer_info
类型 |
名称 |
数量 |
U1 |
Tag |
1 |
U4 |
Bytes |
1 |
Bytes表示按照高位在前存储的int值
CONSTANT_Float_info、CONSTANT_Long_info、CONSTANT_Double_info与CONSTANT_Integer_info类似。
4.访问标志 Access_flags
5.类索引this_class、父类索引super_class与接口索引Interfaces
Class文件中由这三项数据来确定这个类的继承关系,类索引用于确定这个类的全限定名、父类索引用于确定这个类的父类的全限定名。类索引与父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info的name_index找到定义在CONSTANT_utf8_info类型的常量中的全限定名字符串。
6.字段表集合Fields_info
字段表集合Fields_info 用于描述接口或类中声明的变量,字段Fields包括类级变量或实例级变量,不包括在方法内部声明的变量。字段表结构如下:
字段修饰符放在access_flags项目中,与类中的access_flag项目十分类似。Name_index、descriptor_index,都是对常量池的引用,代表字段的简单名称及字段和方法的描述符。描述符的作用是用来描述字段的数据类型、方法的参数列表和返回值。如代码
Private Int m;
Public voidmin();
其中m字段与inc()方法的简单名称分别是m与inc,描述符为I、()V。
7.方法表集合Method_info
结构如下
与字段表集合类似。方法里的代码经过编译器编译成字节码指令之后,存放在方法属性表集合中一个名为code的属性里面。
8.属性表集合Attribute_info
在class文件、字段表、方法表都可以携带自己的属性表集合。Java虚拟机规范预定义了9项虚拟机实现应当能识别的属性。具体如下
对于每一个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,属性值的结构完全自定义,只需要说明属性值所占用的位数长度即可,符合规则的属性表结构如下:
(1)Code属性
Code属性出现在方法表的属性集合中,接口或抽象类中的方法不存在code属性。Code属性表结构如下
类型 |
名称 |
数量 |
说明 |
U2 |
Attribute_name_index |
1 |
指向CONSTANT_Utf8_info型常量的索引,值为code |
U4 |
Attribute_length |
1 |
属性值长度 |
U2 |
Max_stack |
1 |
操作数栈最大深度 |
U2 |
Max_locals |
1 |
局部变量所需的存储空间 |
U4 |
Code_length |
1 |
字节码长度 |
U1 |
Code |
Code_length |
存储字节码 |
U2 |
Exception_table_length |
1 |
异常表长度 |
Exception_info |
Exception_table |
Exception_table_length |
异常表,实现java异常及finally处理机制 |
U2 |
Attributes_count |
1 |
|
Attribute_info |
attributes |
Attributes_count |
(2)Exceptions属性
列举出方法中可能抛出的异常,即方法描述时在throws关键字后面列举的异常,结构如下
类型 |
名称 |
数量 |
说明 |
U2 |
Attribute_name_index |
1 |
|
U4 |
Attribute_length |
1 |
|
U2 |
Number_of_exceptions |
1 |
异常数目 |
U2 |
Exception_index_table |
Number_of_exceptions |
(3)LineNumberTable属性
描述java源码行号与字节码行号之间的对应关系,结构如下
类型 |
名称 |
数量 |
说明 |
U2 |
Attribute_name_index |
1 |
|
U4 |
Attribute_length |
1 |
|
U2 |
Line_number_table_length |
1 |
|
Line_number_info |
Line_number_table |
Line_number_table_length |
包括start_pc和line_number,前者是字节码行号,后者是java源码行号 |
(4)LocalVariableTable属性
描述栈帧中局部变量表中的变量与java源代码中定义的变量之间的关系。
(5)SourceFile
记录生成class文件的源码文件名称。
(6)ConstantValue属性
作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才可以使用这项属性。程序中
Int x=123;与static intx=123;虚拟机对其赋值方式和时刻都不一样。
(7)InnerClasses属性
记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它及它包含的内部类生成InnerClasses属性。
(8)Deprecated及Synthetic属性
Deprecated标志某个类、字段、方法已经被程序标志不再使用,在代码中使用@ deprecated注释进行设置。
Synthetic代表此字段或方法不是由Java源代码直接产生的,而是由编译器自行添加的。
9.举例实战
代码实例
Package org.fenixsoft.clazz Public class TestClass{ Private int m; Public int inc(){ Return m+1; } } |
使用WinHex打开class文件如下
魔数与版本号
前四个字节为魔数,紧接着的四个字节是版本号,0x00000033,转为十进制为51,说明该版本时可以被JDK1.7的虚拟机执行的class文件。
常量池
紧接着两个字节0x0016,转换成十进制为22,代表常量池中有21个常量,索引值为1~21。常量池中共11中常量类型,但每一种类型的第一个字节都是一个标志位字节。
第一个常量,标志位字节为0x07,十进制为7,表示CONSTANT_Class_info,通过前面的讲解,知道CONSTANT_Class_info类型的常量,后面是一个2字节的name_index,指向常量池中的CONSTANT_Utf8_info,2个字节为0x00002,指向常量池中第二项常量,
第二项常量的的标志位字节为0x01,表示是一个CONSTANT_Utf8_info类型的常量,后面两个字节代表长度,0x001D,换成十进制为29,即后面有29个字节的连续字符串。向后数29个,分别为
一个字节的Utf-8编码,相当于1~127的ASCII码,0x6F换成十进制为111,查ASCII码表字符为o,类似,往后推算,29个连续字符为org/fenixsoft/clazz/TestClass。
第三项常量的标志位为0x07,表示CONSTANT_Class_info,后面是一个2字节的name_index,值为0x0004,指向常量池中第四项常量。
第四项常量的标志位为01,表示是一个CONSTANT_Utf8_info类型的常量,后面两个字节代表0x0010,换成十进制为29,即后面有16个字节的连续字符串。向后数16个,分别为
为java/lang/Object。
第五项常量的标志为01,表示是一个CONSTANT_Utf8_info类型的常量,后面两个字节代表0x0001,换成十进制为1,即后面有1个字节的连续字符串。代表m
第六项常量的标志为01,表示是一个CONSTANT_Utf8_info类型的常量,后面两个字节代表0x0001,换成十进制为1,即后面有1个字节的连续字符串。代表I。
第七项常量的标志为01,表示是一个CONSTANT_Utf8_info类型的常量,后面两个字节代表0x0006,换成十进制为6,即后面有6个字节的连续字符串。代表<init>
第八项为()V
第九项为Code
第十项常量的标志位为0x0A,表示CONSTANT_Methodref_info,后面是一个2字节的index,值为0x0003,指向声明方法的类描述符CONSTANT_Class_info的索引项。接下来的2字节index为0x000B,指向名称及类型描述符CONSTANT_NameAndType_info的索引项。即为java/lang/Object.”<init>”.()V
第十一项常量的标志位为0x0C,表示CONSTANT_NameAndType,后面是一个2字节的index,值为0x0007,指向该字段或方法名称常量项的索引。接下来的2字节index为0x0008,指向该字段或方法描述符常量项的索引。即为”<init>”.()V.
一次类推,
第十二项为LineNumberTable
第十三项为LocalVariableTable
第十四项为this
第十五项为Lorg/fenixsoft/clazz/TestClass;
第十六项为inc
第十七项为()I
第十八项为org/fenixsoft/clazz/TestClass.m:I
第十九项为m:I
第二十项为SourceFile
第二十一项为TestClass.java
访问标志
常量池后的两个字节,值为0x0021,为ACC_PUBLIC与ACC_SUPER
类索引、父类索引、接口索引
值分别为0x0001、0x0003、0x0000,表示类索引为1,父类索引为3,接口索引集合大小为0.常量池中,第一项的值为org/fenixsoft/clazz/TestClass,第三项的值为java/lang/Object,即类为org/fenixsoft/clazz/TestClass,父类为java/lang/Object,没有接口
字段
接下来的两个字节0x0001,代表字段的数量为1,根据字段表的结构,后面的几项分别为
访问标志0x0002,表示ACC_PRIVATE
Name_index:0x0005,表示m
Descriptor_index:0x0006,表示I,即int
Attributes_count:0x0000,属性个数为0
即一个字段,为privateint m
方法
接下来的两个字节0x0002,代表方法的数量为2,根据方法表的结构,后面的几项分别为
第一个方法
访问标志0x0001,表示ACC_PUBLIC
Name_index:0x0007,表示<init>
Descriptor_index:0x0008,表示()V,即void()
Attributes_count:0x0001,属性个数为1
根据属性结构
Attribute_name_index:0x0009,为Code
Attribute_length:0x0000,长度为0
第二个方法是父类是父类方法,如果在子类中没有被重写,方法表集合中就不会出现父类方法的信息,因此,看不到第二个方法<clinit>