了解Class文件的结构组成,对于我们后续的JVM以及Java原理深入学习是很有帮助的,因为Class文件帮我们默默的做了很多事,比如、为什么对象方法中可以直接使用this变量?!本文将带领大家,一步步,从开头到结尾,逐字逐句分析、了解、深入Class文件组成和结构!
文章目录
- 1 Class文件与无关性
- 2 Class文件的结构
- 2.1 Class文件结构概述
- 2.2 魔数
- 2.2.1 案例
- 2.3 补充:各种名以及描述符
- 2.3.1 全限定名和非限定名
- 2.3.2 描述符
- 2.3.2.1 字段描述符
- 2.3.2.2 方法描述符
- 2.4 Class文件版本
- 2.4.1 案例
- 2.5 常量池
- 2.5.1 常量池表项目类型
- 2.5.2 常量池表结构
- 2.5.3 案例
- 2.5.3.1 constant_pool_count
- 2.5.3.2 第一项常量
- 2.5.3.3 第二项常量
- 2.5.3.4 第三项常量
- 2.5.3.5 第四项常量
- 2.5.4 javap命令帮助分析Class文件
- 2.6 Class访问标记(access_flag)
- 2.6.1 案例
- 2.7 当前类索引、父类索引、接口索引集合
- 2.7.1 案例
- 2.7.1.1 当前类和父类索引
- 2.7.1.2 接口个数、索引集合
- 2.8 字段表集合(field_info)
- 2.8.1 案例
- 2.8.1.1 fields_count
- 2.8.1.2 第一个字段
- 2.8.1.3 第二个字段
- 2.9 方法表集合(method_info)
- 2.9.1 案例
- 2.9.1.1 methods_count
- 2.9.1.2 第一个方法
- 2.10 属性表集合
- 2.10.1 预定义属性
- 2.10.2 Code属性——方法体
- 2.10.2.1 exception_table——异常表
- 2.10.3 LineNumberTable属性——记录行号
- 2.10.4 LocalVariableTable属性——保存局部变量和参数
- 2.10.5 StackMapTable属性——加快字节码校验
- 2.10.6 Exceptions属性——列举异常
- 2.10.7 SourseFile属性——记录来源
- 2.10.8 InnerClass属性——记录内部类
- 2.10.9 ConstantValue属性——static(常量)标识
- 2.10.11 Signature属性
- 2.10.12 Deprecated属性——废弃
- 2.10.13 Synthetic属性——编译器添加
- 2.10.14 测试
- 2.10.14.1 Code属性
- 2.10.14.2 Code内部第一个属性
- 2.10.14.3 Code内部第二个属性
- 2.10.14.4 验证
- 3 总结
1 Class文件与无关性
Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode,Class文件语法)是构成平台无关性的基石,也是实现语言无关性的基石。
2 Class文件的结构
2.1 Class文件结构概述
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。Class文件是一组以8位字节为基础单位的二进制流。
Java 虚拟机规范规定 Class 文件格式采用一种类似与 C 语言结构体的伪结构体来存储数据,这种伪结构体中只有两种数据类型:无符号数和表。
- 无符号数属于基本的数据类型,以 u1、u2、u4、u8来分别代表 1 个字节、2 个字节、4 个字节和 8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码结构构成的字符串值。对于字符串,则使用u1数组进行表示。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以「_info」结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件就是一张ClassFile 表,它由下表中所示的数据项构成。
根据 Java 虚拟机规范,一个Class文件由单个 ClassFile 结构组成:
ClassFile {
u4magic; //Class 文件的标志,魔术
u2minor_version;//Class 的附版本号
u2major_version;//Class 的主版本号
u2constant_pool_count;//常量池表项的数量
cp_infoconstant_pool[constant_pool_count-1];//常量池表项,索引为1~constant_pool_count-1
u2access_flags;//Class 的访问标志(类访问修饰符)
u2this_class;//表示当前类的引用
u2super_class;//表示父类的引用
u2interfaces_count;//实现接口数量
u2interfaces[interfaces_count];//接口索引数组
u2fields_count;//此类的字段表中的字段数量
field_infofields[fields_count];//一个类会可以有多个字段,字段表
u2methods_count;//此类的方法表中的方法数量
method_infomethods[methods_count];//一个类可以有个多个方法,方法表
u2attributes_count;//此类的属性表中的属性数量
attribute_infoattributes[attributes_count];//属性表集合
}
Class文件字节码结构组织示意图:
下面来一个一个具体介绍!
2.2 魔数
u4magic;//Class 文件的标志
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。之所以使用魔数而不是文件后缀名来进行识别主要是基于安全性的考虑,因为文件后缀名是可以随意更改的(当然魔术也可以改,只不过比起改后缀名来说更复杂)。
Class 文件的魔数值固定为「0xCAFEBABE」。Java一直以咖啡为代言,CAFEBABE可以认为是 Cafe Babe,读音上和Cafe Baby很近。所以这个也许就是代表Cafe Baby的意思。
2.2.1 案例
Java源码
public class ClassFile {
public static final String J = "2222222";
private int k;
public int getK() {
return k;
}
public void setK(int k) throws Exception {
try {
this.k = k;
} catch (IllegalStateException e) {
e.printStackTrace();
} finally {
}
}
public static void main(String[] args) {
}
}
运行后,会出现 Class文件,拿到Class文件,使用notepad++编辑器打开–点击插件–HEX-Editor(没有该插件的自行下载)–view in HEX,即可以16进制形式查看Clsaa文件。
当然也可以直接使用HEX-Editor软件打开:hex-editor。
我们先看前四个字节:
我们可以看到魔数在首位,并且正是0xcafebabe。
2.3 补充:各种名以及描述符
2.3.1 全限定名和非限定名
Class文件中的类和接口,都是使用全限定名,又被称作Class的二进制名称。例如“com/ikang/JVM/classfile”是这个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。
非限定名又被称作简单名称,Class文件中的方法、字段、局部变量、形参名称,都是使用简单名称,没有类型和参数修饰,例如这个类中的getK()方法和k字段的简单名称分别是“getK”和“m”。
非限定名不得包含ASCII字符. ; [ / ,此外方法名称除了特殊方法名称< init >和< clinit >方法之外,它们不能包含ASCII字符<或>,字段名称或接口方法名称可以是< init >或< clinit >,但是没有方法调用指令可以引用< clinit >,只有invokespecial指令可以引用< init >。
2.3.2 描述符
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值类型。
2.3.2.1 字段描述符
根据描述符规则基本数据类型(byte、char、doubIc、float, int、loog、shon, boolean)以及代表无返回伯的void类型都用一个大写字符来表示, 而对象类型则用字符L加对象的全限定名来表示,数组则用[
字段描述符的类型含义表:
字段描述符 | 类型 | 含义 |
B | byte | 基本类型byte |
C | char | 基本类型char |
D | double | 基本类型double |
F | float | 基本类型float |
I | int | 基本类型int |
J | long | 基本类型long |
LClassName; | reference | 对象类型,例如Ljava/lang/Object; |
S | short | 基本类型short |
Z | boolean | 基本类型boolean |
[ | reference | 数组类型 |
对象类型的实例变量的字段描述符是L+类的二进制名称的内部形式。
对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String
[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String;
”,一个整型数组“int[]
”将被记录为“[I
”
2.3.2.2 方法描述符
它基于描述符标识字符含义表所示的字符串的类型表示方法, 同时对方法签名的表示做了一些规定。它将函数的参数类型写在一对小括号中, 并在括号右侧给出方法的返回值。
比如, 若有如下方法:
Object m(int i, double d, Thread t) {… }
则它的方法描述符为:
(IDLjava/lang/Thread;)Ljava/lang/Object;
可以看到, 方法的参数统一列在一对小括号中, “I”表示int , “D”表示double,“Ljava/lang/Thread;”表示Thread对象。小括号右侧的Ljava/lang/Object;表示方法的返同值为Object对象类型。
2.4 Class文件版本
u2minor_version;//Class 的附版本号
u2major_version;//Class 的主版本号
紧接着魔数的 4 个字节存储的是 Class 文件的版本号:第 5 和第 6 两个字节是附版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version)。
高版本的 JDK 能够向下兼容低版本的 Class 文件,但虚拟机会拒绝执行超过其版本号的 Class 文件(低版本不能向上打开高版本文件)。
JDK主版本号 | Class主版本号 | 16进制 |
1.1 | 45.0 | 00 00 00 2D |
1.2 | 46.0 | 00 00 00 2E |
1.3 | 47.0 | 00 00 00 2F |
1.4 | 48.0 | 00 00 00 30 |
1.5 | 49.0 | 00 00 00 31 |
1.6 | 50.0 | 00 00 00 32 |
1.7 | 51.0 | 00 00 00 33 |
1.8 | 52.0 | 00 00 00 34 |
2.4.1 案例
使用上面的案例,向后取四个字节:
我们发现主版本号为0x0034,转换为十进制为52,可知属于JDK1.8
2.5 常量池
u2constant_pool_count;//常量池表项的数量
cp_infoconstant_pool[constant_pool_count-1];//常量池表项,索引为1~constant_pool_count-1
紧接着主次版本号之后的是常量池的常量数量constant_pool_count,但是常量池的常量实际数量是 constant_pool_count-1(常量池计数器是从1开始计数的,将第0项常量空出来是有特殊考虑的,索引值为0代表“不引用任何一个常量池项;其它集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数都是从 0 开始。”)。
之后就是常量池的实际内容constant_pool,每一项以类型、长度、内容或者、类型、内容的格式依次排列存放。
常量池是 Class 文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一,同是它还是 Class 文件中第一个出现的表类型数据项目。
java虚拟机指令并不依赖类、接口、类实例或者数组的运行时布局。相反,指令依靠常量池中的符号信息,常量池是整个Class文件的基石。
Class常量池主要存放两大常量:字面量和符号引用。
- 字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。
- 符号引用则属于编译原理方面的概念。包括下面三类常量:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符
补充:由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。
2.5.1 常量池表项目类型
在JDK1.8中有14种常量池项目类型,每一种项目都有特定的表结构,这14种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型。
常量池tag类型表:
常量类型 | 标志(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_MothodType_info | 16 | 标志方法类型 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
2.5.2 常量池表结构
每种常量类型均有自己的表结构,非常繁琐:
常量池项目结构表:
常量 | 描述 | 项目 | 类型 | 项目描述 |
CONSTANT_Utf8_info | UTF-8编码的字符串 | tag | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用的字节数 | ||
bytes[length] | u1 | 长度为length的UTF-8编码的字符串 | ||
CONSTANT_Integer_info | 整型字面量 | tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的int值 | ||
CONSTANT_Float_info | 浮点型字面量 | tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的float值 | ||
CONSTANT_Long_info | 长整型字面量 | tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的long值 | ||
CONSTANT_Double_info | 双精度浮点型字面量 | tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的double值 | ||
CONSTANT_Class_info | 类或接口的符号引用 | tag | u1 | 值为7 |
name_index | u2 | 指向全限定名常量项的索引 | ||
CONSTANT_String_info | 字符串类型字面量 | tag | u1 | 值为8 |
string_index | u2 | 指向字符串字面量的索引 | ||
CONSTANT_Fieldref_info | 字段的符号引用 | tag | u1 | 值为9 |
class_index | u2 | 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项 | ||
name_and_type_index | u2 | 指向字段描述符CONSTANT_NameAndType的索引项 | ||
CONSTANT_Methodref_info | 类中方法的符号引用 | tag | u1 | 值为10 |
class_index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | ||
name_and_type_index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | ||
CONSTANT_InterfaceMethodref_info | 接口中方法的符号引用 | tag | u1 | 值为11 |
class_index | u2 | 指向声明方法的接口描述符CONSTANT_Class_info的索引项 | ||
name_and_type_index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | ||
CONSTANT_NameAndType_info | 字段或方法的部分符号引用 | tag | u1 | 值为12 |
name_index | u2 | 指向该字段或方法名称常量项的索引 | ||
descriptor_index | u2 | 指向该字段或方法描述符常量项的索引 | ||
CONSTANT_MethodHandle_info | 表示方法句柄 | tag | u1 | 值为15 |
reference_kind | u1 | 值必须在1~9范围,它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为 | ||
reference_index | u2 | 值必须是对常量池的有效索引 | ||
CONSTANT_MethodType_info | 标识方法类型 | tag | u1 | 值为16 |
descriptor_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符 | ||
CONSTANT_InvokeDynamic_info | 表示一个动态方法调用点 | tag | u1 | 值为18 |
bootstrap_method_attr_index | u2 | 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的的有效索引 | ||
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 |
当然,所有的常量池条目都有如下的通用结构如下:
cp_info {
u1 tag;
u1 info[];
}
2.5.3 案例
2.5.3.1 constant_pool_count
继续向后走,到了constant_pool_count,取两个字节:
可以看到常量池的表项目数量0x2f,转换为十进制为47,那么常量池表项为47-1=46项。因为常量池计数器是从1开始计数的,将第0项常量空出来是有特殊考虑的,索引值为0代表不引用任何一个常量池项;但是其它集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数都是从 0 开始。
2.5.3.2 第一项常量
继续向后走,到了constant_pool了,常量类型的通用第一项均是u1长度的tag,那么向后取一个字节,即取第一个常量的tag:
该值为0x0a,转换为10进制就是tag=10,查找上面的常量池项目类型表,可知第一个常量属于CONSTANT_Methodref_info,表示类中方法的符号引用,在常量池结构表中查找它的结构,如下:
由于后两个字段都是指向常量池的索引,因此完整结构为:
继续向后走4个字节:
class_index=0x0006,表示指向声明方法的类描述符CONSTANT_Class_info的索引项由常量池第6个常量字符串指定。
name_and_type_index=0x0025,表示指向名称及类型描述符CONSTANT_NameAndType的索引项由常量池第37个常量字符串指定。
2.5.3.3 第二项常量
第一项常量结束,继续向下查找第二个常量项的tag。
该tag为0x09,转换为十进制就是9,查找上面的项目类型表,可知第二个常量项属于CONSTANT_Fieldref_info,表示字段的符号引用,在结构表中查找它的结构,如下:
可知后面两个u2类型项目同样都是指向常量池的索引,向后取四个字节:
class_index=0x0005,表示指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项由常量池第5个常量字符串指定。
name_and_type_index=0x0026,表示指向字段描述符CONSTANT_NameAndType的索引项由常量池第38个常量字符串指定。
2.5.3.4 第三项常量
第二项常量结束,继续向下查找第三个常量项的tag:
该tag为0x07,转换为十进制就是7,查找上面的项目类型表,可知第三个常量项属于CONSTANT_Class_info,表示类或接口的符号引用,在结构表中查找它的结构,如下:
可知后面一个u2类型项目是指向常量池的索引,向后取两个字节:
name_index=0x0027,表示该类的全限定类名由常量池第39个常量字符串指定。
2.5.3.5 第四项常量
第三项常量结束,继续向下查找第四项常量的tag:
该tag值为0x0a,转换而为10进制就是tag=10,查找上面的项目类型表,可知第四个常量属于CONSTANT_Methodref_info,表示类中方法的符号引用,在结构表中查找它的结构,如下:
可知后面两个u2类型项目都是索引,向后取四个字节:
class_index=0x0003,表示指向声明方法的类描述符CONSTANT_Class_info的索引项由常量池第3个常量字符串指定。回过头看我们上面找到的的第三个常量,刚好是CONSTANT_Class_info类型,说明我们到目前所有查找是正确的。
name_and_type_index=0x0028,表示指向名称及类型描述符CONSTANT_NameAndType的索引项由常量池第40个常量字符串指定。
第四项常量结束,目前找到了4个,还剩下42个,这种方法看起来很麻烦,还容易出错。我们可以使用javap工具加上-v参数帮我们分析字节码文件。
2.5.4 javap命令帮助分析Class文件
javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
我们可以直接使用idea自带的控制台,找到class文件,右键——open in Terminal
输入 javap -v ClassFile.class 指令,即可输出Class文件信息。
信息如下:
Classfile /J:/Idea/jvm/target/classes/com/ikang/JVM/classfile/ClassFile.class
Last modified 2020-4-4; size 960 bytes
MD5 checksum a7fc5ccb0b193f1d32e2658e68fed475
Compiled from "ClassFile.java"
public class com.ikang.JVM.classfile.ClassFile
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#37 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#38 // com/ikang/JVM/classfile/ClassFile.k:I
#3 = Class #39 // java/lang/IllegalStateException
#4 = Methodref #3.#40 // java/lang/IllegalStateException.printStackTrace:()V
#5 = Class #41 // com/ikang/JVM/classfile/ClassFile
#6 = Class #42 // java/lang/Object
#7 = Utf8 J
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 ConstantValue
#10 = String #43 // 2222222
#11 = Utf8 k
#12 = Utf8 I
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lcom/ikang/JVM/classfile/ClassFile;
#20 = Utf8 getK
#21 = Utf8 ()I
#22 = Utf8 setK
#23 = Utf8 (I)V
#24 = Utf8 e
#25 = Utf8 Ljava/lang/IllegalStateException;
#26 = Utf8 StackMapTable
#27 = Class #39 // java/lang/IllegalStateException
#28 = Class #44 // java/lang/Throwable
#29 = Utf8 Exceptions
#30 = Class #45 // java/lang/Exception
#31 = Utf8 main
#32 = Utf8 ([Ljava/lang/String;)V
#33 = Utf8 args
#34 = Utf8 [Ljava/lang/String;
#35 = Utf8 SourceFile
#36 = Utf8 ClassFile.java
#37 = NameAndType #13:#14 // "<init>":()V
#38 = NameAndType #11:#12 // k:I
#39 = Utf8 java/lang/IllegalStateException
#40 = NameAndType #46:#14 // printStackTrace:()V
#41 = Utf8 com/ikang/JVM/classfile/ClassFile
#42 = Utf8 java/lang/Object
#43 = Utf8 2222222
#44 = Utf8 java/lang/Throwable
#45 = Utf8 java/lang/Exception
#46 = Utf8 printStackTrace
{
public static final java.lang.String J;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: String 2222222
public com.ikang.JVM.classfile.ClassFile();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ikang/JVM/classfile/ClassFile;
public int getK();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field k:I
4: ireturn
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/ikang/JVM/classfile/ClassFile;
public void setK(int) throws java.lang.Exception;
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field k:I
5: goto 19
8: astore_2
9: aload_2
10: invokevirtual #4 // Method java/lang/IllegalStateException.printStackTrace:()V
13: goto 19
16: astore_3
17: aload_3
18: athrow
19: return
Exception table:
from to target type
0 5 8 Class java/lang/IllegalStateException
0 5 16 any
8 13 16 any
LineNumberTable:
line 16: 0
line 20: 5
line 17: 8
line 18: 9
line 20: 13
line 19: 16
line 21: 19
LocalVariableTable:
Start Length Slot Name Signature
9 4 2 e Ljava/lang/IllegalStateException;
0 20 0 this Lcom/ikang/JVM/classfile/ClassFile;
0 20 1 k I
StackMapTable: number_of_entries = 3
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/IllegalStateException ]
frame_type = 71 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 2 /* same */
Exceptions:
throws java.lang.Exception
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 25: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 args [Ljava/lang/String;
}
SourceFile: "ClassFile.java"
截取其中的常量池部分,也就是Constant pool部分:
Constant pool:
#1 = Methodref #6.#37 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#38 // com/ikang/JVM/classfile/ClassFile.k:I
#3 = Class #39 // java/lang/IllegalStateException
#4 = Methodref #3.#40 // java/lang/IllegalStateException.printStackTrace:()V
#5 = Class #41 // com/ikang/JVM/classfile/ClassFile
#6 = Class #42 // java/lang/Object
#7 = Utf8 J
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 ConstantValue
#10 = String #43 // 2222222
#11 = Utf8 k
#12 = Utf8 I
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lcom/ikang/JVM/classfile/ClassFile;
#20 = Utf8 getK
#21 = Utf8 ()I
#22 = Utf8 setK
#23 = Utf8 (I)V
#24 = Utf8 e
#25 = Utf8 Ljava/lang/IllegalStateException;
#26 = Utf8 StackMapTable
#27 = Class #39 // java/lang/IllegalStateException
#28 = Class #44 // java/lang/Throwable
#29 = Utf8 Exceptions
#30 = Class #45 // java/lang/Exception
#31 = Utf8 main
#32 = Utf8 ([Ljava/lang/String;)V
#33 = Utf8 args
#34 = Utf8 [Ljava/lang/String;
#35 = Utf8 SourceFile
#36 = Utf8 ClassFile.java
#37 = NameAndType #13:#14 // "<init>":()V
#38 = NameAndType #11:#12 // k:I
#39 = Utf8 java/lang/IllegalStateException
#40 = NameAndType #46:#14 // printStackTrace:()V
#41 = Utf8 com/ikang/JVM/classfile/ClassFile
#42 = Utf8 java/lang/Object
#43 = Utf8 2222222
#44 = Utf8 java/lang/Throwable
#45 = Utf8 java/lang/Exception
#46 = Utf8 printStackTrace
{
我们发现,这样能更加直观的查看Class的信息,左边表示第一个常量,右边是指向的常量索引或者具体的常量值,我们分析前四项:
从上面的数据可以看出,我们在前面把前四项的常量都计算了出来,并且与我们的就算结果一致。后面的分析我们会继续和javap的结果做对比。
查看右边是具体的常量值,会发现其中有一部分常量是咱们没见过的,比如"< init > " : ( ) V 、LineNumberTable、LocalVariableTable等常量。实际上它们会被后面的字段表、方法表、属性表所使用到,他们用来描述无法使用固定字节表达的内容,比如描述方法的返回值、有几个参数、每个参数类型等。因为Java中的 “类” 是无穷无尽的, 无法通过简单的无符号字节来描述一个方法用到了什么类, 因此在描述方法的这些信息时, 需要引用常量表中的符号引用进行表达。
2.6 Class访问标记(access_flag)
u2access_flags;//Class的访问标记(类访问修饰符)
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问权限和属性,例如:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。
类访问和属性修饰符标志表:
表示名称 | 值 | 含义 |
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final类型,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDB1.2之后发生过改变,为了区别这条指令使用哪种语意,JDK1.0.2之后编译出来的类的这个标志都必须为真。 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志为真,其它类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
2.6.1 案例
我们使用javap的帮助,可以快速找到常量池的每一位常量项占用的字节,这样就能快速找到常量池后面的访问标志。通过javap可知,最后一项常量是printStackTrace,那么我们可以轻易找到他所在的字节,如下图:
然后尝试计算access_flag的值,该测试类为public,因此ACC_PUBLIC为真,加上使用的是JDK1.8编译,那么ACC_SUPER一定为真,其他标志则为假。综合起来access_flag应该为:0x0001+0x0020=0x0021。
下一项u2就是access_flag,向后取两个字节来进行验证:
发现确实是0x0021,说明咱们计算正确。
2.7 当前类索引、父类索引、接口索引集合
u2this_class;//表示当前类的引用
u2super_class;//表示父类的引用
u2interfaces_count;//接口数量
u2interfaces[interfaces_count];//接口索引集合
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。他们各自指向常量池中一个CONSTANT_Class_info类型的类描述符常量,通过CONSTANT_Class_info的索引可以定位到内部的CONSTANT_utf8_info常量中的全限定类名字符串。
接口索引数组用来描述这个类实现了哪些接口,这些被实现的接口将按implents(如果这个类本身是接口的话则是extends) 后的接口顺序从左到右排列在接口索引集合中。如果该类没有实现任何接口,则interfaces_count为0,并且后面的interfaces集合不占用字节。
2.7.1 案例
2.7.1.1 当前类和父类索引
继续向后四个字节:
我们找到这两个u2类型的引用,分别是0x0005和0x0006,他们的十进制值为5和6,表示指向常量池中的第5和6个常量项。
使用javap,查看查看常量池中的第5和6个常量验证:
确实是CONSTANT_Class_info类型,继续查看CONSTANT_Class_info的索引指向的第41和42个常量:
确实是CONSTANT_utf8_info字符串类型,后面就能找到本类和父类的全限定类名了,说明我们计算正确。
2.7.1.2 接口个数、索引集合
向后走两位,查看实现的接口个数interfaces_count信息:
interfaces_count接口计数为0x0000,即表示不继承接口,那么后面的interfaces集合就字段不占用字节了。
到此,当前类索引、父类索引、接口索引集合,结束。
2.8 字段表集合(field_info)
u2 fields_count; //此类的字段表中的字段数量
field_info fields[fields_count]; //一个类会可以有多个字段,字段表
字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。不会列出从父类或者父接口继承来的字段,但有可能列出原本Java代码没有的字段,比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
field info(字段表) 的结构如下:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
- access_flags: 字段的作用域(public,private,protected修饰符),是实例变量还是类变量(static修饰符),可否被序列化(transient修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。 只有一个
- name_index:对常量池的引用,CONSTANT_utf8_info,表示的字段的名称;只有一个
- descriptor_index: 对常量池的引用,CONSTANT_utf8_info,表示字段和方法的描述符;只有一个
- attributes_count: 一个字段还会拥有一些额外的属性,表示attributes_count 存放属性的个数;只有一个
- attributes[attributes_count]: 存放具体属性具体内容集合。有attributes_count个
在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
额外的属性表示一个字段还可能拥有一些属性, 用于存储更多的额外信息, 比如初始化值、一些注释信息等。属性个数存放在attributes_count 中, 属性具体内容存放于attributes数组。常量数据比如“public static finall”类型的字段,就有一个ConstantValue的额外属性。
字段访问和属性标识表:
标志名称 | 标志值 | 含义 |
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 |
attribute属性结构(在属性表部分会深入讲解):
以常量属性为例,常量属性的结构为:
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
attribute_name_index 为2 字节整数, 指向常量池的CONSTANT_Utf8_info类型, 并且这个字符串为“ ConstantValue" 。该属性是所有类型的字段都有的。
attribute_length由4个字节组成, 表示这个属性的剩余长度为多少。对常量属性而言, 这个值恒定为2,表示从Ox00000002 之后的两个字节为属性的全部内容。该属性是所有类型的字段都有的。
constantvalue_ index 表示属性值,但值并不直接出现在属性中,而是指向常量池引用,即存放在常量池中,不同类型的属性会和不同的常量池类型匹配。
2.8.1 案例
2.8.1.1 fields_count
向后走两个字节,查看fields_count:
0x0002,即有两个字段,可以猜测是常量J和普通对象变量k。
2.8.1.2 第一个字段
下面来看第一个字段,先取前八个字节:
八个字节,两个一组,表示字段作用域、对常量池的引用–字段的名称、对常量池的引用–字段和方法的描述符、额外属性。
access_flags表示字段作用域为0x19,查询字段访问标志表,可知,该字段作用域为:ACC_PUBLIC+ACC_STATIC+ACC_FINAL,即public static final ,可以猜到该字段就是类中的“J”字段。
name_index表示对常量池的引用(字段的名称),值分别0x0007,即第七个常量。
descriptor_index表示对常量池的引用(字段和方法的描述符),0x0008,即第八个常量。
我们在 javap命令下的常量池中查找:
看到值为“J”,刚好是我们类中常量的字段的名称。字段和方法的描述符为Ljava/lang/String;,即String类型,同样符合我们的预期。
attributes_count表示额外属性个数,值为0x0001,即1个。那么后面的第五个属性就是描述该属性的结构,由于是常量属性,格式会占用8个字节。
attributes描述额外属性的具体内容集合,该例子中只有一个数据,如下8字节:
前两个字节为attribute_name_index,指向常量池,0x09即第九个常量,常量字段值固定为“ConstantValue”,我们通过javap验证:
发现确实如此。
后4个字节为attribute_length**, 表示这个属性的剩余长度为多少,常量固定为2,从上面可以看到确实是2;表示从Ox00000002 之后的两个字节为属性的全部内容。**
最后取两个字节表示constantvalue_ index**,即属性值引用,同样指向常量池,0x0a即第10个常量,我们通过javap验证:
第10项表示该常量是String类型,并且具体值又指向了第43个常量,我们找到它:
这里就是存的具体值了,确实为“2222222”,和代码中一致。
2.8.1.3 第二个字段
下面来看第二个字段,同样先看前四个属性:
access_flag访问标志为 0x0002,查询字段access表,可知该字段为ACC_PRIVATE,即private。
name_index字段名引用为0x000b,即指向常量池第11个字段:
我们看到字段名确实是k。
descriptor_index字段描述符引用为0x000c,即指向常量池第12个字段:
可以看到,由于是基本类型,并且是int,因此只有一个I,符合预测。
attributes_count表示额外的属性,这里是0x0000,即没有额外属性。那么后面的额外属性集合自然不占用字节了。
到此字段表分析结束。
2.9 方法表集合(method_info)
u2methods_count;//此类的方法表中的方法数量
method_infomethods[methods_count];//一个类可以有个多个方法,方法表
Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标记、名称索引、描述符索引、属性表集合几项。而方法中的具体代码则是存放在属性表中了。类似于字段表,子类不会记录父类未重写的方法,同上编译器可能自己加方法,比如< init > 、< clinit >。
method_info(方法表的) 结构如下:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
access_flag表示访问标记,因为volatile修饰符和transient修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了synchronized、native、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为方法描述符, 它也是指向常量池的索引, 是一个字符串, 用以表示方法的签名(参数、返回值等)。关于方法描述符,在最开始已经介绍了。
和字段表类似, 方法也可以附带若干个属性, 用于描述一些额外信息, 比如方法字节码等, attributes_count 表示该方法中属性的数量, 紧接着, 就是attributes_count个属性的描述。
attribute_info通用格式为:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
其中, attribute_name_ index 表示当前attribute的名称, attribute_length为当前attribute的剩余长度, 紧接着就是attribute_length个字节的byte 数组。常见的 attribute在属性表会有详细介绍。
2.9.1 案例
2.9.1.1 methods_count
接着上面的案例,先看methods_count
这说明有四个方法,那么肯定有些方法是编译时自动生成的。
2.9.1.2 第一个方法
然后,看第一个方法的method_info的内部前四个字段:
access_flag为0x0001,查找方的access_flag 取值表可知,该方法使用了ACC_PUBLIC,即pubilc方法。然后没有了修饰符。
name_index为0x000d,即指向常量池第13个 常量:
可以看到该方法名字叫 < init >
descriptor_ index为0x000e,即指向常量池第14个常量:
可以看到该方法描述符号为 ()V
attributes_count 值为0x0001,表示具有一个额外属性,那么可以继续向下找。
向后六个字节,查看attribute_info
attribute_name_index为0x000f,即指向常量池第15个常量:
说明该方法具有名为code的属性!
attribute_length为0x0000002f,即长度为47,这就很长了,并且不同的属性具有自己的格式,因此具体的分析,我们在属性表集合中介绍!
2.10 属性表集合
u2attributes_count;//此类的属性表中的属性数量
attribute_infoattributes[attributes_count];//属性表集合
属性表用于class文件格式中的ClassFile,field_info,method_info和Code_attribute结构,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度去说明属性值所占用的位数即可。
属性表通用结构:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}对于任意属性, attribute_name_index必须是对当前class文件的常量池的有效16位无符号索引。 常量池在该索引处的成员必须是CONSTANT_Utf8_info 结构, 用以表示当前属性的名字。 attribute_length项的值给出了跟随其后的信息字节的 长度, 这个长度不包括attribute_name_index 和 attribute_length项的6个字节。
2.10.1 预定义属性
《java虚拟机规范 JavaSE8》中预定义23项虚拟机实现应当能识别的属性:
属性 | 可用位置 | 含义 |
SourceFile | ClassFile | 记录源文件名称 |
InnerClasses | ClassFile | 内部类列表 |
EnclosingMethod | ClassFile | 仅当一个类为局部类或者匿名类时,才能拥有这个属性,这个属性用于表示这个类所在的外围方法 |
SourceDebugExtension | ClassFile | JDK1.6中新增的属性,SourceDebugExtension用于存储额外的调试信息。如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码运行在Java虚拟机汇中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension就可以存储这些调试信息。 |
BootstrapMethods | ClassFile | JDK1.7新增的属性,用于保存invokedynamic指令引用的引导方法限定符 |
ConstantValue | field_info | final关键字定义的常量值 |
Code | method_info | Java代码编译成的字节码指令(即:具体的方法逻辑字节码指令) |
Exceptions | method_info | 方法声明的异常 |
RuntimeVisibleAnnotations | ClassFile, field_info, method_info | JDK1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性,用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的。 |
RuntimeInvisibleAnnotations | ClassFile, field_info, method_info | JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations相反用于指明哪些注解是运行时不可见的。 |
RuntimeVisibleParameterAnnotations | method_info | JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations类似,只不过作用对象为方法的参数。 |
RuntimeInvisibleParameterAnnotations | method_info | JDK1.5中新增的属性,作用与RuntimeInvisibleAnnotations类似,只不过作用对象为方法的参数。 |
AnnotationDefault | method_info | JDK1.5中新增的属性,用于记录注解类元素的默认值 |
MethodParameters | method_info | 52.0 |
Synthetic | ClassFile, field_info, method_info | 标识方法或字段为编译器自动产生的 |
Deprecated | ClassFile, field_info, method_info | 被声明为deprecated的方法和字段 |
Signature | ClassFile, field_info, method_info | JDK1.5新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息 |
RuntimeVisibleAnnotations | ClassFile, field_info, method_info | JDK1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性,用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的。 |
RuntimeInvisibleAnnotations | ClassFile, field_info, method_info | JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations相反用于指明哪些注解是运行时不可见的。 |
LineNumberTable | Code | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code | 方法的局部变量描述 |
LocalVariableTypeTable | Code | JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
StackMapTable | Code | JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
MethodParameters | method_info | JDK1.8中新加的属性,用于标识方法参数的名称和访问标志。 |
RuntimeVisibleTypeAnnotations | ClassFile, field_info, method_info, Code | JDK1.8中新加的属性,在运行时可见的注释,用于泛型类型,指令等。 |
RuntimeInvisibleTypeAnnotations | ClassFile, field_info, method_info, Code | JDK1.8中新加的属性,在编译时可见的注释,用于泛型类型,指令等。 |
这里主讲一些常见的属性。
2.10.2 Code属性——方法体
Java方法体里面的代码经过Javac编译之后,最终变为字节码指令存储在Code属性内,Code属性出现在在method_info结构的attributes表中,但在接口或抽象类中就不存在Code属性(JDK1.8可以出现了)。一个方法中的Code属性值有一个。
在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。
在字节码指令之后的是这个方法的显式异常处理表(下文简称异常表)集合,异常表对于Code属性来说并不是必须存在的。
Code属性的结构如下:
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];
}
Code属性的第一个字段attribute_name_index指定了该属性的名称,它是一个指向常量池的索引, 指向的类型为CONSTANT_Utf8_info, 对于Code 属性来说,该值恒为“Code"。
attribute_length指定了Code属性的长度,该长度不包括前6个字节,即表示剩余长度。
在方法执行过程中,操作数栈可能不停地变化,在整个执行过程中,操作数栈存在一个最大深度,该深度由max_stack表示。
在方法执行过程中,局部变量表也可能会不断变化。在整个执行过程中局部变量表的最值由max_locals表示, 它们都是2字节的无符号整数。也包括调用此方法时用于传递参数的局部变量。
在max_locals 之后,就是作为方法的最重要部分—字节码。它由code_length和code[code_length]两部分组成, code_length 表示字节码的字节数,为4字节无符号整数,必须大于0;code[code length]为byte数组,即存放的代码的实际字节内容本身。
在字节码之后,存放该方法的异常处理表。异常处理表告诉一个方法该如何处理字节码中可能抛出的异常。异常处理表亦由两部分组成:表项数量和内容。在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的,而是采用异常表来完成的。
exception_table_length表示异常表的表项数量,可以为0;
exception_table[exception_table_length]结构为异常表的数据。
异常表中每一个数据由4部分组成,分别是start_pc、end_pc、handler_pc和catch_type。这4项表示从方法字节码的start_pc偏移量开始(包括)到end_pc 偏移量为止(不包括)的这段代码中,如果遇到了catch_type所指定的异常, 那么代码就跳转到handler_pc的位置执行,handler_pc即一个异常处理器的起点。
在这4项中, start_pc、end_pc和handlerpc 都是字节码的编译量, 也就是在code[code_length]中的位置, 而catch_type为指向常量池的索引,它指向一个CONSTANT_Class_info 类,表示需要处理的异常类型。如果catch_type值为0,那么将会在所有异常抛出时都调用这个异常处理器,这被用于实现finally语句。
至此, Code属性的主体部分已经介绍完毕, 但是Code属性中还可能包含更多信息, 比如行号表、局部变量表等。这些信息都以attribute属性的形式内嵌在Code属性中, 即除了字段、方法和类文件可以内嵌属性外,属性本身也可以内嵌其他属性。attributes_count表示Code属性的属性数量,attributes表示Code属性包含的属性。
2.10.2.1 exception_table——异常表
异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制。(注:在JDK1.4.2之前的Javac编译器采用了jsr和ret指令实现finally语句。在JDK1.7中,已经完全禁止Class文件中出现jsr和ret指令,如果遇到这两条指令,虚拟机会在类加载的字节码校验阶段抛出异常)。
当异常处理存在finally语句块时,编译器会自动在每一段可能的分支路径之后都将finally语句块的内容冗余生成一遍来实现finally语义。
在我们Java代码中,finally语句块是在最后的,但编译器在生成字节码时候,其实将finally语句块的执行指令移到了return指令之前,指令重排序了。所以,从字节码层面,我们解释了,为什么finally语句总会执行!初学Java的时候,我们有感到困惑,为什么方法已经return了,finally语句块里的代码还会执行呢?这是因为,在字节码中,它就是先执行了finally语句块,再执行return的,而这个变化是Java编译器帮我们做的。
2.10.3 LineNumberTable属性——记录行号
位于Code属性中,描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。主要是如果抛出异常时,编译器会显示行号,比如调试程序时展示的行号,就是这个属性的作用。
Code属性表中,LineNumberTable可以属性可以按照任意顺序出现。
在Code属性 attributes表中,可以有不止一个LineNumberTable属性对应于源文件中的同一行。也就是说,多个LineNumberTable属性可以合起来表示源文件中的某行代码,属性与源文件的代码行之间不必有一一对应的关系。
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];
}
其中, attribute_name_index为指向常量池的索引, 在LineNumberTable 属性中, 该值为"LineNumberTable", attribute_length为4 字节无符号整数, 表示属性的长度(不含前6个字节),line_number_table_length 表明了表项有多少条记录。
line_number_table为表的实际内容,它包含line_number_table_length 个<start_pc, line_number>元组,其中,start_pc为字节码偏移量,必须是code数组的一个索引, line_number 为对应的源码的行号。
2.10.4 LocalVariableTable属性——保存局部变量和参数
描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。用处在于当别人使用这个方法是能够显示出方法定义的参数名。
它也不是运行时必需的属性,但默认会生成到Class文件之中。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失。
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];
}
其中, attribute_name_index为当前属性的名字, 它是指向常量池的索引。对局部变量表而言, 该值为“ LocalVariableTable", attribute_length 为属性的长度,local_variable_table_length为局部变量表表项条目。
局部变量表的每一条记录由以下几个部分组成:
- start_pc、length:start_pc表示当前局部变量的开始位置,从0开始,length表示范围覆盖长度,两者结合就是这个局部变量在字节码中的作用域范围。
- name_ index: 局部变量的名称, 这是一个指向常量池的索引,为CONSTANT_Utf8_info类型。
- descriptor_index: 局部变量的类型描述, 指向常量池的索引。使用和字段描述符一样的方式描述局部变量,为CONSTANT_Utf8_info类型。
- index,局部变量在当前帧栈的局部变量表中的槽位(solt)。当变量数据类型是64位时(long 和double), 它们会占据局部变量表中的两个槽位,位置为index和index+1。
在JDK 1.5引入泛型后,LocalVariableTable属性增加了一个“姐妹属性”:LocalVariableTypeTable,这个新增的属性结构与LocalVariableTable非常相似,仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature),对于非泛型类型来说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦出掉,描述符就不能准确地描述泛型类型了,因此出现了LocalVariableTypeTable。
2.10.5 StackMapTable属性——加快字节码校验
JDK 1.6 以后的类文件, 每个方法的Code 属性还可能含有一个StackMapTable 的属性结构。这是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器,加快字节码校验。
StackMapTable属性中包含零至多个栈映射帧(Stack Map Frames),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时局部变量表和操作数栈的验证类型。
该属性不包含运行时所需的信息,仅仅作为Class文件的类型检验。
StackMapTable属性结构如下:
StackMapTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_entries;
stack_map_frame entries[number_of_entries];
}
其中,attribute_name——index为常量池索引, 恒为“ Stack.MapTable", attribute_length为该属性的长度, number_of_entries为栈映射帧的数量, 最后的stack_map_frame entries则为具体的内容,每一项为一个stack_map_ frame结构。
2.10.6 Exceptions属性——列举异常
列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。Exceptions与Code 属性中的异常表不同。Exceptions 属性表示一个方法可能抛出的异常,通常是由方法的throws 关键字指定的。而Code 属性中的异常表,则是异常处理机制,由try-catch语句生成。
Exceptions属性结构如下:
Exceptions_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];
}
Exceptions 属性表中, attribute_name_index 指定了属性的名称, 它为指向常量池的索引,恒为“Exceptions",attribute_lengt表示属性长度, number_of_exceptions表示表项数量即可能抛出的异常个数,最后exception_index_table项罗列了所有的异常,每一项为指向常量池的索引,对应的常量为CONSTANT_Class_info,表示为一个异常类型。
2.10.7 SourseFile属性——记录来源
SourseFile属性ClassFile,记录生成这个Class文件的源码文件名称,抛出异常时能够显示错误代码所属的文件名。
SourseFile属性结构如下:
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}
SourseFile属性表中, attribute_name_index 指定了属性的名称, 它为指向常量池的索引,恒为“SourseFile", attribute_length表示属性长度,对SourseFile属性来说恒为2,sourcefile_index表示源代码文件名, 它是为指向常量池的索引,对应的常量为CONSTANT_Uft8_info类型。
2.10.8 InnerClass属性——记录内部类
InnerClass属性属性ClassFile,用于记录内部类与宿主类之间的关联。
InnerClass属性结构如下:
InnerClasses_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_classes;
{ u2 inner_class_info_index;
u2 outer_class_info_index;
u2 inner_name_index;
u2 inner_class_access_flags;
} classes[number_of_classes];
}
其中, attribute_name_index表示属性名称,为指向常量池的索引,这里恒为“TnnerClasses ” 。attribute_ length 为属性长度,number_of_classes 表示内部类的个数。classes[number_ of_ classes]为描述内部类的表格,每一条内部类记录包含4个字段,其中,inner_class_ info_ index为指向常量池的指针, 它指向一个CONSTANT_ Class_info, 表示内部类的类型。outer_class_ info_index表示外部类类型,也是常量池的索引。inner_name_ index 表示内部类的名称, 指向常量池中的CONSTANT_ Utf8_info项。最后的inner_class_access_flags为内部类的访问标识符, 用于指示static、public等属性。
内部内标识符表如下:
标记名 | 值 | 含义 |
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | 私有类 |
ACC_PROTECTED | 0x0004 | 受保护的类 |
ACC_STATIC | 0x0008 | 静态内部类 |
ACC_FINAL | 0x0010 | fmal类 |
ACC_INTERFACE | 0x0200 | 接口 |
ACC_ABSTRACT | 0x0400 | 抽象类 |
ACC_SYNTHETIC | 0x1000 | 编译器产生的,而非代码产生的类 |
ACC_ANNOTATION | 0x2000 | 注释 |
ACC_ENUM | 0x4000 | 枚举 |
2.10.9 ConstantValue属性——static(常量)标识
ConstantValue属性位于filed_info属性中,通知虚拟机自动为静态变量赋值,只有被static字符修饰的变量(类变量)才可以有这项属性。如果非静态字段拥有了这一属性,也该属性会被虚拟机所忽略。
对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性。但是不同虚拟机有不同的实现。
目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,即编译的时候;如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化,即类加载的时候。
ConstantValue属性结构如下:
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
其中, attribute_name_index表示属性名称,为指向常量池的索引,这里恒为“ConstantValue_attribute ” 。attribute_length表示后面还有几个字节,这里固定为2。constantvalue_index代表了常量池中一个字面常量的引用,根据字段类型不同,字面量可以是CONSTANT_Long_info、 CONSTANT_Float_info、 CONSTANT_Double_info、CONSTANT_lnteger_info、CONSTANT_String_info常量中的一种。
2.10.11 Signature属性
一个可选的定长属性,可以出现于ClassFile, field_info, method_info结构的属性表中。
任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。
Signature属性结构如下:
Signature_attribute {
u2 attribute_name_index; //指向常量池的名称引用,固定为“Signature”
u4 attribute_length; // 固定为2
u2 signature_index; //指向常量池的类签名、方法类型前面、字段类型签名引用
}
2.10.12 Deprecated属性——废弃
标志类型的布尔属性,Deprecated表示类、方法、字段不再推荐使用,使用注解表示为@deprecated
2.10.13 Synthetic属性——编译器添加
标志类型的布尔属性,Synthetic属性用在ClassFile, field_info, method_info中,表示此字段或方法或类是由编译器自行添加的。
对于其他属性,可以查看官方文档:The Java® Virtual Machine Specification——Java SE 8 Edition
2.10.14 测试
2.10.14.1 Code属性
我们接着上面的Class文件向下看,我们知道< init >方法中有一个Code属性,Code属性结构为:
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为0x000f,即指向常量池第15个常量。
该属性名为Code!
attribute_length 表示Code属性剩余长度 ,0x0000002f,即47个字节。
接着看后两个属性:
max_stack为1,max_locals也为1,即方法内部没调用其他方法。
接着看后两个属性,即字节码部分:
code_length 表示字节码的长度,这里是5, **code[code length]**为byte数组, 为字节码内容本身,长度为5个字节:
继续向下,该方法实际上是用于对象创建的方法,实际上是没有异常处理表的,因此长度自然为0,下面的数组部分分字节码为0,如下图:
exception_table_length为0x0000,即0,那么就不会出现exception_table,继续向下两个字节,表示Code属性的属性数量,即attributes_count:
attributes_count值为0x0002,即有Code属性内部有两个属性,下面我们来看看是哪两个属性!
2.10.14.2 Code内部第一个属性
老样子,向后走六个字节:
Code属性内部的第一个属性的attribute_name_index为0x0010,即第16个常量;
Code属性内部的第一个属性的attribute_length为0x00000006,即后接6字节;
通过查找javap的常量池可知,第16个常量属性就是LineNumberTable属性。
我们知道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];
}
继续,向下走六个字节,查看属性后面部分:
line_number_table_length为0x0001,即1,即line_number_table表的实际内容为1个数据。下面进入这个数据内部:
start_pc为0x0000,即字节码偏移量为0。
line_number为0x0004,即对应的行号为4(反编译Class就会出现无参构造器,对应行号就是4)。
到此,Code属性内部的第一个属性LineNumberTable结束。
2.10.14.3 Code内部第二个属性
老样子,第二个属性先向后取六位字节:
Code属性内部的第二个属性的attribute_name_index为0x0011,即第17个常量;
Code属性内部的第二个属性的attribute_length为0x0000000c,即后接12字节;
通过查找javap的常量池可知,第17个常量属性就是LocalVariableTable属性。
我们知道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_length为0x0001,即1,即局部变量表表项条目为1条,后面一条数据,刚好十个占据字节,进入local_variable_table的数据:
start_pc为0x0000,即0,局部变量开始位置为0,即占用0位solt,实际上这个局部变量就是this(在运行时栈结构部分会有讲解)。
length为0x0005,即范围覆盖长度为5。
name_index为0x0012,即第18个常量。
可以看到这个局部变量名叫this,这个变量是虚拟机为我们加到实例方法中的,可以直接使用,但是对于静态方法,却没有这个局部变量。
descriptor_index为0x0013,即第19个常量。
表示局部变量的类型描述,可以看到,该this变量的类型就是该类的类型。
index为0x0000,即0,即this变量,所占用的solt槽位是0。
到此,Code的两个额外属性寻找完毕,刚好47个字节,该< init >方法的额外属性Code寻找完毕,该方法寻找完毕!
2.10.14.4 验证
下面来验证我们上面的分析:
我们的javap -v ClassFile.class 指令实际上也编译出了方法表,我们找到init方法:
可以看到,我们通过计算class文件得到的信息和javap指令反编译得到的信息是一致的!
对于后面的三个方法,就是setK、getK、main,不过是依葫芦画瓢,找到之后,得出结果,然后和javap的结果进行对比,看是不是正确的,在此不再赘述。
在最后,实际上最后8个字节表示的是SourseFile属性,你们可以自己看看到底是不是!
3 总结
到此,我已经带领大家把Class文件从头到尾的大概梳理了一遍,以后遇到更加复杂的类,实际上可以直接使用javap指令查看反解析出的数据,那样更加方便。
关于javap指令和字节码指令的查看,这里没有讲解,可以参考官网:Verification of class Files,后面我也会处相应的中文教程。我们知道了Class文件的结构组成,对于我们后续的JVM深入学习是很有帮助的,比如编译器的学习(这篇文章只是讲解编译之后的数据结构),我们以后可以自己实现一个Java编译器!