- 类文件结构
- 一 概述
- 二 Class 文件结构总结
- 2.1 魔数(Magic Number)
- 2.2 Class 文件版本号(Minor&Major Version)
- 2.3 常量池(Constant Pool)
- 2.4 访问标志(Access Flags)
- 2.5 当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合
- 2.6 字段表集合(Fields)
- 2.7 方法表集合(Methods)
- 2.8 属性表集合(Attributes)
一 概述
在 Java 中,JVM 可以理解的代码就叫做字节码
(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
Clojure(Lisp 语言的一种方言)、Groovy、Scala 等语言都是运行在 Java 虚拟机之上。下图展示了不同的语言被不同的编译器编译成.class
文件最终运行在 Java 虚拟机之上。.class
文件的二进制格式可以使用 WinHex 查看。
可以说.class
文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。
二 Class 文件结构总结
根据 Java 虚拟机规范,Class 文件通过 ClassFile
定义,有点类似 C 语言的结构体。
ClassFile
的结构如下:
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的次版本号
u2 major_version;//Class 的主版本号
u2 constant_pool_count;//常量池的数据数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类会可以有多个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
通过分析 ClassFile
的内容,我们便可以知道 class 文件的组成。
下面这张图是通过 IDEA 插件 jclasslib
查看的,你可以更直观看到 Class 文件结构。
使用 jclasslib
不光可以直观地查看某个类对应的字节码文件,还可以查看类的基本信息、常量池、接口、属性、函数等信息。
下面详细介绍一下 Class 文件结构涉及到的一些组件。
2.1 魔数(Magic Number)
u4 magic; //Class 文件的标志
每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。
程序设计者很多时候都喜欢用一些特殊的数字表示固定的文件类型或者其它特殊的含义。
2.2 Class 文件版本号(Minor&Major Version)
u2 minor_version;//Class 的次版本号
u2 major_version;//Class 的主版本号
紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 位是次版本号,第 7 和第 8 位是主版本号。
每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 javap -v
命令来快速查看 Class 文件的版本号信息。
高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。
2.3 常量池(Constant Pool)
u2 constant_pool_count;//常量池的容量计数值
cp_info constant_pool[constant_pool_count-1];//常量池
紧接着主次版本号之后的是常量池,常量池的容量计数值是 constant_pool_count-1
(常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”)。
常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:开始的第一位是一个 u1 类型的标志位 -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 | 表示一个动态方法调用点 |
.class
文件可以通过javap -v class类名
指令来看一下其常量池中的信息(javap -v class类名-> temp.txt
:将结果输出到 temp.txt 文件)。
2.4 访问标志(Access Flags)
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public
或者 abstract
类型,如果是类的话是否声明为 final
等等。
类访问和属性修饰符:
我们定义了一个 Employee 类
package top.snailclimb.bean;
public class Employee {
...
}
通过javap -v class类名
指令来看一下类的访问标志。
2.5 当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object
之外,所有的 java 类都有父类,因此除了 java.lang.Object
外,所有 Java 类的父类索引都不为 0。
接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implements
(如果这个类本身是接口的话则是extends
) 后的接口顺序从左到右排列在接口索引集合中。
2.6 字段表集合(Fields)
u2 fields_count;//Class 文件的字段的个数
field_info fields[fields_count];//一个类会可以有个字段
字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
field info(字段表) 的结构:
- access_flags: 字段的作用域(
public
,private
,protected
修饰符),是实例变量还是类变量(static
修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。 - name_index: 对常量池的引用,表示的字段的名称;
- descriptor_index: 对常量池的引用,表示字段和方法的描述符;
- attributes_count: 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数;
- attributes[attributes_count]: 存放具体属性具体内容。
上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中常量来描述。
字段的 access_flag 的取值:
2.7 方法表集合(Methods)
u2 methods_count;//Class 文件的方法的数量
method_info methods[methods_count];//一个类可以有个多个方法
methods_count 表示方法的数量,而 method_info 表示方法表。
Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
method_info(方法表的) 结构:
方法表的 access_flag 取值:
注意:因为volatile
修饰符和transient
修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了synchronized
、native
、abstract
等关键字修饰方法,所以也就多了这些关键字对应的标志。
2.8 属性表集合(Attributes)
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
attribute_info(属性表的) 结构:
2.8.1 Code属性
java中方法体在经过编译之后最终以字节的形式存放在Code属性内。Code属性出现在方法表的属性集合之中(不包括抽象类或接口的方法),Code属性表结构如下:
- attribute_name_index:一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为”Code“,它代表该属性的属性名称。
- attribute_length:属性值的长度,由于属性名称索引与属性长度一共6个字节,所以属性值长度=属性表长度-6
- max_stack:代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个最大值。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度
- max_locals:局部变量表所需要的存储空间。单位是Slot
- code_length:字节码长度
- code:编译后生成的字节码指令
- exception_table:包含4个字段(start_pc、end_pc、handler_pc、catch_type)。这些字段的含义是:当字节码在start_pc行到end_pc行之间(try的范围)出现了类型为catch_type的异常或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则跳转到handler_pc行继续处理。
2.8.2 Exceptions属性
Exception属性是和Code属性平级的一项属性,它的结构如下表所示:
number_of_exceptions表示方法可能抛出number_of_exceptions中受查异常,每一种受查异常用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型
2.8.3 LineNumberTable属性
LineNumberTable属性用于描述java源码行号和字节码行号之间的对应关系。它并不是运行时必须的属性,但默认会生成到Class文件中,可以再javac中分别使用-g:none或-g:lines选项取消或要求生成这项信息。如果选择不生成LineNumberTable属性,当程序抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行设置断点,其结构如下表所示:
line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包含了start_pc和line_number两个u2类型的数据项,前者表示字节码行号,后者表示java源码行号
2.8.4 LocalVariableTable属性
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与java源码中定义的变量的关系,它也不是运行时必须数据,但默认会生成在Class文件中。可以在javac时使用-g:none或-g:lines来取消或要求生成这项信息。如果不生成这个信息,当其他人引入这个方法时,所有参数名称会丢失,IDE会使用诸如arg1、arg2之类的占位符替代原有的参数名,这对程序运行没有影响,但是对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获取参数值。其结构如下表所示:
其中local_variable_info 项代表了一个栈帧与源码中局部变量的关联,local_variable_info表结构如下所示:
- start_pc:局部变量的生命周期开始的字节码偏移量
- length:局部变量在生命周四开始的字节码的作用范围覆盖长度
- name_index:指向常量池中CONSTANT_Utf8_info型常量的索引,代表局部变量的名称
- descriptor_index:指向常量池中CONSTANT_Utf8_info型常量的索引,代表局部变量的描述符
- index:这个局部变量在栈帧局部变量表中Slot的位置
2.8.5 SourceFile属性
SourceFile属性用于记录生成这个Class文件的源码名称。这个属性也是可选的,可以分别使用javac 的 -g :none或-g:source来关闭或要求生成这项信息。如果不生成这项信息,当抛出异常,堆栈中将不会显示出错代码所属的文件名。这个属性是一个定长的属性,其结构如下:
表18、SourceFile属性表结构
sourcefile_index数据项时指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件名
2.8.6 ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static修饰的变量才可以使用这个属性。
2.8.7 InnerClasses属性
InnerClasses属性用于记录内部类和宿主类之间的关系。如果一个类中定义了内部类,那编译器将会为它以及它包含的内部类生成InnerClasses属性,该属性结构如下图所示:
表19、InnerClasses属性表结构
数据项number_of_classes代表需要记录多少个内部类信息,每个内部类的信息都由一个inner_classes_info表进行描述,inner_classes_info表结构如下:
- inner_class_info_index:指向常量池中CONSTANT_Class_info类型常量的索引,代表内部类的符号引用
- outer_class_info_index:指向常量池中CONSTANT_Class_info类型常量的索引,代表宿主类的符号引用
- inener_name_index:指向常量池中CONSTANT_Utf8_info型常量的索引,代表内部类的名称,如果是匿名内部类,那么这项值为0
- inner_class_access_flags:内部类的访问标志,它的取值范围见下表:
2.8.8 Deprecated及Synthetic属性
Deprecated及Synthetic属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值概念。Deprecated属性用于表示某个类、字段或方法,它可以通过在代码中使用@deprecated注释进行设置。Synthetic属性代表此字段或方法不是由java源码直接产生,而是由编译器自行添加的。Deprecated和Synthetic属性的结构如下:
2.8.9 StackMapTable属性
StackMapTable属性在JDK1.6发布后增加到Class文件规范中,它是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机的类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于替代以前比较消耗性能的基于数据流分析的类型推导验证器。StackMapTable属性中包含零至多个栈映射帧(Stack Map Frames),每个栈帧射帧都显式或隐式的代表了一个字节码的偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型。类型检查器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定指令是否符合逻辑的约束。其结构如下:
2.8.10 Signature属性
Signature属性在JDK1.5发布后增加到Class文件规范中,它是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中。在任何类、接口、初始化方法或成员的泛型简明中如果包含了类型变量()或参数化类型(),则Signature属性会为它记录泛型签名信息。Signature属性的结构如下所示:
- signature_index:必须是一个对常量池的有效索引。常量池在该索引未知的项必须是CONSTANT_Utf8_info结构,表示类签名、方法类型签名或字段类型签名。 如果当前的Signature属性是类文件的属性,则表示是类签名;如果是方法表的属性则表示是方法类型签名;如果是字段表属性则说明是字段类型签名。
2.8.11 BootstrapMethods属性
BootstrapMethods属性是在JDK1.7发布后增加到CLass文件规范中,它是一个复杂的变长属性,位于类文件表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。如果某个类文件的常量池中曾经出现过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确的BootstrapMethods属性,另外,即使CONSTANT_InvokeDynamic_info类型的常量在常量池中出现过多次,最多也只能有一个BootstrapMethods属性。BootstrapMethods属性结构如下:
其中引用到的bootstrap_method结构如下:
参考
- https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
- https://coolshell.cn/articles/9229.html
- 《实战 Java 虚拟机》