一.Class文件
注意:任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)也就是说实际上它并不一定以磁盘文件的形式存在。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
二.Class文件格式
名称 | 类型 | 数量 | 含义 |
---|---|---|---|
magic | u4 | 1 | 魔数 固定值0xCAFEBABE(咖啡宝贝) |
minor_version | u2 | 1 | 次版本号 |
major_version | u2 | 1 | 主版本号 |
constant_pool_count | u2 | 1 | 常量个数 |
constant_pool | cp_info | constant_pool_count - 1 | 具体的常量池内容 |
access_flags | u2 | 1 | 访问标识 |
this_class | u2 | 1 | 当前类索引 |
super_class | u2 | 1 | 父类索引 |
interfaces_count | u2 | 1 | 接口个数 |
interfaces | u2 | interfaces_count | 接口具体内容 |
fields_count | u2 | 1 | 字段个数 |
fields | field_info | fields_count | 具体的字段内容 |
methods_count | u2 | 1 | 方法个数 |
methods | method_info | methods_count | 具体的方法内容 |
attributes_count | u2 | 1 | 属性个数 |
attributes | attribute_info | attributes_count | 具体的属性内容 |
Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在上表中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
Magic(魔数)
每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。(很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或者jpeg等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。)
class文件的魔数值为0xCAFEBABE(咖啡宝贝)
主版本号和次版本号(major_version、minor_version)
第5,6个字节为次版本号,第7,8个字节为主版本号。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
以下面代码输出的class文件来说明:
public class TestClass{
private int m;
public int intc(){
return m + 1;
}
}
class文件内容
从该图中可以看出主版本号(major_version)为:0x0032 十进制数为52;说明这个文件可以被JDK1.8及以上版本的虚拟机执行。
常量池
紧接着主次版本号以后的是常量池入口,由于常量池中的常量的数量是不固定的,所以在常量池的入口放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count),从上图看出值为0x0013,十进制数为19。该计数是从1开始的,这就表示常量池中有18个常量。
常量池中的内容
主要存放两大类常量:字面量和符号引用。
字面量:就是固定值的表示法,对应java中的常量值。比如字符串常量“jvm”,final修饰的常量值(eg:1)
符号引用:属于编译原理方面的概念,包括下面三类常量:
- 类和接口的全限定名。
- 字段的名称和描述。
- 方法的名称和描述。
注意:Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池中每一项常量都是一个表。目前为止一共有14种,这些表都有一个共同的特点:开始的第一位都是一个u1类型的标志tag,用来表示这个常量属于那种常量类型。
标志 | 类型 | 描述 |
---|---|---|
1 | CONSTANT_Utf8_info | UTF-8编码字符串 |
3 | CONSTANT_Integer_info | 整型字面量 |
4 | CONSTANT_Float_info | 浮点型字面量 |
5 | CONSTANT_Long_info | 长整型字面量 |
6 | CONSTANT_Double_info | 双精度浮点型字面量 |
7 | CONSTANT_Class_info | 类或接口的符号引用 |
8 | CONSTANT_String_info | 字符串类型字面量 |
9 | CONSTANT_Fieldref_info | 字段的符号引用 |
10 | CONSTANT_Methodref_info | 类中方法的符号引用 |
11 | CONSTANT_InterfaceMethodref_info | 接口中方法的符号引用 |
12 | CONSTANT_NameAndType_info | 字段或方法的部分符号引用 |
15 | CONSTANT_MethodHandle_info | 标识方法句柄 |
16 | CONSTANT_MethodType_info | 标识方法类型 |
18 | CONSTANT_InvokeDtnamic_info | 表示一个动态方法调用点 |
第一个常量
从上面的class文件中看出第一个常量值是0x0A,对应十进制10,此类型的常量代表的是类中方法符号的引用(CONSTAWT_Methodref_info)。该表的结构如下:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值位CONSTANT_Methodref(10) |
u3 | class_index | 1 | 声明被引用方法的类的CONSTANT_Class_info入口的索引 |
u2 | name_and_type_index | 1 | 提供类CONSTANT_NameAndType_info入口的索引,该入口提供类方法的简单名称以及描述符 |
0x0004 #4 对应着常量池中的第四个常量。 0x000F #15 对应着常量池中第15个常量。
第二个常量
从上面的class文件中看出第二个常量值是0x09,对应十进制9,此类型的常量代表的是字段的符号引用(CONSTAWT_Fieldref_info)。
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值为CONSTANT_Fieldref(9) |
u2 | class_index | 1 | 生命被引用字段的类或者接口的CONSTANT_Class_info入口的索引 |
u3 | name_and_type_index | 1 | 提供类CONSTANT_NameAndType_info入口的索引,该入口提供类字段的简单名称以及描述符 |
0x0003 #3 对应着常量池中的第三个常量。 0x0010 #16 对应着常量池中第16个常量。
第三个常量
第三个常量的tag为0x07 #7 表示的是类或接口的符号引用。
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | name_index | 1 |
0x0011 #17 对应着第17个常量,依次类推。。。。
//使用命令输出常量表:javap -verbose class文件名
PS C:\Users\87874\Desktop\Test> javap -verbose TestClass
Classfile /C:/Users/87874/Desktop/Test/TestClass.class
Last modified 2019-3-7; size 276 bytes
MD5 checksum f9a5e4dad6af4facf787934c80215748
Compiled from "TestClass.java"
public class TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
//常量池
Constant pool:
// #表示这个十进制数 数字代表第几个常量 ,比如第5个常量的类型是utf8 内容是m ;
//第1个常量 类型是Methodref ,内容是第4和第15个常量的内容。通过“.”连接。
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // TestClass.m:I
#3 = Class #17 // TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 intc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 TestClass
#18 = Utf8 java/lang/Object
{
public TestClass();
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 1: 0
public int intc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 4: 0
}
CONSTANT_Utf8_info:
类型 | 名称 | 数量 |
---|---|---|
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
length值说明了这个UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。
注意:其中有一些常量似乎从来没有在代码中出现过,如“I”、“V”、“<init>”、“LineNumberTable”、“LocalVariableTable”等,这些看起来在代码任何一处都没有出现过的常量是哪里来的呢? 这部分自动生成的常量的确没有在Java代码里面直接出现过,但它们会被后面即将讲到的字段表(field_info)、方法表(method_info)、属性表(attribute_info)引用到,它们会用来描述一些不方便使用“固定字节”进行表达的内容。譬如描述方法的返回值是什么?有几个参数?每个参数的类型是什么?因为Java中的“类”是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。
访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为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 | 枚举类型 | 枚举 |
上面TestClass实例中 访问标志位:flags: ACC_PUBLIC, ACC_SUPER
,ACC_PUBLIC的值为0x0001,ACC_SUPER的值为0x0020,所以access_flags的值为:ACC_PUBLIC | ACC_PUBLIC = 0x0021。
类,父类,接口索引
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。索引用于确定这个类的全限定名,由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
上面的示例:
this_class = 0x0003 #3,对应着常量池中的第三个常量,内容为TestClass。
super_class = 0x0004 #4,对应着常量池中的第四个常量,内容为 java/lang/Object
interfaces_count = 0x0000 #0,接口个数为0,所以interfaces的没有也就没有。
字段表集合
fields:字段表集合,字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的局部变量。
在Java中一般通过如下几项描述一个字段:字段作用域(public、protected、private修饰符)、是类级别变量还是实例级别变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、可序列化与否(transient修饰符)、字段数据类型(基本类型、对象、数组)以及字段名称。在字段表中,变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示。
字段表集合中不会列出从超类或者父接口中继承而来的字段。
字段表结构:
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | access_flags | 1 | 修饰符标记位 |
u2 | name_index | 1 | 代表字段的简单名称,占2字节,是一个常量池的引用 |
u2 | descriptor_index | 1 | 代表字段的类型,占2个字节,是一个对常量池的引用 |
u2 | attributes_count | 1 | 属性技数器 |
attribute_info | attributes | attributes_count | 属性表集合 |
字段访问标志:
标志名称 | 标志值 | 含义 |
---|---|---|
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 |
上面的示例中,fields_count = 0x0001 #1,也就是说只有一个字段。下面看fields:
access_flags = 0x0002 #2 表示是private的。
name_index = 0x0005 #5 表示常量池中的第5个常量,内容为:m
descriptor_index = 0x0006 #6 表示常量池中的第5个常量,内容为:I(字段的类型)
adattributes_count = 0x0000 #0
方法表集合
方法表集合跟字段集合结构一样。
方法表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attribute_info表结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u1 | info | attribute_length |
在上面的示例中,methods_count = 0x0002 #2
表示有两个方法,一个是编译器添加的实例构造器<init>
方法,和我们自己的一个方法。
因为有两个方法,所以methods中有两个内容,先看第一个:
access_flags = 0x0001 #1
表示是public的。
name_index = 0x0007 #7
代表常量池中的第7个常量,内容为:<init>
descriptor_index = 0x0008 #8
常量池中的第8个常量,内容为:()V (表示方法类型)
adattributes_count = 0x0001 #1
: 表示该方法有一个属性
adattribute_name_index = 0x0009 #9
第9个常量 内容为Code(属性名字,该属性是方法的字节码描述)
adattribute_lenght = 0x0000
表示属性的长度为0。Info也就没有了。
。。。。。。
方法的定义可以通过访问标志、名称索引、描述符索引表达清楚,但方法里面的代码去哪里了?方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面。
属性表
虚拟机规范预定义的属性:
属性名称 | 使用位置 | 含义 | |
---|---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 | |
ConstantValue | 字段表 | final关键字定义的常量值 | 、 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 | |
Exceptions | 方法表 | 方法抛出的异常 | |
InnerClasses | 类文件 | 内部类列表 | |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 | |
LocalVariableTable | Code属性 | 方法的局部变量描述 | |
SourceFile | 类文件 | 源文件名称 | |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编辑器自动生成的 |
Code属性表的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
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 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
- attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为Code。
- attribute_length指示了属性的长度。
- max_stack代表了操作数栈深度的最大值,在方法执行的任意时刻,操作数栈不会大于这个深度。
-
max_locals代表了局部变量所需要的存储空间。max_locals单位是slot,slot是虚拟机为局部变量分配内存所使用的最小单位。对于
byte,char,float,int,short,boolean,reference,return Address
等长度不超过32位的数据类型,每个局部变量使用1个slot,而double和long这两种64位数据类型则使用2个slot。注意,slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占用的slot就可以被其它局部变量使用。 - code_length和code用于存储Java源程序编译后生成的字节码指令,code_length代表字节码长度,code用于存储字节码指令的一系列字节流。code由u1表示,u1的取值是0到255,也就是说一共可以表达255条指令。code有点类似cpu上的指令集,例如+号会被编译成 iadd 虚拟机字节码指令。code_length由一个u4表示,理论上最大值是232-1,但虚拟机规范中限制方法不能超过65535。
{
public TestClass();
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 1: 0
public int intc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 4: 0
}
网友评论