Class文件笔记
一、class文件结构
class文件用文本编辑器打开:
cafe babe 0000 0034 0014 0700 1207 0013
0100 1a44 4546 4155 4c54 5f41 4e49 4d41
5449 4f4e 5f44 5552 4154 494f 4e01 0001
4a01 000d 436f 6e73 7461 6e74 5661 6c75
6505 0000 0000 0000 01f4 0100 0e73 7461
7274 416e 696d 6174 696f 6e01 0004 284a
2956 0100 0f63 616e 6365 6c41 6e69 6d61
7469 6f6e 0100 0328 2956 0100 1269 7341
6e69 6d61 7469 6f6e 5374 6172 7465 6401
0003 2829 5a01 0019 7365 7443 6861 7274
416e 696d 6174 696f 6e4c 6973 7465 6e65
7201 003b 284c 6c65 6368 6f2f 6c69 622f
6865 6c6c 6f63 6861 7274 732f 616e 696d
6174 696f 6e2f 4368 6172 7441 6e69 6d61
7469 6f6e 4c69 7374 656e 6572 3b29 5601
000a 536f 7572 6365 4669 6c65 0100 1643
6861 7274 4461 7461 416e 696d 6174 6f72
2e6a 6176 6101 0031 6c65 6368 6f2f 6c69
622f 6865 6c6c 6f63 6861 7274 732f 616e
696d 6174 696f 6e2f 4368 6172 7444 6174
6141 6e69 6d61 746f 7201 0010 6a61 7661
2f6c 616e 672f 4f62 6a65 6374 0601 0001
0002 0000 0001 0019 0003 0004 0001 0005
0000 0002 0006 0004 0401 0008 0009 0000
0401 000a 000b 0000 0401 000c 000d 0000
0401 000e 000f 0000 0001 0010 0000 0002
0011
字节码文件是由 十六进制值组成 的,对于 JVM 来说,在读取数据的时候,它会 以两个十六进制值为一组,即 一个字节 进行读取。
根据 JVM 规范的规定,Class 文件格式采用了一种类似于 C 语言结构体的伪结构来存 储数据,而这种伪结构中有且只有两种数据类型:无符号数和表。
1、无符号数
无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来 描述数字、索引引用、数量值或者按照UTF-8 码构成字符串值
2、表
u4:表示能够保存4个字节的无符号整数,u2同理。
ClassFile {
u4 magic; // 魔法数字,表明当前文件是.class文件,固定0xCAFEBABE(咖啡宝贝)
u2 minor_version; // 分别为Class文件的副版本和主版本
u2 major_version;
u2 constant_pool_count; // 常量池计数
cp_info constant_pool[constant_pool_count-1]; // 常量池内容
u2 access_flags; // 类访问标识
u2 this_class; // 当前类
u2 super_class; // 父类
u2 interfaces_count; // 实现的接口数
u2 interfaces[interfaces_count]; // 实现接口信息
u2 fields_count; // 字段数量
field_info fields[fields_count]; // 包含的字段信息
u2 methods_count; // 方法数量
method_info methods[methods_count]; // 包含的方法信息
u2 attributes_count; // 属性数量
attribute_info attributes[attributes_count]; // 各种属性
}
- magic:每个 Class 文件的头 4 个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机所接受的 Class 文件。很多文件存储标准中都使用魔数来进行身份识别, 譬如图片格式,如 gif 或者 jpeg 等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。并且,Class 文件的魔数获得很有 “浪漫气息”,值为:0xCAFEBABE(咖啡宝贝)。
- minor_version:2 个字节长,表示当前 Class 文件的次版号。
- major_version:2 个字节长,表示当前 Class 文件的主版本号。(Java 的版本号是从 45 开始 的,JDK 1.1 之后的每个 JDK 大版本发布会在主版本号向上加 1(JDK 1.0~1.1 使用了 45.0~45.3 的版本号),例如 JDK 1.8 就是 52.0)。需要注意的是,虚拟机会拒绝执行超过其版本号的 Class 文件。
- constant_pool_count:常量池数组元素个数。
- constant_pool:常量池,是一个存储了 cp_info 信息的数组,每一个 Class 文件都有一个与之对应的常量池。(注意:cp_info 数组的索引从 1 开始)
- access_flags:表示当前类的访问权限,例如:public、private。
- this_class 和 super_class:存储了指向常量池数组元素的索引,this_class 中索引指向的内容为当前类名,而 super_class 中索引则指向其父类类名。
- interfaces_count 和 interfaces:同上,它们存储的也只是指向常量池数组元素的索引。其内容分别表示当前类实现了多少个接口和对应的接口类类名。
- fields_count 和 fields:表示成员变量的数量和其信息,信息由 field_info 结构体表示。
- methods_count 和 methods:表示成员函数的数量和它们的信息,信息由 method_info 结构体表示。
- attributes_count 和 attributes:表示当前类的属性信息,每一个属性都有一个与之对应的 attribute_info 结构。常见的属性信息如调试信息,它需要记录某句代码对应源代码的哪一行,此外,如函数对应的 JVM 字节码、注解信息也是属性信息。
二、常量池
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
常量项 Utf8
常量项 Utf8 的数据结构如下所示:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
- tag:值为 1,表示是 CONSTANT_Utf8_info 类型表。
- length:length 表示 bytes 的长度,比如 length = 10,则表示接下来的数据是 10 个连续的 u1 类型数据。
- bytes:u1 类型数组,保存有真正的常量数据。
常量项 Class、Filed、Method、Interface、String
CONSATNT_Class_info {
u1 tag;
u2 name_index;
}
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_MethodType_info {
u1 tag;
u2 descriptor_index;
}
CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
CONSATNT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index
}
name_index:指向常量池中索引为 name_index 的常量表。比如 name_index = 6,表明它指向常量池中第 6 个常量。
class_index:指向当前方法、字段等的所属类的引用。
name_and_type_index:指向当前方法、字段等的名字和类型的引用。
name_index:指向某字段或方法等的名称字符串的引用。
descriptor_index:指向某字段或方法等的类型字符串的引用。
注意:CONSTANT_String 和 CONSTANT_Utf8 的区别
CONSTANT_Utf8:真正存储了字符串的内容,其对应的数据结构中有一个字节数组,字符串便酝酿其中。
CONSTANT_String:本身不包含字符串的内容,但其具有一个指向 CONSTANT_Utf8 常量项的索引。
常量项 Integer、Long、Float、Double
CONSATNT_Integer_info {
u1 tag;
u4 bytes;
}
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
CONSTANT_Float_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
三、信息描述规则
1、数据类型
-
原始数据类型:
Java 类型的 byte、char、double、float、int、long、short、boolean => "B"、"C"、"D"、"F"、"I"、"J"、"S"、"Z"。 -
引用数据类型:
ClassName => L + 全路径类名(其中的 "." 替换为 "/",最后加分号),例如 String => Ljava/lang/String。 -
数组(引用类型):
不同类型的数组 => "[该类型对应的描述名",例如 int 数组 => "[I",String 数组 => "[Ljava/lang/Sting",二维 int 数组 => "[[I"。
2、成员变量
在 JVM 规范之中,成员变量即 Field Descriptor 的描述规则如下所示:
FiledDescriptor:
# 1、仅包含 FieldType 一种信息
FieldType
FiledType:
# 2、FiledType 的可选类型
BaseType | ObjectType | ArrayType
BaseType:
B | C | D | F | I | J | S | Z
ObjectType:
L + 全路径ClassName;
ArrayType:
[ComponentType:
# 3、与 FiledType 的可选类型一样
ComponentType:
3、成员函数描述规则
在 JVM 规范之中,成员函数即 Method Descriptor 的描述规则如下所示:
MethodDescriptor:
# 1、括号内的是参数的数据类型描述,* 表示有 0 至多个 ParameterDescriptor,最后是返回值类型描述
( ParameterDescriptor* ) ReturnDescriptor
ParameterDescriptor:
FieldType
ReturnDescriptor:
FieldType | VoidDescriptor
VoidDescriptor:
// 2、void 的描述规则为 "V"
V
四、filed_info 与 method_info
field_info {
u2 access_flags;
u2 name
u2 descriptor_index
u2 attributes_count
attribute_info attributes[attributes_count]
}
method_info {
u2 access_flags;
u2 name
u2 descriptor_index
u2 attributes_count
attribute_info attributes[attributes_count]
}
可以看到,filed_info 与 method_info 都包含有 访问标志、名字引用、描述信息、属性数量与存储属性 的数据结构。对于 method_info 所描述的成员函数来说,它的内容经过编译之后得到的 Java 字节码会保存在属性之中。
注意:类构造器为 “< clinit >” 方法,而实例构造器为 “< init >” 方法。
五、access_flags
1、Class 的 access_flags 取值类型
标志名 | 标志值 | 标志含义 |
---|---|---|
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 | 枚举类型 |
2、Filed 的 access_flag 取值类型
标志名 | 标志值 | 标志含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | private |
ACC_PROTECTED | 0x0004 | protected |
ACC_STATIC | 0x0008 | static |
ACC_FINAL | 0x0010 | final |
ACC_VOLATILE | 0x0040 | volatile |
ACC_TRANSIENT | 0x0080 | transient,不能被序列化 |
ACC_SYNTHETIC | 0x1000 | 由编译器自动生成 |
ACC_ENUM | 0x4000 | enum,字段为枚举类型 |
3、Method 的 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 | bridge,方法由编译器产生 |
ACC_VARARGS | 0x0080 | 该方法带有变长参数 |
ACC_NATIVE | 0x0100 | native |
ACC_ABSTRACT | 0x0400 | abstract |
ACC_STRICT | 0x0800 | strictfp |
AACC_SYNTHETIC | 0x1000 | 方法由编译器生成 |
六、属性
attribute_info 的数据结构伪代码如下所示:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
- attribute_name_index:为 CONSTANT_Utf8 类型常量项的索引,表示属性的名称。
- attribute_length:属性的长度。
- info:属性具体的内容。
1、attribute_name_index
- ConstantValue:仅出现在 filed_info 中,描述常量成员域的值,通知虚拟机自动为静态变量赋值。对于非 static 类型的变量(也就是实例变量)的赋值是在实例构造器方法中进行的;而对 于类变量,则有两种方式可以选择:在类构造器方法中或者使用 ConstantValue 属性。如果变量没有被 final 修饰,或者并非基本类型及字 符串,则将会选择在方法中进行初始化。
- Code:仅出现 method_info 中,描述函数内容,即该函数内容编译后得到的虚拟机指令,try/catch 语句对应的异常处理表等等。
- StackMapTable:在 JDK 1.6 发布后增加到了 Class 文件规范中,它是一个复杂的变长属性。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流 分析的类型推导验证器。它省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而是在编译阶 段将一系列的验证类型(Verification Types)直接记录在 Class 文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能。这个验证器在 JDK 1.6 中首次提供,并在 JDK 1.7 中强制代替原本基于类型推断的字节码验证器。StackMapTable 属性中包含零至多个栈映射帧(Stack Map Frames),其中的类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。
- Exceptions:当函数抛出异常或错误时,method_info 将会保存此属性。
- InnerClasses:用于记录内部类与宿主类之间的关联。
- EnclosingMethod
- Synthetic:标识方法或字段为编译器自动生成的。
- Signature:JDK 1.5 中新增的属性,用于支持泛型情况下的方法签名,由于 Java 的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息。
- SourceFile:包含一个指向 Utf8 常量项的索引,即 Class 对应的源码文件名。
- SourceDebugExtension:用于存储额外的调试信息。
- LineNumberTable:Java 源码的行号与字节码指令的对应关系。
- LocalVariableTable:局部变量数组/本地变量表,用于保存变量名,变量定义所在行。
- LocalVariableTypeTable:JDK 1.5 中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加。
- Deprecated
- RuntimeVisibleAnnotations
- RuntimeInvisibleAnnotations
- RuntimeVisibleParameterAnnotations
- RuntimeInvisibleParameterAnnotations
- AnnotationDefault
- BootstrapMethods:JDK 1.7中新增的属性,用于保存 invokedynamic 指令引用的引导方法限定符。切记,类文件的属性表中最多也只能有一个 BootstrapMethods 属性。
**LineNumberTable **
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];
}
1、start_pc:为 code[] 数组元素的索引,用于指向 Code_attribute 中 code 数组某处指令。
2、line_number:为 start_pc 对应源文件代码的行号。需要注意的是,多个 line_number_table 元素可以指向同一行代码,因为一行 Java 代码很可能被编译成多条指令。
**LocalVariableTable **
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];
}
其中最重要的元素是 local_variable_table 数组,其中的 start_pc 与 length 这两个参数
决定了一个局部变量在 code 数组中的有效范围。
需要注意的是,每个非 static 函数都会自动创建一个叫做 this 的本地变量,代表当前是在哪个对象上调用此函数。并且,this 对象是位于局部变量数组第1个位置(即 Slot = 0),它的作用范围是贯穿整个函数的。
此外,在 JDK 1.5 引入泛型之后,LocalVariableTable 属性增加了一个 “姐妹属性”: LocalVariableTypeTable,这个新增的属性结构与 LocalVariableTable 非常相似,仅仅是把记录 的字段描述符的 descriptor_index 替换成了字段的特征签名(Signature),对于非泛型类型来 说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确地描述泛型类型了,因此出现了 LocalVariableTypeTable。
2、Code_attribute
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
- attribute_name_index、attribute_length:attribute_length 的值为整个 Code 属性减去 attribute_name_index 和 attribute_length 的长度。
- max_stack:为当前方法执行时的最大栈深度,所以 JVM 在执行方法时,线程栈的栈帧(操作数栈,operand satck)大小是可以提前知道的。每一个函数执行的时候都会分配一个操作数栈和局部变量数组,而 Code_attribure 需要包含它们,以便 JVM 在执行函数前就可以分配相应的空间。
- max_locals:为当前方法分配的局部变量个数,包括调用方式时传递的参数。long 和 double 类型计数为 2,其他为 1。max_locals 的单位是 Slot,Slot 是
虚拟机为局部变量分配内存所使用的最小单位。局部变量表中的 Slot 可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量 所占的 Slot 可以被其他局部变量所使用,Javac 编译器会根据变量的作用域来分配 Slot 给各个 变量使用,然后计算出 max_locals 的大小。 - code_length:为方法编译后的字节码的长度。
- code:用于存储字节码指令的一系列字节流。既然叫字节码指令,那么每个指令就是一个 u1 类型的单字节。一个 u1 数据类型的取值范围为 0x00~0xFF,对应十进制的 0~255,也就是一共可以表达 256 条指令。
- exception_table_length:表示 exception_table 的长度。
- exception_table:每个成员为一个 ExceptionHandler,并且一个函数可以包含多个 try/catch 语句,一个 try/catch 语句对应 exception_table 数组中的一项。
- start_pc、end_pc:为异常处理字节码在 code[] 的索引值。当程序计数器在 [start_pc, end_pc) 内时,表示异常会被该 ExceptionHandler 捕获。
- handler_pc:表示 ExceptionHandler 的起点,为 code[] 的索引值。
- catch_type:为 CONSTANT_Class 类型常量项的索引,表示处理的异常类型。如果该值为 0,则该 ExceptionHandler 会在所有异常抛出时会被执行,可以用来实现 finally 代码。当 catch_type 的值为 0 时,代表任意异常情况都需要转向到 handler_pc 处进行处理。此外,编译器使用异常表而不是简单的跳转命令来实现 Java 异常及 finally 处理机制。
- attributes_count 和 attributes:表示该 exception_table 拥有的 attribute 数量与数据。