05类加载机制篇(D1_类文件结构)
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一 大步。
一、基本介绍
我们知道:“计算机只认识0和1,所以我们写的程序需要被编译器翻译成由0和1构成的二进制格式才能被计
算机执行。”
由于最近十年内虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展, 把我们编写的
程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语 言选择了与操作系统和
机器指令集无关的、平台中立的格式作为程序编译后的存储格式。
二、JVM的无关性
如果全世界所有计算机的指令集就只有x86一种,操作系统就只有Windows一种,
那也许就不会有Java语言的出现。
Java在刚刚诞生之时曾经提出过一个非常著名的宣传口号
“一次编写,到处运行 (Write Once,Run Anywhere)”。
“与平台无关”的理想最终只有实现在操作系统以上的应用层:
Oracle公司以及其他虚拟机发行商发布过许多可以运行在各种不同硬件平台和操作系统上的Java虚拟机,
这些虚拟机都可以载入和执行同一种平台无关的字节 码,从而实现了程序的“一次编写,到处运行”。
各种不同平台的Java虚拟机,以及所有平台都统一支持的程序存储格式字节码(Byte Code) 是构成平台无关性
的基石
Java虚拟机提供的语言无关性
- Java语言中的各种语法、关键字、常量变量和运算符号的语义最终都会由多条字节码指令组合来表达,这决定了字节码指令所能提供的语言描述能力必须比Java语言本身更加强大才行。
- 因此,有一些Java语言本身无法有效支持的语言特性并不代表在字节码中也无法有效表达出来,这为其他程序语言实现一些有别于Java的语言特性提供了发挥空间。
三、Class类文件的结构
Class 文件是二进制文件,它的内容具有严格的规范,文件中没有任何空格,全都是连续的 0/1。
Class 文件 中的所有内容被分为两种类型:无符号数、表。
- 无符号数 无符号数表示 Class 文件中的值,这些值没有任何类型,但有不同的长度。u1、u2、u4、u8 分别代表 1/2/4/8 字节的无符号数。
- 表 由多个无符号数或者其他表作为数据项构成的复合数据类型。
Class 文件具体由以下几个构成:
- 魔数
- 版本信息
- 常量池
- 访问标志
- 类索引、父类索引、接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
我们可任意打开一个Class文件(使用Hex Editor等工具打开),内容如下(内容是16进制):
十六进制转字符串:16进制到文本字符串的转换,16进制-BeJSON.com
进制转换网址(十六进制转十进制):在线进制转换
参考下图去阅读上面的十六进制文档:
据上述的叙述,我们可以将class的文件组织结构概括成以下面这个表格
(其中u表示u4表示4个无符号 字节,u2表示2个无符号字节):
类型 | 名称 | 数量 |
u4 | magic(魔数) | 1 |
u2 | minor_version(JDK次版本号) | 1 |
u2 | major_version(JDK主版本号) | 1 |
u2 | constant_pool_count(常量池数量) | 1 |
cp_info | constan_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 |
1. 魔数
所有由Java编译器编译而成的class文件的前4个字节都是“0xCAFEBABE”。
它的作用在于:当JVM在尝试加载某个文件到内存中来的时候,会首先判断此class文件有没有JVM认为 可
以接受的“签名”,即JVM会首先读取文件的前4个字节,判断该4个字节是否是“0xCAFEBABE”,如 果
是,则JVM会认为可以将此文件当作class文件来加载并使用。
知识小结:
Class 文件的头 4 个字节称为魔数,用来表示这个 Class 文件的类型。
Class 文件的魔数是用 16 进制表示的“CAFE BABE”,是不是很具有浪漫色彩?
魔数相当于文件后缀名,只不过后缀名容易被修改,不安全,因此在 Class 文件中标识文件类型比较合适。
2. 版本信息
随着Java本身的发展,Java语言特性和JVM虚拟机也会有相应的更新和增强。
目前我们能够用到的JDK 版本如:1.5,1.6,1.7,还有现如今的1.8及更高的版本。
发布新版本的目的在于:在原有的版本上 增加新特性和相应的JVM虚拟机的优化。
而随着主版本发布的次版本,则是修改相应主版本上出现的 bug。我们平时只需要关注主版本就可以了。
主版本号和次版本号在class文件中各占两个字节,副版本号占用第5、6两个字节,而主版本号则占用 第7,8两个
字节。
JDK1.0的主版本号为45,以后的每个新主版本都会在原先版本的基础上加1。
若现在 使用的是JDK1.7编译出来的class文件,则相应的主版本号应该是51,对应的7,8个字节的十六进制的 值
应该是 0x33。
一个 JVM实例只能支持特定范围内的主版本号 (Mi 至Mj) 和 0 至特定范围内 (0 至 m) 的副版 本号。
假设一个 Class 文件的格式版本号为 V, 仅当Mi.0 ≤ v ≤ Mj.m成立时,这个 Class 文件 才可以被此 Java 虚拟机
支持。不同版本的 Java 虚拟机实现支持的版本号也不同,高版本号的 Java 虚拟机实现可以支持低版本号的
Class 文件,反之则不成立。
JVM在加载class文件的时候,会读取出主版本号,然后比较这个class文件的主版本号和JVM本身的版本号,如果
JVM本身的版本号 < class文件的版本号,JVM会认为加载不了这个class文件,会抛出我 们经常见到的"
java.lang.UnsupportedClassVersionError: Bad version number in .class file " Error 错误;
反之,JVM会认为可以加载此class文件,继续加载此class文件。
JDK版本号信息对照表:
JDK版本 | 16进制版本号 | 十进制版本号 |
JDK8 | 00 00 00 34 | 52 |
JDK7 | 00 00 00 33 | 51 |
JDK6 | 00 00 00 32 | 50 |
JDK5 | 00 00 00 31 | 49 |
JDK1.4 | 00 00 00 30 | 48 |
JDK1.3 | 00 00 00 2F | 47 |
JDK1.2 | 00 00 00 2E | 46 |
JDK1.1 | 00 00 00 2D | 45 |
小贴士:
- 有时候我们在运行程序时会抛出这个Error 错 误:"java.lang.UnsupportedClassVersionError: Bad version number in .class file"。上面已经揭示了出现这个问题的原因,就是在于当前尝试加载class文件的JVM虚拟 机的版本 低于class文件的版本。
解决方法:
a. 重新使用当前jvm编译源代码,然后再运行代码;
b. 将当前JVM虚拟机更新到class文件的版本。
- 怎样查看class文件的版本号?可以借助于文本编辑工具,直接查看该文件的7,8个字节的 值,确定class文件是什么版本的。
当然快捷的方式使用JDK自带的 javap 工具,如当前有Math.class 文件,进入此文件所在的目 录,然后执行
”javap -v Math“,
结果会类似如下所示:
知识小结:
紧接着魔数的 4 个字节是版本信息,5-6 字节表示次版本号,7-8 字节表示主版本号,它们表示当前 Class
文件中使用的是哪个版本的 JDK。
高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发
生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。
3. 常量池
版本信息之后就是常量池,常量池中存放两种类型的常量:
- 字面值常量字面值常量就是我们在程序中定义的字符串、被 final 修饰的值。
- 符号引用符号引用就是我们定义的各种名字:类和接口的全限定名、字段的名字和描述符、方法的名字和描述符。
常量池数据区
常量池的特点
- 常量池中常量数量不固定,因此常量池开头放置一个 u2 类型的无符号数,用来存储当前常量池的容量。
- 常量池的每一项常量都是一个表,表开始的第一位是一个 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_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
对于 CONSTANT_Class_info(此类型的常量代表一个类或者接口的符号引用),它的二维表结构如下:
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | name_index | 1 |
tag 是标志位,用于区分常量类型;name_index 是一个索引值,它指向常量池中一个 CONSTANT_Utf8_info
类型常量,此常量代表这个类(或接口)的全限定名,这里 name_index 值若为 0x0002,也即是指向了常量池
中的第二项常量。
CONSTANT_Utf8_info 型常量的结构如下:
类型 | 名称 | 数量 |
u1 | tag | 1 |
u2 | length | 1 |
u1 | bytes | length |
tag 是当前常量的类型;length 表示这个字符串的长度;bytes 是这个字符串的内容(采用缩略的 UTF8 编码)
知识小结:
常量池是class文件中非常重要的结构,它描述着整个class文件的字面量信息。
常量池是由一组 constant_pool结构体数组组成的,而数组的大小则由常量池计数器指定。
常量池计数器 constant_pool_count 的值 = constant_pool表中的成员数+ 1。
constant_pool表的索引值只有在 大于 0 且小于constant_pool_count时才会被认为是有效的。
注意事项:
常量池计数器默认从1开始而不是从0开始:
当constant_pool_count = 1时,常量池中的cp_info个数为0;
当constant_pool_count为n时,常 量池中的cp_info个数为n-1。
原因:
在指定class文件规范的时候,将索引#0项常量空出来是有特殊考虑的,
这样当:某些数据在特定的情 况下想表达“不引用任何一个常量池项”的意思时,就可以将其引用的常量
的索引值设置为#0来表示。
4. 访问标志
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,
包括:这个 Class 是类还是接口;是否定义为 public 类型;是否被 abstract/final 修饰。
知识小结:访问标志,access_flags 是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。
5. 类索引 & 父类索引 & 接口索引集合
类索引和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,Class 文件中由
这三项数据来确定类的继承关系。
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
由于 Java 不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父
类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。一个类可能实现了多个接口,因此用接
口索引集合来描述。这个集合第一项为 u2 类型的数据,表示索引表的容量,接下来就是接口的名字索引。
类索引和父类索引用两个 u2 类型的索引值表示,它们各自指向一个类型为 CONSTANT_Class_info 的类描
述符常量,通过该常量总的索引值可以找到定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。
类索引
类索引,this_class的值必须是对constant_pool表中项目的一个有效索引值。
constant_pool表 在这个索引处的项必须为CONSTANT_Class_info 类型常量,表示这个 Class 文件所定
义的类或接 口。
父类索引
父类索引,对于类来说,super_class 的值必须为 0 或者是对constant_pool 表中项目的一个有 效索引
值。
如果它的值不为 0,那 constant_pool 表在这个索引处的项必须为CONSTANT_Class_info 类型常 量,表
示这个 Class 文件所定义的类的直接父类。
当前类的直接父类,以及它所有间接父类的access_flag 中都不能带有ACC_FINAL 标记。对于接口来说,
它的Class文件的super_class项的 值必须是对constant_pool表中项目的一个有效索引值。constant_pool
表在这个索引处的项必须为代表java.lang.Object 的 CONSTANT_Class_info 类型常量 。
如果 Class 文件的 super_class的值为 0,那这个Class文件只可能是定义的是java.lang.Object类,只有它
是唯一没有父类的类。
6. 接口计数器
接口计数器,interfaces_count的值表示当前类或接口的【直接父接口数量】。
7. 接口信息数据区
接口表,interfaces[]数组中的每个成员的值必须是一个对constant_pool表中项目的一个有效索引 值, 它
的长度为 interfaces_count。
每个成员interfaces[i] 必须为 CONSTANT_Class_info类型常量,其中 【0 ≤ i 】。
在interfaces[]数组 中,成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,
即interfaces[0]对 应的是源代码中最左边的接口。
8. 字段表集合
字段表集合存储本类涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量。
每一个字段表只表示一个成员变量,本类中的所有成员变量构成了字段表集合。字段表结构如下:
类型 | 名称 | 数量 | 说明 |
u2 | access_flags | 1 | 字段的访问标志,与类稍有不同 |
u2 | name_index | 1 | 字段名字的索引 |
u2 | descriptor_index | 1 | 描述符,用于描述字段的数据类型。 基本数据类型用大写字母表示; 对象类型用“L 对象类型的全限定名”表示。 |
u2 | attributes_count | 1 | 属性表集合的长度 |
u2 | attributes | attributes_count | 属性表集合,用于存放属性的额外信息,如属性的值。 |
字段表集合中不会出现从父类(或接口)中继承而来的字段,但有可能出现原本 Java 代码中不存在的字段,譬
如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
9. 字段计数器
字段计数器,fields_count的值表示当前 Class 文件 fields[]数组的成员个数。
fields[]数组 中每一项都是一个field_info结构的数据项,它用于表示该类或接口声明的【类字段】或者【实例字 段】。
10. 字段信息数据区
字段表,fields[]数组中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接 口中某个字段的完整描述。
fields[]数组描述当前类或接口声明的所有字段,但不包括从父类或父接 口继承的部分。
11. 方法表集合
方法表结构与属性表类似。
volatile 关键字 和 transient 关键字不能修饰方法,所以方法表的访问标志中没有 ACC_VOLATILE 和
ACC_TRANSIENT 标志。
方法表的属性表集合中有一张 Code 属性表,用于存储当前方法经编译器编译后的字节码指令。
12. 方法计数器
方法计数器, methods_count的值表示当前Class 文件 methods[]数组的成员个数。
Methods[]数组中每一项都是一个 method_info 结构的数据项。
13. 方法信息数据区
方法表,methods[] 数组中的每个成员都必须是一个 method_info 结构的数据项,用于表示当前类 或接口中某
个方法的完整描述。
如果某个method_info 结构的access_flags 项既没有设置 ACC_NATIVE 标志也没有设置ACC_ABSTRACT 标志,
那么它所对应的方法体就应当可以被 Java 虚拟机直接从当前类加载,而不需 要引用其它类。
method_info结构可以表示类和接口中定义的所有方法,
包括【实例方法】、【类方法】、【实例初始 化方法】和【类或接口初始化方法】。
methods[]数组只描述【当前类或接口中声明的方法】,【不包括从父类或父接口继承的方法】。
14. 属性表集合
每个属性对应一张属性表,属性表的结构如下:
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
Code属性
Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。
Exceptions属性
Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也 就是方法描述时在
throws关键字后面列举的异常。
LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。
LocalVariableTable及LocalVariableTypeTable属性
LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,它 也不是运行时必需的属性,
SourceFile及SourceDebugExtension属性
SourceFile属性用于记录生成这个Class文件的源码文件名称。
SourceDebugExtension属性就可以用于 存储这个标准所新加入的调试信息,
一 个类中最多只允许存在一个SourceDebugExtension属性。
ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类 变量)才可以使用这项属性。
InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联。
如果一个类中定义了内部类,那编译器将 会为它以及它所包含的内部类生成InnerClasses属性。
Deprecated及Synthetic属性
Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值 的概念。
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通 过代码中使
用“@deprecated”注解进行设置。
Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的,
StackMapTable属性
StackMapTable属性在JDK 6增加到Class文件规范之中,它是一个相当复杂的变长属性,位于Code属性的属性表
中。
这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比
较消耗性能的基于数据流分析的 类型推导验证器。
StackMapTable属性中包含零至多个栈映射帧(Stack Map Frame),每个栈映射帧都显式或隐式地 代表了一个
字节码偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型。
类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑 约
束。
Signature属性
Signature属性在JDK 5增加到Class文件规范之中,它是一个可选的定长属性,可以出现于类、字段 表和方法表结
构的属性表中。
在JDK 5里面大幅增强了Java语言的语法,在此之后,任何类、接口、初 始化方法或成员的泛型签名
如果包含了类型变量(Type Variable)或参数化类型(Parameterized Type),则Signature属性会为它记录泛
型签名信息。
BootstrapMethods属性
BootstrapMethods属性在JDK 7时增加到Class文件规范之中,它是一个复杂的变长属性,位于类 文件的属性表
中。这个属性用于保存invokedynamic指令引用的引导方法限定符。
MethodParameters属性
MethodParameters是在JDK 8时新加入到Class文件格式中的,它是一个用在方法表中的变长属性。
MethodParameters的作用是记录方法的各个形参名称和信息。
模块化相关属性
JDK 9的一个重量级功能是Java的模块化功能,
因为模块描述文件(module-info.java)最终是要编 译成一个独立的Class文件来存储的,
所以,Class文件格式也扩展了Module、ModulePackages和ModuleMainClass三个属性用于支持Java模块化相
关功能。
Module属性是一个非常复杂的变长属性,除了表示该模块的名称、版本、标志信息以外,
还存储 了这个模块requires、exports、opens、uses和provides定义的全部内容。
运行时注解相关属性
早在JDK 5时期,Java语言的语法进行了多项增强,其中之一是提供了对注解(Annotation)的支 持。
为了存储源码中注解信息,Class文件同步增加了RuntimeVisibleAnnotations、
RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations和
RuntimeInvisibleParameterAnnotations四个属性。
到了JDK 8时期,进一步加强了Java语言的注解使用范围,又新增类型注解 (JSR 308),所以Class文件中也同
步增加了RuntimeVisibleTypeAnnotations和RuntimeInvisibleTypeAnnotations两个属性。
由于这六个属性不论结构还是功能都比较雷同,因此我们 把它们合并到一起,以RuntimeVisibleAnnotations为
代表进行介绍。
RuntimeVisibleAnnotations是一个变长属性,它记录了类、字段或方法的声明上记录运行时可见注 解,当我们
使用反射API来获取类、字段或方法上的注解时,返回值就是通过这个属性来取到的。
15. 属性计数器
属性计数器,attributes_count的值表示当前 Class 文件attributes表的成员个数。
attributes表中每一项都是一个attribute_info 结构的数据项。
16. 属性信息数据区
属性表,attributes 表的每个项的值必须是attribute_info结构。
在Java 7 规范里,Class文件结构中的attributes表的项包括下列定义的属性: InnerClasses 、
EnclosingMethod 、 Synthetic 、Signature、SourceFile,SourceDebugExtension 、Deprecated、
RuntimeVisibleAnnotations 、RuntimeInvisibleAnnotations以及BootstrapMethods属性。
对于支持 Class 文件格式版本号为 49.0 或更高的 Java 虚拟机实现,必须正确识别并读取attributes表中的
Signature、RuntimeVisibleAnnotations和RuntimeInvisibleAnnotations属性。
对于支持Class文件格式版本号为 51.0 或更高的 Java 虚拟机实现,必须正确识别并读取 attributes表中的
BootstrapMethods属性。
Java 7 规范 要求 任一 Java 虚拟机实现可以自动忽略 Class 文件的 attributes表中的若干 (甚至全部) 它不可
识别的属性项。
任何本规范未定义的属性不能影响Class文件的语义,只能提供附加的描述信息 。