为了研究Class文件,先编写一个最简单的代码:java
package com.courage; public class T0100_ByteCode01 { }
之因此说最简单,是由于这个类里面任何方法,变量都没有,看看编译以后Class文件的16进制代码:安全
在解读上面的Class文件(后面没有特殊生命的话都是指16进制)以前,须要先学习几个前置知识,Java 虚拟机规范规定 Class 文件格式采用一种相似与 C 语言结构体的微结构体来存储数据,这种伪结构体中只有两种数据类型:无符号数和表。学习
有了无符号数这个概念,就能够根据虚拟机规范来解读上面的文件了:this
类型 | 名称 | 含义 | 数量 |
---|---|---|---|
u4 | magic | 魔数,不变 | 1 |
u2 | minor_version | 小版本号:JDK 8_255u中的255u | 1 |
u2 | major_version | 大版本号,JDK 8_255u中的8 | 1 |
u2 | constant_pool_count | 常量池数量 | 1 |
cp_info | constant_pool | 常量池 | constant_pool_count-1 |
u2 | access_flags | 访问修饰符 public static 等 | 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文件里面的属性都按照上表的规则排序,中间没有空行或其余转义字符。哪一个字节表明什么含义,长度是多少,前后顺序如何都是被严格限制的,不容许有任何改变。编码
每一个 Class 文件的头 4 个字节称为魔数(Magic Number),它的惟一做用是肯定这个文件是否为一个能被虚拟机接收的 Calss 文件。之因此使用魔数而不是文件后缀名来进行识别主要是基于安全性的考虑,由于文件后缀名是能够随意更改的。Class 文件的魔数值为「0xCAFEBABE」。3d
紧接着魔数的 4 个字节存储的是 Class 文件的版本号:第 5 和第 6 两个字节是次版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version)。高版本的 JDK 可以向下兼容低版本的 Class 文件,虚拟机会拒绝执行超过其版本号的 Class 文件。code
主版本号以后是常量池入口,常量池能够理解为 Class 文件之中的资源仓库,它是 Class 文件结构中与其余项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一,同是它仍是 Class 文件中第一个出现的表类型数据项目。blog
由于常量池中常量的数量是不固定的,因此在常量池入口须要放置一个 u2 类型的数据来表示常量池的容量「constant_pool_count」,和计算机科学中计数的方法不同,这个容量是从 1 开始而不是从 0 开始计数。之因此将第 0 项常量空出来是为了知足后面某些指向常量池的索引值的数据在特定状况下须要表达「不引用任何一个常量池项目」的含义,这种状况能够把索引值置为 0 来表示。排序
Class 文件结构中只有常量池的容量计数是从 1 开始的,其它集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数都是从 0 开始。继承
常量池中主要存放两大类常量:字面量和符号引用。
字面量比较接近 Java 语言层面的常量概念,如字符串、声明为 final 的常量值等。
符号引用属于编译原理方面的概念,包括了如下三类常量:
紧接着常量池以后的两个字节表明访问标志(access_flag),这个标志用于识别一些类或者接口层次的访问信息,包括这个 Class 是类仍是接口;是否认义为 public 类型;是否认义为 abstract 类型;若是是类的话,是否被申明为 final 等。具体的标志位以及标志的含义见下表:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否容许使用 invokespecial 字节码指令的新语意,invokespecial 指令的语意在 JKD 1.0.2 中发生过改变,微聊区别这条指令使用哪一种语意,JDK 1.0.2 编译出来的类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或者抽象类来讲,此标志值为真,其它类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并不是由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
access_flags 中一共有 16 个标志位可使用,当前只定义了其中的 8 个,没有使用到的标志位要求一概为 0。
类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据集合,Class 文件中由这三项数据来肯定这个类的继承关系。
字段表集合(field_info)用于描述接口或者类中声明的变量。字段(field)包括类变量和实例变量,但不包括方法内部声明的局部变量。下面咱们看看字段表的结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flag | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段修饰符放在 access_flags 中,它与类中的 access_flag 很是类似,都是一个 u2 的数据类型。
标志名称 | 标志值 | 含义 |
---|---|---|
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 |
Class 文件中对方法的描述和对字段的描述是彻底一致的,方法表中的结构和字段表的结构同样。
由于 volatile 关键字和 transient 关键字不能修饰方法,因此方法表的访问标志中没有 ACC_VOLATILE 和 ACC_TRANSIENT。与之相对的,synchronizes、native、strictfp 和 abstract 关键字能够修饰方法,因此方法表的访问标志中增长了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP 和 ACC_ABSTRACT 标志。
对于方法里的代码,通过编译器编译成字节码指令后,存放在方法属性表中一个名为「Code」的属性里面。
在 Class 文件、字段表、方法表中均可以携带本身的属性表(attribute_info)集合,用于描述某些场景专有的信息。
属性表集合不像 Class 文件中的其它数据项要求这么严格,不强制要求各属性表的顺序,而且只要不与已有属性名重复,任何人实现的编译器均可以向属性表中写入本身定义的属性信息,Java 虚拟机在运行时会略掉它不认识的属性。
下面就能够对类文件逐行分析了: