1 概述
- Java很重要的特点是平台无关性,即用Java语言编写的程序可以在不同平台之间无缝迁移,在Java诞生之初,有一个著名的宣传口号:“一次编写,到处运行(Write Once,Run AnyWhere)”。Java能够实现平台无关性的原因是它在平台之上提供了一个Java运行环境,也就是JVM,各种不同平台上的虚拟机都统一使用的程序存储结构——字节码,是构成平台无关系的基石。
2 Class 类文件的结构
- Class文件是一组以8位字节位基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没人任何分隔符,整个Class文件存储的内容几乎是全部程序运行的必要数据,没有空隙存在。
- 当遇到8个字节以上的空间数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。
- Class文件只有两种数据类型,无符号数和表。
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,如下表所示的数据项构成。
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u4 | magic | 1 | 魔数,表明当前文件是.class文件,固定0xCAFEBABE |
u2 | minor_version | 1 | Class文件的次版本号 |
u2 | major_version | 1 | Class文件主版本号 |
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 | 各种属性 |
Class文件解析:
将以下代码使用jdk1.8进行编译,得到class文件,通过 Binary Vivewer 软件打开进行查看解析,或者用其它二进制软件打开文件。
// 测试代码1:
public class TestClass1{
private int m;
public int inc(){
return m +1;
}
}
打开结果:
2.1 魔数
u4 magic
- Class文件的头4个字节称为魔数,唯一作用就是让虚拟机接受这个 Class 文件,Class 的魔数为:0xCAFEBABY
2.2 文件版本
u2 minor_version //次版本号
u2 major_version //主版本号
次版本号占2个字节:0x0000
主版本号占4个字节:0x0034(十六进制) = 52(十进制)
jdk 对应主版本号:
JDK 1.8 = 52
JDK 1.7 = 51
JDK 1.6 = 50
JDK 1.5 = 49
JDK 1.4 = 48
JDK 1.3 = 47
JDK 1.2 = 46
JDK 1.1 = 45
2.3 常量池
- 常量池可以理解为 Class 文件之中的资源仓库,是 Class 文件结构中与其它相关联最多的数据类型,也是占用 Class 文件空间最大的数据项之一。
//表类型数据项
u2 constant_pool_count
cp_info constant_pool[constant_pool_count-1]
constant_pool_count:常量池的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值 (计数从1开始)
常量池数量:0x0013(十六进制) = 19(十进制),说明有18个常量
常量池内容:存放两大常量:字面量和符号引用
- 字面量:文本字符串、生命的final常量等
- 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
//常量池的表结构
cp_info{
u1 tag;
u1 info[];
}
tag 标志位取值:
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MothodType_info | 16 | 标志方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
常量池中的14种常量项的结构总表:
具体表结构描述如下:跳过描述
- CONSTANT_Utf8_info
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
- tag:CONSTANT_Utf8(1)。
- length:length项的值指明了bytes[]数组的长度(注意,不能等同于当前结构所表示的String对象的长度),CONSTANT_Utf8_info结构中的内容是以length属性确定长度而不是以null作为字符串的终结符。
- bytes[]:bytes[]是表示字符串值的byte数组,bytes[]数组中每个成员的byte值都不会是0,也不在0xf0至0xff范围内。
- CONSTANT_Integer_info 和 CONSTANT_Float_info
// 表示4字节(int和float)的数值常量
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
- tag:
CONSTANT_Integer_info结构的tag项的值是CONSTANT_Integer(3)。
CONSTANT_Float_info结构的tag项的值是CONSTANT_Float(4)。 - bytes:
CONSTANT_Integer_info结构的bytes项表示int常量的值,按照Big-Endian的顺序存储。
CONSTANT_Float_info结构的bytes项按照IEEE 754单精度浮点格式表示float常量的值,按照Big-Endian的顺序存储。
- CONSTANT_Long_info 和 CONSTANT_Double_info
// 表示8字节(long和double)的数值常量
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
- tag:
CONSTANT_Long_info结构的tag项的值是CONSTANT_Long(5)。
CONSTANT_Double_info结构的tag项的值是CONSTANT_Double(6)。 - high_bytes和low_bytes
CONSTANT_Long_info结构中的无符号的high_bytes和low_bytes项用于共同表示long型常量,构造形式为((long) high_bytes << 32) + low_bytes,high_bytes和low_bytes都按照Big-Endian顺序存储。
CONSTANT_Double_info结构中的high_bytes和low_bytes共同按照IEEE 754双精度浮点格式表示double常量的值。high_bytes和low_bytes都按照Big-Endian顺序存储。
- CONSTANT_Class_info
// 用于表示类或接口
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
- tag:CONSTANT_Class_info结构的tag项的值为CONSTANT_Class(7)
- name_index:name_index项的值,必须是对常量池的一个有效索引。常量池在该索引处的项必须是CONSTANT_Utf8_info结构,代表一个有效的类或接口二进制名称的内部形式。
- CONSTANT_String_info
// 用于表示java.lang.String类型的常量对象
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
- tag:CONSTANT_String_info结构的tag项的值为CONSTANT_String(8)。
- string_index:string_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info 结构,表示一组Unicode码点序列,这组Unicode码点序列最终会被初始化为一个String对象。
- CONSTANT_Fieldref_info,CONSTANT_Methodref_info 和 CONSTANT_InterfaceMethodref_info
// 字段,方法,接口方法具有相同的类型
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
- tag:
CONSTANT_Fieldref_info结构的tag项的值为CONSTANT_Fieldref(9)。
CONSTANT_Methodref_info结构的tag项的值为CONSTANT_Methodref(10)。
CONSTANT_InterfaceMethodref_info结构的tag项的值为CONSTANT_InterfaceMethodref(11)。 - class_index:
class_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Class_info结构,表示一个类或接口,当前字段或方法是这个类或接口的成员。
CONSTANT_Methodref_info结构的class_index项的类型必须是类(不能是接口)
CONSTANT_InterfaceMethodref_info结构的class_index项的类型必须是接口(不能是类)。
CONSTANT_Fieldref_info结构的class_index项的类型既可以是类也可以是接口。 - name_and_type_index:
name_and_type_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,它表示当前字段或方法的名字和描述符。
CONSTANT_Fieldref_info结构中,描述符必须是字段描述符。CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info中描述符必须是方法描述符。
- CONSTANT_NameAndType_info
// 用于表示字段或方法
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
- tag:CONSTANT_NameAndType(12)。
- name_index:必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,这个结构要么表示特殊的方法名,要么表示一个有效的字段或方法的非限定名(Unqualified Name)。
- descriptor_index:必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,这个结构表示一个有效的字段描述符或方法描述符。
- CONSTANT_MethodHandle_info
// 用于表示方法句柄
CONSTANT_MethodHandle_info {
u1 tag;
u1 reference_kind;
u2 reference_index;
}
- tag:CONSTANT_MethodHandle(15)。
- reference_kind:必须在1至9之间(包括1和9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为(Bytecode Behavior)。
- reference_index:
reference_index项的值必须是对常量池的有效索引:
(1)如果reference_kind项的值为1(REF_getField)、2(REF_getStatic)、3(REF_putField)或4(REF_putStatic),那么常量池在reference_index索引处的项必须是CONSTANT_Fieldref_info结构,表示由一个字段创建的方法句柄。
(2)如果reference_kind项的值是5(REF_invokeVirtual)、6(REF_invokeStatic)、7(REF_invokeSpecial)或8(REF_newInvokeSpecial),那么常量池在reference_index索引处的项必须是CONSTANT_Methodref_info结构,表示由类的方法或构造函数创建的方法句柄。
(3)如果reference_kind项的值是9(REF_invokeInterface),那么常量池在reference_index索引处的项必须是CONSTANT_InterfaceMethodref_info结构,表示由接口方法创建的方法句柄。
(4)如果reference_kind项的值是5(REF_invokeVirtual)、6(REF_invokeStatic)、7(REF_invokeSpecial)或9(REF_invokeInterface),那么方法句柄对应的方法不能为实例初始化()方法或类初始化方法()。
(5)如果reference_kind项的值是8(REF_newInvokeSpecial),那么方法句柄对应的方法必须为实例初始化()方法。
- CONSTANT_MethodType_info
// 用于表示方法类型
CONSTANT_MethodType_info {
u1 tag;
u2 descriptor_index;
}
- tag:CONSTANT_MethodType(16)。
- descriptor_index:必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符。
- CONSTANT_InvokeDynamic_info
// 表示invokedynamic指令所使用到的引导方法(Bootstrap Method)、引导方法使用到动态调用名称(Dynamic Invocation Name)、参数和请求返回类型、以及可以选择性的附加被称为静态参数(Static Arguments)的常量序列。
CONSTANT_InvokeDynamic_info {
u1 tag;
u2 bootstrap_method_attr_index;
u2 name_and_type_index;
}
- tag:ONSTANT_InvokeDynamic(18)。
- bootstrap_method_attr_index:对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引。
- name_and_type_index:对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info 结构,表示方法名和方法描述符。
了解了以上tag取值及表结构描述,我们继续解析。
常量池数量后跟的为具体的常量池信息,
第一位字节为tag,0x0A(十进制为10),根据 tag 取值表查到了
第一个常量表 CONSTANT_Methodref_info,对应两个索引,各占两个字节0x0004 #4,0x000F #15,表示对应了具体位置在第4和第15个常量表位置。
再接着是一个字节的tag,0x0009对应9,找到
第二个常量表 CONSTANT_Fieldref_info,对应两个索引,各占两个字节 0x0003 #3,0x0010 #16,表示对应了具体位置在第3和第16个常量表位置。
再接着是一个字节的tag,0x0007对应7,找到
第三个常量表 CONSTANT_Class_info,对应了一个索引,占了两个字节 0x0011 #17,表示对应了具体位置在第17个常量表位置。
再接着是一个字节的tag,0x0007对应7,找到
第四个常量表 CONSTANT_Class_info,对应了一个索引,占了两个字节 0x0012 #18,表示对应了具体位置在第18个常量表位置。
再接着是一个字节的tag,0x0001对应1,找到
第五个常量表 CONSTANT_Utf8_info,表示一个UTF-8的字符串,两个字节表示字符串的
长度, 0x0001 表示字符串长度为1,后面跟着的 1 个字节为字符串内容,0x006D 109表示 ASCII 的m。
不断的向下找,一共有 18 个这样的结构。这里借助 Java 提供的命令帮助我们读取:
javap -verbose class文件
可以看到,我们刚刚读取到的五个都在命令行中显示了,这里根据命令行也得到了总共有18个常量,分别对应的常量表的位置,最后一个常量就是 #18 在对应的 java/lang/Object 结束,对应了字节码中 t 结尾,到这里我们的字节码中常量池部分就解析完成啦。
2.4 访问标志
u2 access_flags
- 访问标志为两个字节,识别类或者接口层次的访问信息,包括:Class文件时类还是接口;是否定义为 public 类型;如果是类,是否声明为 final 等。具体如下表:
标志名称 | 标志值 | 含义 | 针对的对像 |
---|---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 | 所有类型 |
ACC_FINAL | 0x0010 | 是否为声明为final | 类 |
ACC_SUPER | 0x0020 | 使用新的invokespecial语义 | 类和接口 |
ACC_INTERFACE | 0x0200 | 标识是一个接口类型 | 接口 |
ACC_ABSTRACT | 0x0400 | 是否为抽象类型 | 接口或抽象类,此标识符为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 该类不由用户代码生成 | 所有类型 |
ACC_ANNOTATION | 0x2000 | 注解类型 | 注解 |
ACC_ENUM | 0x4000 | 枚举类型 | 枚举 |
access_flags 的值为上表中或关系的集合。
根据访问标志的定义,可以找到下两个字节为 0x0021,根据表中比对 0x0021 是 0x0001 | 0x0020,因此可以读出是 public class类。这里我更改一次测试代码,在练习一次:
// 测试代码2:
public interface TestClass2{
public int inc();
}
这里 0x0601,就是 0x0001 | 0x0200 | 0x0400 ,及 publlic interface abstract 的标识。
2.5 类索引、父类索引与接口索引集合
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
- 类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。
- 类索引:确定这个类的全限定名,
- 父类索引:确定这个类的父类的全限定名,由于Java语言不允许多重继承,所以父类索引,除了 java.lang.Object 之外,所有的Java类至少有一个父类。
- 接口索引集合:确定类实现了哪些接口。
类索引和父类索引均由 u2 类型的索引值表示,各自指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过 CONSTANT_Class_info 类型的常量中的索引值可以确定定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。
接口索引集合,因为引用接口数量不一定,第一项为 u2 类型的接口计数器,表示索引表的容量,如果没有实现接口,则计数器为0,后面不在占用任何字节。
可以读到 TestClass1 中类索引为 0x0003 #3, 0x0004 #4,0x0000 即没有接口,数量为0。
利用 Javap 也可以查看到,#3 代表常量池中 TestClass1 类的全限名,#4 为父类 java.lang.Object。
此时,我们对 TestClass1.class 的类索引、父类索引与接口索引集合已经解析完成了。
2.6 字段表集合
- 字段表用于描述接口或类中生命的变量。字段包括类级变量和实例级变量,不包括方法内部声明的局部变量。
u2 fields_count; //字段的数量
field_info { //字段表
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
field_info 具体结构如下:
- access_flags:访问标识符
标志 | 值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | public 访问修饰符 |
ACC_PRIVATE | 0x0002 | private 访问修饰符 |
ACC_PROTECTED | 0x0004 | protected 访问修饰符 |
ACC_STATIC | 0x0008 | static |
ACC_FINAL | 0x0010 | final |
ACC_VOLATILE | 0x0040 | valatile |
ACC_TRANSIENT | 0x0080 | transient |
ACC_SYNTHETIC | 0x1000 | 编译器自动产生 |
ACC_ENUM | 0x4000 | enum |
注:实际情况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED 三个标识符最多只有一项,ACC_FINAL、ACC_VOLATILE 不能同时选择,接口中字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL 标志。
- name_index:引用的常量池,描述字段的简单名称。简单名称是指没有类型或参数修饰的方法或者字段名称。
- descriptor_index:引用的常量池,描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写V字符来表示,而对象类型则用字符L加对象的全限定名来表示。如下表:
标识字符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型,如Ljava/lang/Object; |
[ | 数组类型,多个维度则有多个[ |
用描述符来描述方法时,按照先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组 “()” 之内。如方法 void inc() 的描述符为 “()V”,方法 java.lang.String.toString() 的描述符为 “()Ljava/lang/String;”。
接下来看我们继续解析 TestClass1.class 的字段 m 。如下图所示:
注:字段表集合中不会列出从超类或者父类接口继承而来的字段;Java 中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,字段名称都必须不一样,但对于字节码来说,如果两个字段的描述符不一致,那字段重名就是合法的。
2.6 方法表集合
u2 methods_count; //方法的数量
method_info { //方法表
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
method_info 具体结构如下:
- access_flags:访问标识符
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为Public类型 |
ACC_PRIVATE | 0x0002 | 是否为private类型 |
ACC_PROTECTED | 0x0004 | 是否为PROTECTED类型 |
ACC_STATIC | 0x0008 | static |
ACC_FINAL | 0x00 10 | 是否被声明为final,只有类可以设置 |
ACC_SYNCHRONIZED | 0x0020 | synchronized |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_BRIDGE | 0x0040 | 方法是否由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NITIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICTFP | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生 |
接下来继续分析 TestClass1.class 文件,如下图:
这里可以看到,我们分析得到一个 public void init() 的方法。接下来在学习了属性表集合后,在解析后面的内容。
2.7 属性表集合
- 在 Class 文件、字段表、方法表都可以携带自己的属性表集合,用于描述专有信息。
u2 attributes_count; //属性的数量
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
虚拟机规范预定义的属性:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量池 |
Deprecated | 类,方法,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部便狼描述 |
StackMapTable | Code属性 | 供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 |
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_utf8_info类型的常量类表示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性区说明属性值所占用的位数即可。
属性表定义的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u1 | info | attribute_length |
- Code 属性
- Java程序方法体中的代码经过 Javac 编译器处理后,最终变为字节码指令存储在 Code 属性内。Code 属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在 Code 属性。
Code属性表结构:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u2 | attribute_name_index | 1 | attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为Code,它代表了该属性的属性名称 |
u4 | attribute_length | 1 | attribute_length指示了属性值的长度 |
u2 | max_stack | 1 | max_stack代表了操作数栈(Operand Stacks)深度的最大值。虚拟机运行的时候需要根据这个值来分配栈帧(StackFrame)中的操作栈深度。 |
u2 | max_locals | 1 | max_locals代表了局部变量表所需的存储空间 |
u4 | code_length | 1 | code_length代表字节码长度;理论上一个方法的字节码不超过u4,但实际是u2,如果超过这个限制,javac会拒绝 |
u1 | code | code_length | code是用于存储字节码指令的一系列字节流 |
u2 | exception_table_length | 1 | 异常表长度 |
exception_info | exception_table | exception_table_length | |
u2 | attributes_count | 1 | 属性数量 |
attribute_info | attributes | attributes_count |
了解了Code属性,我们继续完善上一节未读的 init 方法属性,
注意: code 的内容可以看到 2A B7 00 01 B1对应相应的指令。
- 读入2a,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
- 读入b7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。
- 读入00 01,这是invokespecial的参数,查常量池得0x0001对应的常量为实例构造器<init>方法的符号引用。
- 读入b1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。
到紧接着后面为 0x000A #10 指向的为 LineNumberTable 表,这里介绍两个概念。
- LineNumberTable:用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
- LocalVariableTable:属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}
至此,剩余的字节信息可以逐步分析,直至解析完成。最后,我们用 javap -verbose 直接打印出详细信息。
可以看到,总共有两个方法,init() 和 inc(),其中 init() 方法,stack=1,locals=1,args_size=1,代表栈中元素有1个,局部变量表中元素有1个,参数有1个。
init() 是无参构造方法,inc() 也是无参方法为什么args_size=1?
在任何实例方法里面,都可以通过this关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,如果 inc() 声明为static,那args_size就不会等于1而是等于0了。