一, 概述
各种不同平台的 Java 虚拟机, 以及所有平台都统一支持的程序存储格式 -- 字节码 (Byte Code) 是构成平台无关性的基石, 所以 class 文件主要用于解决平台无关性的中间文件. 如下图所示:
java 虚拟机不与包括 Java 语言在内的任何程序语言绑定, 它只与 "Class 文件" 这种特定的二进制文件格式所关联, Class 文件中包含了 Java 虚拟机指令集, 符号表以及若干其他辅助信息.
每一个 class 文件都对应着唯一一个类或者接口的定义信息, 但是相对地, 类或者接口并不一定都必须定义在文件里(比如类或者接口也可以通过类加载器直接生成)
每个 class 文件都是由字节流组成, 各个数据项目严格按照顺序紧凑地排列在文件之中, 中间没有添加任何分隔符, 每个字节流含有 8 个二进制位, 所有的 16 位, 32 位和 64 位长度的数据将通过 2 个, 4 个和 8 个连续的 8 位字节来对其进行表示, 多字节数据总是按照 big-endian(大端在前: 也就是说高位字节存储在低的地址上面, 而低位字节存储到高地址上面)的顺序进行存储, 在 Java JDK 中, 可以使用 java.io.DataInput,java.io.DataOutput 等接口和 java.io.DataInputStream 和 java.io.DataOutputStream 等类来访问这种格式的数据 Class 文件结构采用类似 C 语言的结构体来存储数据的.
Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据, 主要有两类数据项, 无符号数和表, 无符号数用来表述数字, 索引引用以及字符串等, 比如 u1,u2,u4,u8 分别代表 1 个字节, 2 个字节, 4 个字节, 8 个字节的无符号数, 而表是任意数量的可变长项组成, 是有多个无符号数以及其它的表组成的复合结构, 所有表的命名都习惯性地以 "_info" 结尾, 无论是无符号数还是表, 当需要描述同一类型但数量不定的多个数据时, 经常会使用一个前置的容量计数器加若干个连续的数据项的形式, 这时候称这一系列连续的某一类型的数据为某一类型的 "集合".
二, Class 类文件的结构
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
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 |
2.1, 魔数和 java 版本号
每个 Class 文件的头 4 个字节被称为魔数(Magic Number) , 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件. Class 文件的魔数取得很有 "浪漫气息",
值为 0xCAFEBABE(咖啡宝贝? )
紧接着魔数的 4 个字节存储的是 Class 文件的版本号: 第 5 和第 6 个字节是次版本号(MinorVersion) , 第 7 和第 8 个字节是主版本号(Major Version)
这里我们使用一个简单的代码进行分析:
- public class TestClass {
- private int m;
- public int inc() {
- return m + 1;
- }
- }
使用 javac 命令对其进行编译, 并使用 WinHex (下载地址: http://www.x-ways.net/winhex/index-m.html)工具打开, 得到如下的图, 前面几位就是魔数和版本号
这里可以得出我们使用的版本为 java1.8,16 进制的 34 等于 10 进制的 52
2.2, 常量池
紧接着主, 次版本号之后的是常量池入口, 常量池可以比喻为 Class 文件里的资源仓库, 它是 Class 文件结构中与其他项目关联最多的数据, 通常也是占用 Class 文件空间最大的数据项目之一, 另外, 它还是在 Class 文件中第一个出现的表类型数据项目 , 常量池的入口需要放置一项 u2 类型的数据, 代表常量池容量计数值(constant_pool_count) , 这个容量计数是从 1 开始的. 如下图所示: 常量池容量(偏移地址: 0x00000008) 为十六进制数 0x0013, 则十进制为 19, 则这里有 18 个长常量, 索引范围为 1-18, 在 Class 文件格式规范制定之时, 设计者将第 0 项常量空出来是有特殊考虑的, 这样做的目的在于, 如果后面某些指向常量池的索引值的数据在特定情况下需要表达 "不引用任何一个常量池项目" 的含义, 可以把索引值设置为 0 来表示.
然后我们使用 javap 命令查看该 class 文件:(这里明显显示为 18 个常量)
常量池中主要存放两大类常量: 字面量(Literal) 和符号引用(Symbolic References) .
字面量比较接近于 Java 语言层面的常量概念, 如文本字符串, 被声明为 final 的常量值等.
符号引用则属于编译原理方面的概念, 主要包括下面几类常量:
被模块导出或者开放的包(Package)
类和接口的全限定名(Fully Qualified Name)
字段的名称和描述符(Descriptor)
方法的名称和描述符
方法句柄和方法类型(Method Handle, Method Type, Invoke Dynamic)
动态调用点和动态常量(Dynamically-Computed Call Site, Dynamically-Computed Constant)
虚拟机在加载 Class 文件时才会进行动态连接, 也就是说, Class 文件中不会保存各个方法, 字段最终在内存中的布局信息, 这些字段, 方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址, 也就无法直接被虚拟机使用的, 当虚拟机做类加载时, 将会从常量池获得对应的符号引用, 再在类创建时或运行时解析, 翻译到具体的内存地址之中常量池中每一项常量都是一个表, 截至 JDK13, 常量表中分别有 17 种不同类型的常量. 这 17 类表都有一个共同的特点, 表结构起始的第一位是个 u1 类型的标志位, 代表着当前常量属于哪种常量类型. 17 种常量类型所代表的具体含义如下图所示.
符号引用: 符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能无歧义地定位到目标即可. 符号引用与虚拟机实现的内存布局无关, 引用的目标并不一定已经加载到了内存中.
直接引用: 直接引用可以是直接指向目标的指针, 相对偏移量或是一个能间接定位到目标的句柄. 直接引用是与虚拟机实现的内存布局相关的, 同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同. 如果有了直接引用, 那说明引用的目标必定已经存在于内存之中了
类型 | 项目 | 类型 | 描述 |
CONSTANT_Utf8_info | tag | u1 | 值为 1 |
length | u2 | utf-8 缩略编码字符串占用字节数 | |
bytes | 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 |
index | u2 | 指向全限定名常量项的索引 | |
CONSTANT_String_info | tag | u1 | 值为 8 |
index | u2 | 指向字符串字面量的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值为 9 |
index | u2 | 指向声明字段的类或接口描述符 CONSTANT_Class_info 的索引项 | |
index | u2 | 指向字段描述符 CONSTANT_NameAndType_info 的索引项 | |
CONSTANT_Methodref_info | tag | u1 | 值为 10 |
index | u2 | 指向声明方法的类描述符 CONSTANT_Class_info 的索引项 | |
index | u2 | 指向名称及类型描述符 CONSTANT_NameAndType_info 的索引项 | |
CONSTANT_InterfaceMethodref_info | tag | u1 | 值为 11 |
index | u2 | 指向声明方法的接口描述符 CONSTANT_Class_info 的索引项 | |
index | u2 | 指向名称及类型描述符 CONSTANT_NameAndType_info 的索引项 | |
CONSTANT_NameAndType_info
| tag | u1 | 值为 12 |
index | u2 | 指向该字段或方法名称常量项的索引 | |
index | u2 | 指向该字段或方法描述符常量项的索引 | |
CONSTANT_MethodHandle_info | tag | u1 | 值为 15 |
refrence_kind | u1 | 值必须在 1-9 之间,决定了方法句柄的类型,方法句柄的类型的值表示方法句柄字节码的行为 | |
refrence_index | u2 | 值必须是对常量池的有效索引 | |
CONSTANT_MethodType_info | tag | u1 | 值为 16 |
descriptor_index | u2 | 值必须对常量池的有效索引,常量池在该处的项必须是 CONSTANT_Utf8_info 表示方法的描述符 | |
CONSTANT_Dynamic_info | tab | u1 | 值为 17 |
bootstrap_method_attr_index | u2 | 值必须对当前 Class 文件中引导方法表的 bootstrap_methods[] 数组的有效索引 | |
name_and_type_index | u2 | 值必须对当前常量池的有效索引,常量池中在该索引出的项必须是 CONSTANT_NameAndType_info 结构,表示方法名和方法描述符 | |
CONSTANT_InvokeDynamic_info | tag | u1 | 值为 18 |
bootstrap_method_attr_index | u2 | 值必须对当前 Class 文件中引导方法表的 bootstrap_methods[] 数组的有效索引 | |
name_and_type_index | u2 | 值必须对当前常量池的有效索引,常量池中在该索引出的项必须是 CONSTANT_NameAndType_info 结构,表示方法名和方法描述符 | |
CONSTANT_Module_info | tag | u1 | 值为 19 |
name_index | u2 | 值必须对常量池的有效索引,常量池在该处的项必须是 CONSTANT_Utf8_info 表示模块名 | |
CONSTANT_Package_info | tag | u1 | 值为 20 |
name_index | u2 | 值必须对常量池的有效索引,常量池在该处的项必须是 CONSTANT_Utf8_info 表示包名 |
2.3, 访问标志
在常量池结束之后, 紧接着的两个字节代表访问标志(access_flags), 这个标志用于识别一些类或者接口层次的访问信息, 包括: 这个 Class 是类还是接口; 是否定义为 public 类型; 是否定义为 abstract 类型, 如果是类的话, 是否被声明为 final 等, 具体的标志位以及标志的含义如下:
字段的访问权限 | ||
Flag Name | Value | Remarks |
ACC_PUBLIC | 0x0001 | pubilc,包外可访问。 |
ACC_PRIVATE | 0x0002 | private,只可在类内访问。 |
ACC_PROTECTED | 0x0004 | protected,类内和子类中可访问。 |
ACC_STATIC | 0x0008 | static,静态。 |
ACC_FINAL | 0x0010 | final,常量。 |
ACC_VOILATIE | 0x0040 | volatile,直接读写内存,不可被缓存。不可和 ACC_FINAL 一起使用。 |
ACC_TRANSIENT | 0x0080 | transient,在序列化中被忽略的字段。 |
ACC_SYNTHETIC | 0x1000 | synthetic,由编译器产生,不存在于源代码中。 |
ACC_ENUM | 0x4000 | enum,枚举类型字段 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
2.4, 类索引, 父类索引与接口索引集合
类索引(this_class) 和父类索引(super_class) 都是一个 u2 类型的数据, 而接口索引集合(interfaces) 是一组 u2 类型的数据的集合, Class 文件中由这三项数据来确定该类型的继承关系. 类索引用于确定这个类的全限定名, 父类索引用于确定这个类的父类的全限定名. 由于 Java 语言不允许多重继承, 所以父类索引只有一个, 除了 java.lang.Object 之外, 所有的 Java 类都有父类, 因此除了 java.lang.Object 外, 所有 Java 类的父类索引都不为 0. 接口索引集合就用来描述这个类实现了哪些接口, 这些被实现的接口将按 implements 关键字(如果这个 Class 文件表示的是一个接口, 则应当是 extends 关键字) 后的接口顺序从左到右排列在接口索引集合中.
2.5, 字段表集合
字段表(field_info) 用于描述接口或者类中声明的变量. Java 语言中的 "字段"(Field) 包括类级变量以及实例级变量, 但不包括在方法内部声明的局部变量. 字段可以包括的修饰符有字段的作用域(public, private, protected 修饰符) , 是实例变量还是类变量(static 修饰符) , 可变性(final) , 并发可见性(volatile 修饰符, 是否强制从主内存读写) , 可否被序列化(transient 修饰符) , 字段数据类型(基本类型, 对象, 数组) , 字段名称. 上述这些信息中, 各个修饰符都是布尔值, 要么有某个修饰符, 要么没有, 很适合使用标志位来表示. 而字段叫做什么名字, 字段被定义为什么数据类型, 这些都是无法固定的, 只能引用常量池中的常量来描述. 字段表的最终格式如下.
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
而字段修饰符放在 access_flags 项目中, 它与类中的 access_flags 项目是非常类似的, 都是一个 u2 的数据类型, 其中可以设置的标志位和含义如下所示:
标志名称 | 标志值 | 含义 |
---|---|---|
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_TRANSIENT | 0x0080 | 字段是否为 transient |
ACC_ABSTRACT | 0x0400 | 字段是否为 abstract |
ACC_SYNTHETIC | 0x1000 | 字段是否为编译器自动产生 |
name_index 和 descriptor_index. 它们都是对常量池项的引用, 分别代表着字段的简单名称以及字段和方法的描述符.
全限定名: 仅仅是把类全名中的 "." 替换成了 "/" 而已, 例如类名 org.apache.xxxx, 器全限定名为 org/apache/xxxx.
简单名称: 就是指没有类型和参数修饰的方法或者字段名称, 比如类中的 inc()方法和 m 字段的简单名称分别就是 "inc" 和 "m".
方法和字段的描述符: 描述符的作用是用来描述字段的数据类型, 方法的参数列表(包括数量, 类型以及顺序) 和返回值. 根据描述符规则, 基本数据类型(byte, char, double, float, int, long, short, boolean) 以及代表无返回值的 void 类型都用一个大写字符来表示, 而对象类型则用字符 L 加对象的全限定名来表示, 祥见下表:
标识字符 | 含义 |
B | 基本类型 byte |
C | 基本类型 char |
D | 基本类型 double |
F | 基本类型 float |
I | 基本类型 int |
J | 基本类型 long |
S | 基本类型 short |
Z | 基本类型 boolean |
V | 特殊类型 void |
L | 对象类型, 如 java/lang/Object |
对于数组类型, 每一维度将使用一个前置的 "[" 字符来描述, 如一个定义为 "java.lang.String[][]" 类型的二维数组将被记录成 "[[Ljava/lang/String;", 一个整型数组 "int[]" 将被记录成 "[I"
用描述符来描述方法时, 按照先参数列表, 后返回值的顺序描述, 参数列表按照参数的严格顺序放在一组小括号 "()" 之内. 如方法 void inc()的描述符为 "()V", 方法 java.lang.String toString()的描述符为 "()Ljava/lang/String;", 方法 int indexOf(char[]source, int sourceOffset, int sourceCount, char[]target,int targetOffset, int targetCount, int fromIndex)的描述符为 "([CII[CIII)I"
2.6, 方法表集合
Class 文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式, 方法表的结构如同字段表一样, 依次包括访问标志(access_flags) , 名称索引(name_index) , 描述符索引(descriptor_index) , 属性表集合(attributes) 几项, 如下图所示
在访问标志和属性表集合的可选项中有所区别, 因为 volatile 关键字和 transient 关键字不能修饰方法, 所以方法表的访问标志中没有了 ACC_VOLATILE 标志和 ACC_TRANSIENT 标志. 与之相对, synchronized, native, strictfp 和 abstract 关键字可以修饰方法, 方法表的访问标志中也相应地增加了 ACC_SYNCHRONIZED,ACC_NATIVE, ACC_STRICTFP 和 ACC_ABSTRACT 标志.
2.7, 属性表集合
1,code 属性
方法的定义可以通过访问标志, 名称索引, 描述符索引来表达清楚, 但方法里面的代码去哪里了? 方法里的 Java 代码, 经过 Javac 编译器编译成字节码指令之后, 存放在方法属性表集合中一个名为 "Code" 的属性里面, 属性表作为 Class 文件格式中最具扩展性的一种数据项目,
java 程序方法体里面的代码经过 Javac 编译器处理之后, 最终变为字节码指令存储在 Code 属性内. Code 属性出现在方法表的属性集合之中, 但并非所有的方法表都必须存在这个属性, 譬如接口或者抽象类中的方法就不存在 Code 属性.
Code 属性是 Class 文件中最重要的一个属性, 如果把一个 Java 程序中的信息分为代码(Code, 方法体里面的 Java 代码) 和元数据(Metadata, 包括类, 字段, 方法定义及其他信息) 两部分, 那么在整个 Class 文件里, Code 属性用于描述代码, 所有的其他数据项目都用于描述元数据.
2,Exceptions 属性
Exceptions 属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons) , 也就是方法描述时在 throws 关键字后面列举的异常.
3,LineNumberTable 属性
LineNumberTable 属性用于描述 Java 源码行号与字节码行号(字节码的偏移量) 之间的对应关系. 并不是运行时必需的属性, 但默认会生成到 Class 文件之中, 可以在 Javac 中使用 - g: none 或 - g: lines 选项来取消或要求生成这项信息.
4,LocalVariableTable 及 LocalVariableTypeTable 属性
LocalVariableTable 属性用于描述栈帧中局部变量表的变量与 Java 源码中定义的变量之间的关系, 它也不是运行时必需的属性, 但默认会生成到 Class 文件之中, 可以在 Javac 中使用 - g: none 或 - g: vars 选项来取消或要求生成这项信息
5,SourceFile 及 SourceDebugExtension 属性
SourceFile 属性用于记录生成这个 Class 文件的源码文件名称. 这个属性也是可选的, 可以使用 Javac 的 - g: none 或 - g: source 选项来关闭或要求生成这项信息. 在 Java 中, 对于大多数的类来说, 类名和文件名是一致的, 但是有一些特殊情况(如内部类) 例外
SourceDebugExtension 属性用于存储额外的代码调试信息. 典型的场景是在进行 JSP 文件调试时, 无法通过 Java 堆栈来定位到 JSP 文件的行号.
6,ConstantValue 属性
ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值. 只有被 static 关键字修饰的变量 (类变量) 才可以使用这项属性. 类似 "int x=123" 和 "static int x=123" 这样的变量定义在 Java 程序里面是非常常见的事情, 但虚拟机对这两种变量赋值的方式和时刻都有所不同. 对非 static 类型的变量(也就是实例变量) 的赋值是在实例构造器 < init>() 方法中进行的; 而对于类变量, 则有两种方式可以选择: 在类构造器 < clinit>()方法中或者使用 ConstantValue 属性.
7,InnerClasses 属性
InnerClasses 属性用于记录内部类与宿主类之间的关联. 如果一个类中定义了内部类, 那编译器将会为它以及它所包含的内部类生成 InnerClasses 属性
8,Deprecated 及 Synthetic 属性
Deprecated 和 Synthetic 两个属性都属于标志类型的布尔属性, 只存在有和没有的区别, 没有属性值的概念.
Deprecated 属性用于表示某个类, 字段或者方法, 已经被程序作者定为不再推荐使用, 它可以通过代码中使用 "@deprecated" 注解进行设置
Synthetic 属性代表此字段或者方法并不是由 Java 源码直接产生的, 而是由编译器自行添加的, 在 JDK 5 之后, 标识一个类, 字段或者方法是编译器自动产生的, 也可以设置它们访问标志中的 ACC_SYNTHETIC 标志位.
9,StackMapTable 属性
StackMapTable 是一个相当复杂的变长属性, 位于 Code 属性的属性表中. 这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(TypeChecker), 目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器.
StackMapTable 属性中包含零至多个栈映射帧(Stack Map Frame) , 每个栈映射帧都显式或隐式地代表了一个字节码偏移量, 用于表示执行到该字节码时局部变量表和操作数栈的验证类型. 类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束.
10,Signature 属性
Signature 属性是一个可选的定长属性, 可以出现于类, 字段表和方法表结构的属性表中. 任何类, 接口, 初始化方法或成员的泛型签名如果包含了类型变量(Type Variable) 或参数化类型(ParameterizedType) , 则 Signature 属性会为它记录泛型签名信息. 之所以要专门使用这样一个属性去记录泛型类型, 是因为 Java 语言的泛型采用的是擦除法实现的伪泛型, 字节码(Code 属性) 中所有的泛型信息编译(类型变量, 参数化类型) 在编译之后都通通被擦除掉.
11,BootstrapMethods 属性
BootstrapMethods 是一个复杂的变长属性, 位于类文件的属性表中. 这个属性用于保存 invokedynamic 指令引用的引导方法限定符.
12,MethodParameters 属性
MethodParameters 是一个用在方法表中的变长属性. MethodParameters 的作用是记录方法的各个形参名称和信息.
13, 模块化相关属性
JDK 9 的一个重量级功能是 Java 的模块化功能, 因为模块描述文件(module-info.java) 最终是要编译成一个独立的 Class 文件来存储的, 所以, Class 文件格式也扩展了 Module, ModulePackages 和 ModuleMainClass 三个属性用于支持 Java 模块化相关功能.
Module 属性是一个非常复杂的变长属性, 除了表示该模块的名称, 版本, 标志信息以外, 还存储了这个模块 requires, exports, opens, uses 和 provides 定义的全部内容,
ModulePackages 是另一个用于支持 Java 模块化的变长属性, 它用于描述该模块中所有的包, 不论是不是被 export 或者 open 的.
ModuleMainClass 属性是一个定长属性, 用于确定该模块的主类(Main Class)
参考:
《深入理解 java 虚拟机第三版》
来源: https://www.cnblogs.com/zsql/p/12907120.html