JAVA类文件结构(一)
Java技术能够一直保持良好的想后兼容,类文件结构是工功不可没的。类文件结构大多数都在在《Java虚拟机规范第一版》中已经定义好的。虽然发展到现在经历了十几个大版本和几个小版本,但是类文件结构细节几乎没有发生什么改变。尽管不同版本的《Java虚拟机规范》都对类结构有改进,但是都是进行扩充,原来定义好的都没有发生改变
根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数 据,这种伪结构中只有两种数据类型:“无符号数”和“表”。后面的解析都要以这两种数据类型为基础,
·无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个 字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。表是由多个无符号数或者其他表作为数据项构成的复合数据类型
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count |
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 |
mothod_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的 容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集 合”。
Class的结构不像XML等描述语言,由于它没有任何分隔符 号,所以在表6-1中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class 文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少, 先后顺序如何,全部都不允许改变
示例代码
下面说讲解的内容都是以改示例代码为基础
源码
package org.test;
public class Test{
private int value=0;
public void setValue(int value){
this.value=value;
}
public int getValue(){
return value;
}
public void autoIncrement(){
++value;
}
}
字节码
Compiled from "Test.java"
public class org.test.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // org/test/Test.value:I
#3 = Class #20 // org/test/Test
#4 = Class #21 // java/lang/Object
#5 = Utf8 value
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 setValue
#12 = Utf8 (I)V
#13 = Utf8 getValue
#14 = Utf8 ()I
#15 = Utf8 autoIncrement
#16 = Utf8 SourceFile
#17 = Utf8 Test.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // value:I
#20 = Utf8 org/test/Test
#21 = Utf8 java/lang/Object
{
public org.test.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #2 // Field value:I
9: return
LineNumberTable:
line 3: 0
line 5: 4
public void setValue(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field value:I
5: return
LineNumberTable:
line 8: 0
line 9: 5
public int getValue();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field value:I
4: ireturn
LineNumberTable:
line 12: 0
public void autoIncrement();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field value:I
5: iconst_1
6: iadd
7: putfield #2 // Field value:I
10: return
LineNumberTable:
line 16: 0
line 17: 10
}
SourceFile: "Test.java"
hex
CAFEBABE0000003400160A00040012090003001307001407001501000576616C7565010001490100063C696E69743E010003282956010004436F646501000F4C696E654E756D6265725461626C6501000873657456616C75650100042849295601000867657456616C756501000328294901000D6175746F496E6372656D656E7401000A536F7572636546696C65010009546573742E6A6176610C000700080C0005000601000D6F72672F746573742F546573740100106A6176612F6C616E672F4F626A65637400210003000400000001000200050006000000040001000700080001000900000026000200010000000A2AB700012A03B50002B100000001000A0000000A000200000003000400050001000B000C000100090000002200020002000000062A1BB50002B100000001000A0000000A000200000008000500090001000D000E000100090000001D00010001000000052AB40002AC00000001000A0000000600010000000C0001000F00080001000900000027000300010000000B2A59B400020460B50002B100000001000A0000000A000200000010000A001100010010000000020011
1. 魔数与Class文件的版本
每个Class文件的头4个字节被称为魔术,它的唯一作用是确定这个文件是否为一个被虚拟机接受的文件。除了Java还有很多文件格式标准中都是用了魔数,如Gif或者JEPG等文件头中都有魔数。java文件的魔数为0xCAFEBABE。
紧接着魔数的4个字节是Class的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)如上面的示例代码中Hex 前4个字节分别是CAFEBABE,后面紧跟着次版本号占两个字节 0x0000,再后面是主版本号0x0034,查询资料0x0034为JAVA8
2.常量池
在主次版本之后是常量池入口,此处是一个u2类型的数据,表示常量池中数据的格式。此处需要注意常量池数量是从1开始计数的也就是当这个位置是0x0001时没有表示常量池中数据数量为0。这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下 需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。上面的示例中常量池容量(偏移地址:0x00000008)是:0x0016(十进制 22),表示常量池中有21项常量
常量池中主要放两类常量:字面量(Literal),符号引用(Symbolic References)
-
字面量
接近Java语言层面的常量概念,如文本字符串,被声明为final的常量值 -
符号常量
属于编译原理方面的概念主要包含一下几种- 被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
常量池每一项常量都是一个表,最初表中有11项结构不同的表后来又扩充了4中动态语言相关的常量,为了支持java模块化系统又加入了两个常量。17类常量都有同一个特点,表的起始第一位是一个u1类型的标志位代表常量属于那种常量
常量池中17种数据类型表结构
- CONSTANT_Utf8_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用的字节数 |
byte | u2 | 长度为length的UTF-8编码的字符串 |
- CONSTANT_Integer_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为3 |
byte | u4 | 按高位在前储存的int值 |
- CONSTANT_Float_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为4 |
byte | u4 | 按高位在前储存的float值 |
- CONSTANT_Long_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为5 |
byte | u4 | 按高位在前储存的long值 |
- CONSTANT_Double_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为6 |
byte | u4 | 按高位在前储存的double值 |
- CONSTANT_Class_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为7 |
index | u2 | 指向全限定常量的索引 |
- CONSTANT_String_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为8 |
index | u2 | 指向字符串字面量的索引 |
- CONSTANT_Fieldref_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为9 |
index | u2 | 指向声明字段的类或接口描述符CONSTANT_Class_info |
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引 |
- CONSTANT_Methodref_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为10 |
index | u2 | 指向声明字段的类或接口描述符CONSTANT_Class_info |
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引 |
- CONSTANT_InterfaceMethodref_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为11 |
index | u2 | 指向声明字段的类或接口描述符CONSTANT_Class_info |
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引 |
- CONSTANT_NameAndType_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为12 |
index | u2 | 指向该字段或方法名称的索引 |
index | u2 | 指向该字段或方法描述符常量的索引 |
- CONSTANT_MethodHandle_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为15 |
reference_kind | u1 | 值必须在1-0之间(包括1,9)它决定了方法句柄类型。方法句柄类型的值表示方法句柄的字节码行为 |
reference_index | u2 | 值必须是对常量池的有效索引 |
- CONSTANT_MethodType_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为16 |
descriptor | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符 |
- CONSTANT_Dynamic_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为17 |
bootstrap_method_attr_index | u2 | 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引 |
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 |
- CONSTANT_InvokeDynamic_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为18 |
bootstrap_method_attr_index | u2 | 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引 |
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 |
- CONSTANT_Module_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为19 |
name_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示模块名称 |
- CONSTANT_Package_info
项目 | 类型 | 描述 |
---|---|---|
tag | u1 | 值为20 |
name_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示包名称 |
回过头我们在分析一下上面的示例代码
第一项
在常量池数量后的是0x0A(位置:偏移量0x000A),查找上面常量池项目类型0x0A对应的是CONSTANS_Methodref_info 它一共占用5个字节所以就是0x0A 0004 0012 查表可知0x0004表示的是类或接口描述符的索引, 0x0012 表示字段描述符 索引。查看示例代码中字节码常量池中第4项是一个Class项,第 0x0012项是一个NameAndType
第二项
第二项是0x09(位置:偏移量0x000F),查表可知0x09是CONSTANT_Fieldref_info, 一共占5个字节0x09 0003 0013。
第一个index CONSTANT_Class_info的索引是0x03,查示例代码中字节码可知常量池第三个项Class。
第二个index:CONSTANT_NameAndType_info的索引是0x0013,查看示例代码中字节码可知常量中第0x0013项是NameAndType
第三项
第三项0x07(位置:偏移量0x0014),查表可知0x07是CONSTANT_Class_info ,一共占3个字节 0x07 0014。
index: 全限定常量名常量项的索引 0014 (是一个CONSTANST_Utf8_info类型的表),查示例代码中字节码可知常量池中第0x0014项是 org/test/Test。
第四项
第四项0x07(位置:偏移量0x0017),,查表可知0x07是CONSTANT_Class_info ,一共占3个字节 0x07 0015
index: 全限定常量名常量项的索引 0014 (是一个CONSTANST_Utf8_info类型的表),查示例代码中字节码可知常量池中第0x0014项是 java/lang/Object。
第五项
第五项0x01(位置:偏移量0x001A),查表可知0x01是CONSTANT_Utf8_info,查表可知 length 占2个字节是0x0005,
所以一个bytes是一个字节一共又length(0x0005)个,第五项一共占了8个字节(u1+u2+u1*5=u8) 0x01 0005 76616C7565 。
76 61 6C 75 65转换成字符串(Assic)"value"
第六项
第六项0x01(位置:偏移量0x0022),查表可知0x01是CONSTANT_Utf8_info,查表可知 length 占2个字节是0x0001,
所以一个bytes是一个字节一共又length(0x0001)个,第五项一共占了4个字节(u1+u2+u1*1=u4) 0x01 0001 49 。
49转换成字符串(Assic)"I"
第七项
第七项0x01(位置:偏移量0x0026),查表可知0x01是CONSTANT_Utf8_info,查表可知 length 占2个字节是0x0006,
所以一个bytes是一个字节一共又length(0x0001)个,第五项一共占了9个字节(u1+u2+u1*6=u9) 0x 01 0006 3C696E69743E 。
3C 69 6E 69 74 3E转换成字符串(Assic)"< init >"
第八项
第八项0x01(位置:偏移量0x002F),查表可知0x01是CONSTANT_Utf8_info,查表可知 length 占2个字节是0x0003,
所以一个bytes是一个字节一共又length(0x0003)个,第五项一共占了5个字节(u1+u2+u1*3=u5) 0x 01 0003 282956 。
28 29 56转换成字符串(Assic)"()V"
第九项
第九项0x01(位置:偏移量0x0035),和上面差不多,一共占7字节 为0x01 0004 436F6465,bytes为"Code"
第十项
第十项0x01(位置:偏移量0x003C),和上面差不多,一共占0x12字节 为0x01 000F4 C696E654E756D6265725461626C65,bytes为Code ""LineNumberTable"
第十一项
第十一项0x01(位置:偏移量0x004E),和上面差不多,一共占0x0B字节 为0x01 0008 73657456616C7565,bytes为"setValue"
第十二项
第十二项0x01(位置:偏移量0x0059),和上面差不多,一共占0x07字节 为0x01 0004 28492956,bytes为"(I)V"
第十三项
第十三项0x01(位置:偏移量0x0060),和上面差不多,一共占0x0B字节 为0x01 0008 67657456616C7565,bytes为”getValue”
第十四项
第十四项0x01(位置:偏移量0x006b),和上面差不多,一共占0x06字节 为0x01 0003 282949,bytes为”()I”
第十五项
第十五项0x01(位置:偏移量0x0071),和上面差不多,一共占0x10字节 为0x01 000D 6175746F496E6372656D656E74,bytes为”autoIncrement"
第十六项
第十六项0x01(位置:偏移量0x0081),和上面差不多,一共占0x0D字节 为0x01 000A 536F7572636546696C65,bytes为”SourceFile"
第十七项
第十七项0x01(位置:偏移量0x008E),和上面差不多,一共占0x0C字节 为0x01 0009 546573742E6A617661,bytes为”Test.java"
第十八项
第十八项0x0C(位置:偏移量0x009A),查表可知0x0C是CONSTANT_MethodHandle_info,完整信息0x0C 0007 0008。查表可知 :
第一个index:占2个字节是0x0007,常量池的0x07项CONSTANT_Utf8_info 是 "< init >"
第二个index:占2个字节0x0008,常量池的0x08项CONSTANT_Utf8_info 是 "()V"
第十九项
第十八项0x0C(位置:偏移量0x009F),查表可
知0x0C是CONSTANT_MethodHandle_info,完整信息0x0C 0005 0006。
第一个index:占2个字节是0x0005,常量池的0x05项CONSTANT_Utf8_info 是 "value"
第二个index:占2个字节0x0006,常量池的0x06项CONSTANT_Utf8_info 是 "I"
第二十项
第十四项0x01(位置:偏移量0x00A4),和上面差不多,一共占0x10字节 为0x01 000D 6F72672F746573742F54657374,bytes为”org/test/Test”
第二十一项
第十四项0x01(位置:偏移量0x00A4),和上面差不多,一共占0x10字节 为0x01 0010 6A6176612F6C616E672F4F626A656374,bytes为”java/lang/Object”
到此位置常量池部分字节码Hex就分析完了,下一节在常量池之后的字节码
如有错误希望您指出,我将及时修改更新。
“路漫漫其修远兮,吾将上下而求索”