类文件结构及类加载机制
写在前面
我们都知道 JVM 并不能直接运行 Java 源文件,而是开发者通过 JDK 自带的工具命令 javac
将 Java 源文件编译成 class 字节码文件,也就是二进制文件,然后供JVM加载并使用。
为了深入学习这一块的内容,先创建类 User
:
- User.java
1 | package com.openmind; |
类文件结构
Class类文件结构
编译User.java
类,我们用 Sublime Text 打开 User.class
字节码文件,可以看到如下十六进制代码如下:
1 | cafe babe 0000 0034 0021 0a00 0500 1c09 |
- class文件是一组以 8 字节为基础的二进制流,用 u1,u2,u4,u8分别表示 1 个字节,2 个字节,4 个字节,8 个字节的无符号数,采用 Big-edian 形式,即高位字节在前
- 各个数据项严格按照顺序紧凑排列在class文件中
- class文件中没有任何分隔符,这使得class文件存储的几乎都是可执行代码(在class文件中注释信息已经不复存在)
一个class文件完整地描述了Java源文件的各种信息,Oracle JVM规范中的4.1 The ClassFile Structure 详细定义了一个标准class文件的结构,如下:
1 | ClassFile { |
我们需要特别注意,1 个字节是2个16进制位,也就是说上面 cafe babe
是 4 个字节,这 4 个字节称之为“魔数”。
1 个字节是 8 位二进制位,表示的范围是 xxxxxxxx,也就是从 00000000-11111111,表示 0 到 255。1 位 16 进制数(用二进制表示为 xxxx)最多只能表示到 15 (即对应的十六进制 F),要表示到 255 就需要两个十六进制位。所以,1个字节=2个16进制字符,一个16进制位=0.5个字节。
用图表表示 ClassFile 的数据结构:
魔数
每个 class 文件的前四个字节称为魔数,它的唯一作用就是鉴定是否为一个合法的 class 文件。很多文件存储标准中都使用了魔数来进行身份识别的,譬如 WAV 语音文件的魔数也用 4 个字节表示为: 0x52494646,转为字符串为RIFF
。
由User.class
的 16 进制字节码可以看到,class 的魔数为:0xCAFEBABE,我们可以亲切的称呼为“咖啡宝贝”。
Class文件的版本
class 文件中第 5 到 第 8 个字节表示版本号,其中 5、6表示次版本,7、8表示主版本号。Java 版本号是从 45 开始的,JDK1.1 之后的每个JDK大版本发布时,主版本号都要加 1。我们看一看到主版本号为 0x0034
,转为十进制表示为 52,由此可以计算出编译 class 文件的JDK版本为JDK8(52-45=7,7+1=8)。
JDK高本版能向下兼容以前的 class 版本,但不能运行以后的版本,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件,否则会抛出java.lang.UnsupportedClassVersionError
。
主版本号和次版本号一起决定了类文件格式的版本。 如果一个类文件的主版本号为M,次版本号为m,那么我们将它的类文件格式的版本表示为M.m. 因此,类文件格式版本可以按照字典顺序排列,例如,1.5 <2.0 <2.1。 Java虚拟机实现可以支持版本v的类文件格式,当且仅当v处于某个连续范围Mi.0≤v≤Mj.m. 范围基于实现符合的Java SE平台的版本。 符合给定Java SE平台版本的实现必须支持表4.1-A中为该版本指定的范围,并且不支持其他范围。
Java SE版本 | class文件格式版本号范围 |
---|---|
1.0.2 | 45.0 ≤ v ≤ 45.3 |
1.1 | 45.0 ≤ v ≤ 45.65535 |
1.2 | 45.0 ≤ v ≤ 46.0 |
1.3 | 45.0 ≤ v ≤ 47.0 |
1.4 | 45.0 ≤ v ≤ 48.0 |
5.0 | 45.0 ≤ v ≤ 49.0 |
6 | 45.0 ≤ v ≤ 50.0 |
7 | 45.0 ≤ v ≤ 51.0 |
8 | 45.0 ≤ v ≤ 52.0 |
9 | 45.0 ≤ v ≤ 53.0 |
10 | 45.0 ≤ v ≤ 54.0 |
11 | 45 ≤ v ≤ 55 |
12 | 45 ≤ v ≤ 56 |
13 | 45 ≤ v ≤ 57 |
常量池
紧接着版本的则是常量池,常量池是Java内存中很重要的一部分。它是 class 文件结构与其他内存结构关联最多的数据类型,也是占用class文件空间最大的数据项之一。
常量池主要存放两类数据:字面量和符号引用。字面量比较接近Java层面的常量,如文本字符串,声明为final的常量值等。而符号引用则包括:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
符号引用属于编译原理中的知识,Java在用javac编译时,不需要在class文件中保存各个方法、属性、字段的最终内存布局信息,而是在jvm虚拟机加载class文件的时候动态链接的。因此,这些方法、字段、属性的符号不经过运行期转换的话是无法直接得到内存入口地址,也就无法直接被虚拟机直接使用。当运行虚拟机时,需要从常量池获取对应的符号引用,再在类创建时或运行时解析,翻译到具体的内存地址。
常量池首先是用 2 字节来表示常量的个数。根据 class 文件的格式,第 9、第 10 表示常量池的个数,我们看看上面User.class
的 16 进制,取第 9 位和第 10 位来计算:0x0021=33,这说明我们有 32 个常量,因为第 0 个常量表示不可用,其范围为[1,33]。
- 常量池是一系列的数组,它的下标是从
1
开始的,即有效大小实际为constant_pool_count - 1
,第0项是无效的,因此有些结构可以用第0项表示对常量没有引用。 - 常量池的设计有效减少class文件的大小,想想那些重复使用的类名,字符串表示现在只需要保留一份,并且引用的地方只需要用 u2 保存它在常量池的引用即可。
因为每个常量都有一种具体的类型来代表不同的含义,光知道常量的个数还没办法解析出具体的常量项来,所以定义每个常量的第一个字节u1表示该常量的类型tag,然后就可以根据该类型常量的存储结构来解析了。
常量的 tag 有:CONSTANT_Utf8
,CONSTANT_Integer
,CONSTANT_Float
,CONSTANT_Long
,CONSTANT_Double
,CONSTANT_Class
,CONSTANT_String
,CONSTANT_Fieldref
,CONSTANT_Methodref
,CONSTANT_InterfaceMethodref
,CONSTANT_NameAndType
,CONSTANT_MethodHandle
,CONSTANT_MethodType
,CONSTANT_InvokeDynamic
等14种。
类型 | 标志 | 描述 |
---|---|---|
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 | 16 | 表示方法类型 |
CONSTANT_MethodType_info | 15 | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
CONSTANT_Utf8_info
CONSTANT_Utf8_info 是常量池中最常见的最基本的常量,用来保存一个 utf8 编码的字符串,如常量字符串,类名,字段名,方法名等的值都是对它的引用(偏移量)。
1 | CONSTANT_Utf8_info { |
tag=1,length 表示字符串字节长度,如 length=16,则表示当前结构剩余 16 个字节是一个 utf8 编码的字符串。
需要注意的是:
-
Java 使用的是可变 utf8 编码: ASCII 字符(
\u0001
~\u0007F
,即 1 ~ 127)用 1 个字节表示,null(\u0000
)和\u0080
到\u07FF
之间用 2 个字节表示,\u8000
到\uFFFF
之间用 3 个字节表示。如果读到一个字节的最高位是 0,则是一个单字节。
读到一个字节最高3位是110则是一个双字节字符,紧接着还要再读1个字节。
读到一个字节最高4位是1110,则是一个三字节字符,紧接着还要再读2个字节。
关于如何解码可以查看官方文档,在java中,我们只需要使用new String(bytes, StandardCharset.UTF8)即可得到解码字符串
-
length使用了u2(0-65535)来表示,则其表示的字符串最大长度为65535(2^16 -1)。
CONSTANT_Integer_info
CONSTANT_Integer_info 常量池保存整形字面量,tag=3,之后的 4 个字节表示存储的整型值。
1 | CONSTANT_Integer_info { |
- 整形常量池是大端存储 big-endian,字节高位在前。可以用下面的代码解析:
1 | int value = 0; |
当然我们可以使用 DataInputStream::readInt() 方法读取一个 int 值。
- java 中 short, char, byte, boolean 使用 int 来表示,boolean 数组则用 byte 数组来表示(1 个 byte 表示 1 个 boolean 元素)。
CONSTANT_Float_info
1 | CONSTANT_Float_info { |
该常量池用来保存浮点数数据,tag=4,剩余的 4 字节保存浮点数的具体值,采用 IEEE 754标准定义。可以使用DataInputStream::readFloat()方法读取一个float值。
CONSTANT_Long_info
1 | CONSTANT_Double_info { |
该常量池存储长整型字面值,tag=5,long 和 double 类型在 class 中用两部分(高 4 位和低 4 位)存储。 可以通过 DataInputStream::readLong() 方法读取一个 float 值。
CONSTANT_Double_info
1 | CONSTANT_Double_info { |
该常量池存储双精度浮点型字面值,tag=6,存储方式同 CONSTANT_Long_info 一样。可以通过 DataInputStream::readDouble() 方法读取一个 double 值。
CONSTANT_Class_info
1 | CONSTANS_Class_info { |
该常量池用于存储类或接口的信息,tag=7, 注意不是 field 类型或者 method 参数类型、返回值类型。name_index 是常量池,存储的是 CONSTANT_Utf8_info 的索引。
CONSTANT_String_info
1 | CONSTANT_String_info { |
字符串常量池,tag=8,string_index 是常量池的索引,该索引值肯定是一个 CONSTANT_Utf8_info
的偏移量,CONSTANT_Utf8_info
中存储着实际的字符串值。
CONSTANT_Fieldref_info
1 | CONSTANT_Fieldref_info { |
tag=9,表示一个引用field信息,包括静态field和实例field。class_index 是常量池中一个CONSTANT_Class_info
类型常量(类/接口)的索引,表示字段 field 归属类。name_and_type_index 是常量池中一个CONSTANT_NameAndType_info
类型常量的索引,表示字段的名称和类型。
关于 field 引用解释一下,包括下面的 method,接口 method 引用同理:
- 以本文开头 User 类的 name 字段为例,name 在多个方法中都有用到,相比保存多份该字段信息来讲,在常量池中保存一份该字段信息,然后在其他用到的地方保存其索引显然更合适。
- CONSTANT_Fieldref_info 是把在代码中引用的 field(可能是本类的,也可能是外部类的)抽离成常量,请不要
class
中的字段表集合field_info
混淆。
CONSTANT_Methodref_info
1 | CONSTANT_Methodref_info { |
tag=10,表示引用一个方法 method 的信息,包括静态方法和实例方法。class_index class_index 是常量池中一个CONSTANT_Class_info
类型常量(类/接口)的索引,表示方法 method 归属类。name_and_type_index 是常量池中一个CONSTANT_NameAndType_info
类型常量的索引,表示方法的名称、参数和返回类型。
CONSTANT_InterfaceMethodref_info
1 | CONSTANT_InterfaceMethodref_info { |
tag=11,表示一个接口 method 信息。class_index 是常量池中一个CONSTANT_Class_info
类型常量(接口)的索引,表示方法的所属接口。name_and_type_index 同CONSTANT_Methodref_info
。
CONSTANT_NameAndType_info
1 | CONSTANT_NameAndType_info { |
CONSTANT_NameAndType_info
用于存储 field 和 method,类信息等,其 tag=12。name_index
指向一个CONSTANT_Utf8_info
,表示字段或方法的非全限定名称。descriptor_index
也指向一个CONSTANT_Utf8_info
,表示字段或方法的描述信息。
descriptor
用一个字符串CONSTANT_Utf8_info
保存。分为字段描述符和方法描述符。
- 字段描述符(FieldType): FieldType 可以是基本数据类型:
B(byte)
、C(Char)
、D(Double)
、F(Float)
、I(int)
、J(long)
、S(short)
、Z(boolean)
,对象类型:L+全限定名称
,数组类型:[+元素类型
。
1 | int a; // I |
- 方法描述符(MethodDescriptor):MethodDescriptor表示
(参数类型)返回类型
。
1 | /** |
CONSTANT_MethodHandle_info
1 | CONSTANT_MethodHandler_info { |
tag = 15,方法句柄,比如获取一个类的静态字段,实例字段,调用一个方法,构造器等都会转化成一个句柄。
- reference_kind
Kind | Description(描述) | Interpretation(解释) |
---|---|---|
1 | REF_getField |
getfield C.f:T |
2 | REF_getStatic |
getstatic C.f:T |
3 | REF_putField |
putfield C.f:T |
4 | REF_putStatic |
putstatic C.f:T |
5 | REF_invokeVirtual |
invokevirtual C.m:(A*)T |
6 | REF_invokeStatic |
invokestatic C.m:(A*)T |
7 | REF_invokeSpecial |
invokespecial C.m:(A*)T |
8 | REF_newInvokeSpecial |
new C; dup; invokespecial C.init:(A*)V |
9 | REF_invokeInterface |
invokeinterface C.m:(A*)T |
f
: field, m
: method, C
: 实例构造器,T
: 返回类型,(V 是 void类型),A*
: 参数
- reference_index
- 对于 kind 为 1,2,3,4,
reference_index
引用一个CONSTATNT_Feildre_info
。 - 对于 kind 为 5,6,7,8,
reference_index
引用一个CONSTANT_Methodref_info
。 - 对于 kind=9,
reference_index
引用一个CONSTANT_InterfaceMethodref_into
。
- 对于 kind 为 1,2,3,4,
CONSTANT_MethodType_info
1 | CONSTANT_MethodType_info { |
tag=16,描述一个方法类型。descriptor_index
是一个CONSTANT_Utf8_info
,保存方法的描述符。
CONSTANT_InvokeDynamic_info
1 | CONSTANT_InvokeDynamic_info { |
tag=18,invokedDynamic 动态调用指令信息。
- bootstrap_method_attr_index: BootstrapMethods属性中 bootstrap_methods[]数组的索引,每个引导方法引用一个
CONSTANT_MethodHandler_info
常量。 - name_and_type_index: 动态调用中方法名称等信息,引用一个
CONSTANT_NameAndType_info
常量。
访问标志
在 class 文件中,访问标志(access flags)占 2 个字节,总共 16bit,该标志用于描述方法或接口的访问信息。比如该类是 class 还是 interface,是 public 还是其他,是否为 abstract,是否被声明为 final 等。
标志名称 | 标志值 | 作用域对象 | 含义 |
---|---|---|---|
ACC_PUBLIC | 0X0001 | class, inner, field, method | 是否为public |
ACC_PRIVATE | 0x0002 | inner, field, method | 是否为private |
ACC_PROTECTED | 0x0004 | inner, field, method | 是否为protected |
ACC_STATIC | 0x0008 | inner, field, method | 是否为静态static |
ACC_FINAL | 0x0010 | class, inner, field, method | 是否为最终final的 |
ACC_SUPER | 0x0020 | class | 是否允许使用invokespecial字节码指令,JDK1.2之后编译出来的类这个标志为真 |
ACC_SYNCHRONIZED | 0x0020 | method | 是否为同步,即synchronized修饰 |
ACC_VOLATILE | 0x0040 | field | 是否被volatitle修饰 |
ACC_BRIDGE | 0x0040 | method | 桥方法标志,有该标志的方法上同时有ACC_SYNTHETIC标志 |
ACC_TRANSIENT | 0x0080 | field | |
ACC_VARARGS | 0x0080 | method | |
ACC_NATIVE | 0x0100 | method | 是否为native方法 |
ACC_INTERFACE | 0x0200 | class, inner | 是否为接口 |
ACC_ABSTRACT | 0x0400 | class, inner, method | 是否为抽象类或抽象方法 |
ACC_STRICT | 0x0800 | method | strictfp,strict float point,方法使用 FP-strict 浮点格式 |
ACC_SYNTHETIC | 0x1000 | class, inner, field, method | 标识这个类并非由用户代码生产 |
ACC_ANNOTATION | 0x2000 | class, inner | 标识这是一个注解 |
ACC_ENUM | 0x4000 | class, inner, field | 标识这是一个枚举 |
例如:
- 0x0011(0000 0000 0001 0001):public + final
- 0x0201(0000 0010 0000 0001):public + 接口
类、父类与接口索引的集合
类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据的集合,class 文件中由这三项数据来确定这个类的继承关系。类索引(this_class)用于确定这个类的全限定名,父类索引(super_class)用于确定这个类的父类的全限定名。由于 Java 不允许多多继承,所以父类所以只有一个,除了java.lang.Object
外,所有 Java 父类的索引都不为 0。接口索引集合就是用来描述该类实现了哪些接口,这些被实现了的接口就按照 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引的集合中。
- this class: 当前类或接口,指向一个
CONSTANT_Class_info
常量,可以解析出当前类的全限定名,其中包名层次用/
,而不是.
,如java/lang/Object
。 - super class: 当前类的直接父类,指向一个
CONSTANT_Class_info
常量,当没有直接父类时,super_class = 0。 - interfaces: 首先用 u2 表明当前类或接口的直接父接口数量 n。紧接着 n 个 u2 组成的数组即是这些父接口在常量池的索引,类型为
CONSTANT_Class_info
,按声明顺序从左至右。
字段表集合
字段(field)其实就是接口或者类中定义的变量,有实例变量和类变量之分。字段表(field_info)用于描述这些字段。方法中定义的变量不能算作字段,字段特指定义在接口或者类中的变量。
1 | field_info { |
在字段表之前有一个 u2 字节的数据表示字段个数。然后紧跟的是field_info,保存当前类的fields信息。
- access_flags:占用两个字节,它描述了该字段的基本访问标志,主要包括:字段的作用域、实例或类变量(static)、可否序列化(transient)、可变性(final)、并发可见性(volatile修饰符)等,每种状态使用一个比特位来标识对于该状态的修饰与否。
标志名称 | 标志值 | 含义 |
---|---|---|
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 | 字段是否是 synthetic |
ACC_ENUM | 0x4000 | 字段是否是 enum |
- name_index:占两个字节,它存储的是当前字段名称在常量池中的偏移量,也就是内存地址。
- descriptor_index:占用两个字节,它是对当前字段数据类型的描述,存储的也是一个字符在常量池中的偏移量。但是如果你对应到常量池查看的话,你会发现这个描述符的值是:I。
标识字符 | 含义 |
---|---|
B | 基本数据类型 byte |
C | 基本数据类型 char |
D | 基本数据类型 double |
F | 基本数据类型 float |
I | 基本数据类型 int |
J | 基本数据类型 long |
S | 基本数据类型 short |
Z | 基本数据类型 boolean |
V | 特殊类型 void |
L | 对象类型,如 Ljava/lang/Object |
基本数据类型与实际存储的符号之间有这么一种映射关系,为的是简单存储。其中,如果字段是数组类型的话,需要前置一个 『[
』,多维数组就前置多个该符号进行描述,如[B
表示是一个一维 byte 数组。
- attributes_count: 属性数量。如果没有属性,则为 0。
- attribute_info: 属性表,此处的属性表描述的是字段中携带子集的属性表集合,以用于描述某些场景专有的信息。具体的分析放在属性表集中。
方法集合
方法集合(method_info)保存当前类的方法信息。在 method_info 之前紧靠的是 methods_count,methods_count 记录该类或接口中的方法个数。方法集合同 field_info。
1 | method_info { |
属性表集
属性表:属性存在与 ClassFile, field_info, method_info 中,此外 Code 属性中又包含嵌套属性信息,属性用来描述指令码,异常,注解,泛型等信息,JLS8预定义了23种属性,每种属性结构不同(变长),但可以抽象成下面通用结构。
1 | attribute_info { |
- attribute_name_index: 是该属性在常量池中的偏移量(内存地址的引用),通过该名称才可以判定当前属性属于具体的哪一种,如“Code”表示当前是一个
Code_attribute
属性。 - attribute_length: 表示接下来多少字节属于该属性的内容信息,Java 允许自定义新的属性,如果 jvm 不认识,则按通用结构直接读取 attribute_length 个字节。
- info:存放着 23 中属性中的任意一种。23 中属性则按作用可分为 3 组:
- 被jvm翻译使用:ConstantValue,Code, StackMapTable, Exceptions, BootstrapMethods
- 被java类库解析使用: InnerClasses, EnclosingMethod, Synthetic, Signature, RuntimeVisibleAnnotations/RuntimeInvisibleAnnotations, RuntimeVisibleParameterAnnotations/RuntimeInvisibleParameterAnnotations, RuntimeVisibleTypeAnnotations/RuntimeInvisibleTypeAnnotations, AnnotationDefault, MethodParameters
- 既不要求jvm解析,也不要求java类库解析,用于调试工具等场景: SourceFile, SourceDebugExtension, LineNumberTable, LocalVariableTable, LocalVariableTypeTable, Deprecated
ConstantValue
1 | ConstantValue_attribute { |
存在于field_info
,代表一个常量值,如private final int x = 5;
中的5
。attribute_name_index 引用的值是“ConstantValue”,attribute_length是固定值2,两个字节的 constantvalue_index 是该常量值在常量池中的索引,是Constant_Long
、Constant_Float
、Constant_Double
、Constant_Integer
、Constant_String
的一种。
Code
1 | Code_attribute { |
描述方法体编译后的字节码指令。前面讲过描述方法的method_info
结构,而方法的方法体信息就存在它的属性表中的 code 属性内。如果是抽象方法,则没有这个属性。
前面属性通用架构attribute_info
中已经涉及attribute_name_index
、attribute_length
,它是每个属性都有的,后面不再说明,只对其他部分介绍。
-
max_stack
,操作数栈的最大深度,用来分配栈的大小。 -
max_locals
,方法栈中局部变量表最大容量,存储局部变量、方法参数、异常参数等。以 slot 为单位,32-bit 以内的变量分配 1 slot,大于 32-bit,如long
、double
则分配 2 slot,注意对象存的是引用。另外指出一点,对于实例方法,默认会传入this
对象指针,所以这时的max_locals
最小为 1。 -
code[code_length],存储字节码指令列表,每条字节码指令是一个 byte,这样,8bit 最多可以表示256条不同的指令,需要指出的是,这个字节流数组存的不全是指令,有的指令还有对应的操作数,跳过相应 n 个字节的操作数后才是下一条指令。
-
exception_table[exception_table_length],方法异常表,注意不是方法申明抛出的异常,而是显示 tyr-catch 的异常,每个 catch 的异常是 exception_table 的一项。
- catch_type,捕获的异常类型,指向一个 CONSTANT_Class_info 常量。
- start_pc,字节码指令相对方法开始的偏移量,相当于 code[code_length] 中的索引。
- end_pc,字节码指令相对方法结束的偏移量,相当于code[code_length] 中的索引。
- handler_pc,字节码指令相对方法处理的偏移量,相当于code[code_length] 中的索引。
这几项表示的意思是,如果在[start_pc,end_pc]区间发生了 try-catch 类型或其他子类型的异常(catch_type=0表示补货任意异常),则跳转至 handler_pc 处的指令继续执行。
补充三点:
(1) 关于 finaly 中的指令采用方式是在每个代码分支中冗余一份。
(2) 关于未显示捕获的异常,则通过athrow
指令继续抛出。
(3) 虽然指令长度 code_length 是 u4,但 start_pc, end_pc, handler_pc 都只有 2 个字节的无符号数 u2,最大范围只能表示 65535,因此方法最多只能有65535条指令(每条指令都不带操作数的情况下)。 -
attributes[attributes_count],嵌套属性列表。
StackMapTable
1 | StackMapTable_attribute { |
上面讲到的 code_attribute 中也可以包含属性表,StackMapTable 就位于 code 属性的属性表中,它是为了在 jvm 字节码验证阶段做类型推到而添加的。
Exceptions
1 | Exceptions_attribute { |
表示通过throws
声明可抛出的异常,结构很简单,exception_index_table 的每一项 u2 指向一个 CONSTANT_Class_info 常量。
BootstrapMethods
1 | BootstrapMethods_attribute { |
位于 classFile 中,保存invokedynamic
指令引用的引导方法。
- bootstrap_method_ref,引用一个 CONSTANT_MethodHandle_info 常量,此时该 MethodHandle 的 reference_kind 必定为 REF_invokeStatic 或 REF_newInvokeSpecial。
- bootstrap_arguments,引导方法参数列表,数组中的每一项是一个
CONSTANT_String_info
,CONSTANT_Class_info
,CONSTANT_Long_info
,CONSTANT_Double_info
,CONSTANT_Float_info
,CONSTANT_Integer_info
,CONSTANT_MethodHandle_info
,CONSTANT_MethodType_info
的一种。
InnerClasses
1 | InnerClasses_attribute { |
记录内部类信息,classes 就是当前内部类列表,其中 innner_class_info_index 和 outer_class_info_index 都指向CONSTANT_Class_info
常量,分别代表内部类和外部类信息引用,inner_name_index 是内部类名称的引用,指向CONSTATNT_Utf8_info
常量,等于0则代表是匿名内部类,inner_class_access_flags 是内部类访问标志,同 access_flag。
EnclosingMethod
1 | EnclosingMethod_attribute { |
位于 classFile 的结构中,存储局部类或匿名类信息。
- class_index,对直接包含它的类的引用,引用一个 CONSTANT_Class_info 常量,代表包含当前类的最内层类。
- method_index,引用一个 CONSTANT_NameAndType_info 常量,表示直接包含该局部内、匿名类的方法名称或类型。
Synthetic
1 | Synthetic_attribute { |
标记类、方法、字段是否编译器生成, 与 ACC_SYNTHETIC 同义。attribute_length=0,存在该属性则表示 true。
Signature
1 | Signature_attribute { |
存在于类、方法、字段的属性表中,用于存储类、方法、字段的泛型信息(类型变量 Type Variables,参数化类型 Parameterized Types)。
signature_index 引用一个 CONSTANT_Utf8_info 常量,表示签名。
RuntimeVisibleAnnotations
1 | RuntimeVisibleAnnotations_attribute { |
存在于类、方法、字段,存储运行时可见的注解信息(RetentionPolicy.RUNTIME),可以被反射 API 获取到。annotation 结构存储了注解名称、元素值对的信息。
RuntimeInvisibleAnnotations
1 | RuntimeInvisibleAnnotations_attribute { |
与RuntimeVisibleAnnotations结构相同,但不可见,即不能被反射API获取到,目前jvm忽略此属性。
RuntimeVisibleParameterAnnotations
1 | RuntimeVisibleParameterAnnotations_attribute { |
存在于method_info的属性表中,存储运行时可见的方法参数注解信息,与RuntimeVisibleAnnotations 对比发现,RuntimeVisibleParameterAnnotations 存储的是方法的参数列表上每个参数的注解(相当与一组 RuntimeVisibleParameterAnnotations),顺序与方法描述符中参数顺序一致。
RuntimeInvisibleParameterAnnotations
1 | RuntimeInvisibleParameterAnnotations_attribute { |
RuntimeVisibleTypeAnnotations
1 | RuntimeVisibleTypeAnnotations_attribute { |
存在于class_file,method_info,field_info,code 的属性表中,java8 新增。JLS8新增两种 ElementType(ElementType.TYPE_PARAMETER, ElementType.TYPE_USE),相应用来描述的注解属性也做了相应的改的,就有了该属性,type_annotation 存储着注解信息及其作用对象。
RuntimeInvisibleTypeAnnotations
1 | RuntimeInvisibleTypeAnnotations_attribute { |
AnnotationDefault
1 | AnnotationDefault_attribute { |
存在于method_info属性表 ,记录注解元素的默认值。
MethodParameters
1 | MethodParameters_attribute { |
存在于method_info
属性表 ,记录方法参数信息,name_index 形参名称,access_flags 有 ACC_FINAL,ACC_SYNTHETIC,ACC_MANDATED 。
SourceFile
1 | SourceFile_attribute { |
class_file属性表中,记录生成该的文件名,异常堆栈可能显示此信息,一般与类名相同,但内部类不是。这是一个可选属性,意味着不强制编译器生成此信息。
SourceDebugExtension
1 | SourceDebugExtension_attribute { |
存在于class结构中,可选,保存非java语言的扩展调试信息。debug_extension 数组是指向 CONSTAN_Utf8_info 的索引。
LineNumberTable
1 | LineNumberTable_attribute { |
code 的属性表中,存储源码行号与字节码偏移量(方法第几条指令)之间映射关系,start_pc 字节码偏移量,line_number 源码行号,可选。
LocalVariableTable
1 | LocalVariableTable_attribute { |
code的属性表中,存储栈帧中局部变量表的变量与源码中定义的变量的映射,可以在解析code属性时关联到局部变量表变量在源码中的变量名等,可选。
LocalVariableTypeTable
1 | LocalVariableTypeTable_attribute { |
code 的属性表中,与 LocalVariableTable 相似,signature_index 也引用一个CONSTANT_Utf8_info 常量,对应含有泛型的变量会同时存储到 LocalVariableTable 和 LocalVariableTypeTable 中个一份。
Deprecated
1 | Deprecated_attribute { |
类、方法、字段过期标记,没有额外信息,attribute_length=0,如果出现该属性则说明加了 @deprecated 注解。
JVM的限制
classFile
结构中u2
(16-bit)的constant_pool_count
限制了 per-class 或者 per-interface 的常量池的大小,最多只能有 65535 个条目(entries)。这对单个类或者接口的复杂性起到类内部限制作用。classFile
结构中fields_count
限制了一个类或者接口能声明的field
数量不能超过 65535(不包括继承的)。classFile
结构中methods_count
限制了一个类或者接口能声明的method
数量不能超过 65535(不包括继承的)。- 直接父接口的数量限制同上(
interfaces_count
)。 - 方法调用时创建的帧里面,本地变量表中的本地变量数量最多不能超过 65535,由
code
属性中的max_locals
item 所限制,以及由 JVM 指令集的 16-bit 本地变量索引所限制。其中long
和double
类型视为两个本地变量。 code
属性中的max_stack
item 限制了frame中的操作数栈的大小为 65535。其中,long
和double
类型视为两个操作单元。method descriptor
方法描述符限制了方法参数最多为 255 个,其中实例单元this
占用一个,long
和double
占用两个单元。field
和方法名称、field和方法描述符以及其他String常量值(包括ConstantValue
attribute引用)最多为 65535 个 byte,由CONSTATNT_Utf8_info
结构中的 16-bit 无符号length
item限制。- 数组的维度最多 255,由
multianewarray
指令中的 opcodedimensions
的大小所限制。
参考文章
1.反认他乡是故乡——class字节码,这次我算看透你了!
2.Gordon——Java class文件格式
3.薛勤的博客——深入理解Java虚拟机(类文件结构+类加载机制+字节码执行引擎)
4. 周志明——《深入理解Java虚拟机(第2版)》
5. Java Virtual Machine Specification ——The class File Format
感谢上面三维作者的总结,对我理解 classfile 起到特别大的帮助,文章中部分内容直接来源于上述文章。