上篇文章结束了自动内存管理的篇章,本文开始将会讲解类文件、类加载相关的内容。
Java 类文件的结构一直很稳定。(虽说以类文件代称,但实质上不一定是文件形式,可以是任意字节流)
和其他二进制格式类似,Class
文件没有任何分隔符。Class
文件使用 Big-Endian 编码。结构如下所示,u
表示无符号,后面的数字是字节数量:
类型 | 名称 | 数量 |
---|---|---|
u4 | Magic | 1 |
u2 | 小版本号 | 1 |
u2 | 大版本号 | 1 |
u2 | 常量池个数 | 1 |
cp_info | 常量池 | 常量池个数 - 1 |
u2 | 访问 flags | 1 |
u2 | 类索引 | 1 |
u2 | 父类索引 | 1 |
u2 | 接口数量 | 1 |
u2 | 接口索引 | 接口数量 |
u2 | 字段数量 | 1 |
field_info | 字段 | 字段数量 |
u2 | 方法数量 | 1 |
method_info | 方法 | 方法数量 |
u2 | 属性数量 | 1 |
attribute_info | 属性 | 属性数量 |
magic 和版本
每个 class
文件的首 4 个字节为 magic
number,用来做基本的标识。很多二进制文件都会在头部放置 magic number。而
class
文件的 magic number 为 0xCAFEBABE
。
后面四个字节,即为字节码版本。第 5、6 个字节为次版本号(Minor Version),第 7、8 个字节为主版本号(Major Version)。主版本号从 45 开始,在 JDK 1.1 之后随着 JDK 大版本发布而递增。同时,高版本的 JVM 可以加载低版本的类文件(向后兼容),但拒绝更高版本的类文件。
以下是 JDK 1.0 到 JDK 20 所对应的字节码版本(摘自 javaalmanac.io):
JDK Version | Bytecode Version |
---|---|
Java 1.0 | 45.0 |
Java 1.1 | 45.3 |
Java 1.2 | 46.0 |
Java 1.3 | 47.0 |
Java 1.4 | 48.0 |
Java 5 | 49.0 |
Java 6 | 50.0 |
Java 7 | 51.0 |
Java 8 | 52.0 |
Java 9 | 53.0 |
Java 10 | 54.0 |
Java 11 | 55.0 |
Java 12 | 56.0 |
Java 13 | 57.0 |
Java 14 | 58.0 |
Java 15 | 59.0 |
Java 16 | 60.0 |
Java 17 | 61.0 |
Java 18 | 62.0 |
Java 19 | 63.0 |
Java 20 | 64.0 |
次版本号通常用于标识 EA 版(Early Access),当用了实验性功能时,会将其置为 65535。
常量池
在二进制中,存储数组通常会用 数量 + 内容 的形式,类文件也不例外。
常量池长度从 1 开始记数,可以理解为包含了自身。(然而其他数组的数量是从 0 开始的。)
常量池中主要存放两大常量:
- 字面量(Literal):如字符串字面量,被声明为 final 的常量等。
- 符号引用(Symbolic References),包含:
- 包(Package)
- 限定名(Fully Qualified Name)
- 字段名称和描述符(Descriptor)
- 方法名称和描述符
- 方法句柄和类型(Method Handle, Method Type, Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site, Dynamically-Computed Constant)
Java 不存在「静态链接」,所有类信息都是在加载 Class 时动态链接的。Class 文件中不会保存各个方法、字段最后在内存中的布局信息。这是 JVM 负责的。
常量池中,每一项常量都是一张表。最初有 11 种结构各不相同的数据结构。后来为了更好支持动态语言,又新增了 4 种。为了支持模块化系统(Jigsaw),又增加了 2 种。
这 17 类表起始都为其对应的标识位,代表对应的类型。从 UTF-8 字符串到整形、浮点、符号引用、动态类型、模块无所不有。
常量池十分繁琐,每种类型都不一样。这里就不做具体讲解了。作为代替,可以使用
javap
查看常量表。
不防用以下代码:
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world!");
}
}
javac HelloWorld.java
javap -verbose HelloWorld
笔者这里使用 JDK 17 编译,结果为(删减了一部分):
Classfile HelloWorld.class
Compiled from "HelloWorld.java"
public class HelloWorld
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #21 // HelloWorld
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Class #10 // java/lang/System
#9 = NameAndType #11:#12 // out:Ljava/io/PrintStream;
#10 = Utf8 java/lang/System
#11 = Utf8 out
#12 = Utf8 Ljava/io/PrintStream;
#13 = String #14 // hello world!
#14 = Utf8 hello world!
#15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V
#16 = Class #18 // java/io/PrintStream
#17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V
#18 = Utf8 java/io/PrintStream
#19 = Utf8 println
#20 = Utf8 (Ljava/lang/String;)V
#21 = Class #22 // HelloWorld
#22 = Utf8 HelloWorld
#23 = Utf8 Code
#24 = Utf8 LineNumberTable
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 SourceFile
#28 = Utf8 HelloWorld.java
// ...
javap
工具帮我们把常量池中的所有内容都计算了出来。其中有些内容在代码中没有出现过,如
I
V
<init>
LineNumberTable
LocalVariableTable
等。这些常量都是编译器自动生成的。会被后面的字段表、方法表、属性表等引用。
访问标识
在常量池结束后,紧邻的两个字节就是访问标识(access_flags
)了,用于表示类或接口层次的访问信息:
标识名称 | 标识位 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 标识 public 类型 |
ACC_FINAL | 0x0010 | 标识 final 类型,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码的新语义(因为在 JDK 1.0.2 发生过改变) |
ACC_INTERFACE | 0x0200 | 标识接口 |
ACC_ABSTRACT | 0x0400 | 标识 abstract 类型,对于接口和抽象类为真 |
ACC_SYNTHETIC | 0x1000 | 标识非用户代码生成的类 |
ACC_ANNOTATION | 0x2000 | 标识注解 |
ACC_ENUM | 0x4000 | 标识枚举 |
ACC_MODULE | 0x8000 | 标识模块 |
一共有 16 个标识可用,目前只用了 9 个(截止 JDK 18),未定义的标识位要求为 0。
类、父类、接口索引
类索引(this_class
)和父类索引(super_class
)都是
u2 类型的数据,而接口是一组 u2
类型的数据。它们都是前面常量池的索引,指向对应的全限定类名。
除了 java.lang.Object
之外。所有 Java
类都有父类。因此,除了它之外,所有 Java
类的父类索引都不能为 0。而接口索引则会按照 implements
时的顺序依次排列。
字段表
字段表(field_info
)用于描述接口或类中声明的标量。字段(field)包括静态变量和实例变量,但不包含局部变量。不妨回忆
Java
中对字段声明时需要或可以包含的信息,包括作用域修饰符(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
包括:
名称 | 标识位 | 解释 |
---|---|---|
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 | 类型是否为枚举 |
显然的,ACC_PUBLIC
ACC_PRIVATE
ACC_PROTECTED
只能三者选一个。同时
ACC_VOLATILE
和 ACC_FINAL
也不能同时出现。和之前一样,未使用的标识位保持 0。
紧随 access_flags
后面的就是 name_index
和
descriptor_index
,查常量池即可知道名称和描述符了。此时可以介绍几个概念:
- 全限定名称(qualified name):如
moe/sdl/clazz/TestClass
就是这个类的全限定名称,.
换为/
即可。一般还会在结尾加上;
避免混淆。 - 简单名称(simple name):没有类型和参数修饰的方法、字段。比如
inc()
和字段m
的简单名称就分别是inc
和m
。 - 描述符(descriptor):用于描述字段的类型,方法的参数列表和返回值。标识字符可以见下表:
标识字符 | 含义 | 标识字符 | 含义 |
---|---|---|---|
B | 基本类型 byte | J | 基本类型 long |
C | 基本类型 char | S | 基本类型 short |
D | 基本类型 double | Z | 基本类型 boolean |
F | 基本类型 float | V | 特殊类型 void |
I | 基本类型 int | L | 对象类型,如 Ljava/lang/Object; |
对于数组类型,每一维就使用一个前置的 [
来描述,如
java/lang/String[][]
的标识符就为
[[java/lang/String;
,整形数组 [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
还有一项属性表集合,可以为空。然而如果改为
final static int m = 123;
可能就会存在名为
ConstantValue
的属性,指向常量
123。关于此的更多内容,下面会详细介绍。
此外,字段中不会出现从父类中继承的字段,但可能出现代码中不存在的字段,譬如内部类为了访问外部类,就会自动添加对外部类实例的引用。
还有一点,虽然 Java 中字段无法重载,但类文件中是允许的。只要描述符不完全相同,就允许重载。
方法表
字段表结构和方法表结构是一样的:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
不一样的是标识位:
标识名称 | 标识位 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否 public |
ACC_PRIVATE | 0x0002 | 是否 private |
ACC_PROTECTED | 0x0004 | 是否 protected |
ACC_STATIC | 0x0008 | 是否 final |
ACC_SYNCHRONIZED | 0x0020 | 是否 synchronized |
ACC_BRIDGE | 0x0040 | 是否是编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 是否接受不定参数 |
ACC_NATIVE | 0x0100 | 是否 native |
ACC_ABSTARCT | 0x0400 | 是否 abstract |
ACC_STRICT | 0x0800 | 是否 strictfp |
ACC_SYNTHETIC | 0x1000 | 是否由编译器自动产生 |
方法表只存储元数据,真正的逻辑存储在属性表名为 Code
的属性里。
和属性表类似,如果父类方法没有被子类重写,是不会出现来自父类的信息的。但可能出现由编译器自动添加的方法,比如类构造器
<clinit>
() 和实例构造器
<init>()
。
同样的,Java 不允许只有返回值不同的方法重载。但在类文件中这是允许的。
属性表
属性表(attribute_info
)之前提到过几次了,Class
文件、字段表、方法表都可以携带自己的属性表。
属性表要求比较宽松,只要不和现有属性名重复,任何人都可以在属性表中定义自己的信息。JVM 会忽略掉不认识的属性。JVM 中定义的属性很多,这里就不列全表了,有兴趣的可以自己查看《JVM 规范》对应的章节。
对于每个属性,都能看作以下定义:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
除了名称和长度需要明确以外,其他部分完全是自定义的。
Code
JVM 程序经过编译后,其中的代码逻辑就储存在 Code
属性中。然而并非所有方法表都必须存在该属性,比如接口、抽象类中的虚方法,就不需要
Code 属性。Code 属性结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attribute_name_index
是指向常量池的索引,固定为
Code
。
max_stack
代表了操作数栈(Operand Stack)的最大深度。JVM
会根据该值分配栈帧(Stack Frame)。
max_locals
代表了局部变量需要的储存空间。max_locals
的单位是变量槽。对于 byte
char
int
等长度不超过 32 bit 的数据类型,只占用一个变量槽。而
long
和 double
需要两个变量槽。方法参数(包括隐藏参数
this
)、异常处理程序的参数(Exception Handler Parameter,即
try-catch
语句中定义的异常)、方法体中定义的局部变量等,都需要局部变量表来存放。此外,并非用了多少个变量,就将其数量作为
max_locals
的值,因为不必要的栈深度和变量槽数量会浪费内存。Javac
编译器会根据变量作用域来分配变量槽,而非单纯的加和。
code_length
和 code
就是编译后的字节码指令。每个指令都是 u1
类型的单字节,并知道后面是否需要参数,如何解析参数。可定义的范围为
0x00 ~ 0xFF
也就是最多表达 256 条指令。
此外,code_length
虽然为 u4
类型,但实际限制一个方法的最大长度为 65535
条字节码指令。如果超过这个限制,Javac 会拒绝编译。
字节码也可以使用 javap
命令计算。比如:
public class TestClass {
private int m;
public int inc() { return m + 1; }
}
{
public TestClass();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public int inc();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #7 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 3: 0
}
可以注意到其中的 args_size
。可能会有疑问:两个方法
init<>()
和 inc()
都没有参数,为什么有一个参数?这是因为 Java 隐式传递了
this
,将对象自动传入了而已。
有些语言,比如 Python,需要显式写明要传递
self
或this
,然而值得一提的是,Python 却不用有无self
来判断是否static
。需要打上类似@classmethod
@staticmethod
之类蹩脚的注解。
如果 inc()
被声明为
static
,args_size
就会变为 0。
若代码中使用了 try-catch
那么在字节码之后,就是异常表了。结构如下:
类型 | 名称 | 数量 | 类型 | 名称 | 数量 |
---|---|---|---|---|---|
u2 | start_pc | 1 | u2 | handler_pc | 1 |
u2 | end_pc | 1 | u2 | catch_type | 1 |
Java 正是使用异常表实现异常处理机制的,不妨再次查看字节码:
public class TestClass {
public int inc() {
int x;
try {
= 1;
x return x;
} catch (Exception e) {
= 2;
x return x;
} finally {
= 3;
x }
}
}
{
// ...
public int inc();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1 # try 块中的 x = 1
1: istore_1
2: iload_1 # 保存 x 到 returnValue, 此时 x 为 1
3: istore_2
4: iconst_3 # finally 块中的 x = 3
5: istore_1
6: iload_2 # 将 returnValue 中的值放到栈顶, 准备返回
7: ireturn
8: astore_2 # 给 Exception e 赋值
9: iconst_2 # catch 块中的 x = 2
10: istore_1
11: iload_1 # 保存 x 到 returnValue 中, 此时 x 为 2
12: istore_3
13: iconst_3 # finally 块中的 x = 3
14: istore_1
15: iload_3 # 将 returnValue 中的值放到栈顶, 准备返回
16: ireturn
17: astore 4 # 如果有不属于 Exception 及其子类的异常 才能到这里
19: iconst_3 # finally 块中的 x = 3
20: istore_1
21: aload 4 # 将异常 push 到栈顶,抛出
23: athrow
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
// ...
}
Exceptions
这里的 Exceptions
是和 Code
平级的属性,不要和刚才的异常表混淆了。该属性的作用是列出可能抛出的受检异常(Checked Exceptions
)也就是
throws
关键字后列举的异常,结构见表:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exception |
其中 exception_index_table
指向常量池中
CONSTANT_class_info
类型的常量。
LineNumberTable
用于描述 Java
源码行号和字节码行号之间的对应关系。不是运行时的必要属性,但对错误处理和调试很有帮助。因此会默认生成,可以用
-g:none
或 -g:lines
关闭。结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
而 line_number_info
类型包含 start_pc
和
line_number
两个 u2
数据,前者是字节码行号,后者是源码行号。
LocalVariableTable / LocalVariableTypeTable
LocalVariableTable
的结构和 LineNumberTable
类似,不过将 line_number_info
换为了
local_variable_info
:
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | length | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | index | 1 |
LocalVariableTable
也不是运行时必须的属性。但默认会生成,也可以通过 -g:none
或
-g:vars
关闭。如果没有生成该属性,最大的影响是,在别人调用时参数名称将会变为
arg1
arg2
等占位符。
start_pc
和 length
共同表示了局部变量的作用域。
name_index
和 descriptor_index
分别代表了变量名称和类型。
index
代表在局部变量表中变量槽的位置。若为 64
位的数据类型(double
long
),那么会占用
index
和 index + 1
两个变量槽。
LocalVariableTypeTable
是在 JDK 1.5
加入泛型时加入的。用于记录泛型参数。只是将 descriptor_index
这一项换成了 signature
。
Source File / SourceDebugExtension
也是可选的,用于记录源代码文件名称。可以通过 -g:none
或
-g:source
关闭。
如果不生成这项属性,抛出异常时不能显示出代码所属的文件名。该属性结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
sourcefile_index
是常量池中的
CONSTANT_Utf8_info
类型常量的索引,即源码文件名。
SourceDebugExtension
是在 JDK 1.5
时加入的。用于储存额外的调试信息。结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | debug_extension | attribute_length |
Constant Value
作用为通知 JVM 为静态变量赋值。只有 static
关键字的变量可以使用该属性。对于实例变量,可以在
<init>()
方法中实例化。而对于类变量,可以在
<clinit>()
中或使用 Constant Value
初始化。Oracle 的 Javac 编译器使用 Constant Value
来初始化
static final
且为基本类型或字符串类型的常量。如果不被
final
修饰,或者不是基本类型或字符串,就会在
<clinit>()
方法中初始化。
Constant Value
属性结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
InnerClasses
用于记录内部类和宿主类的关系。如果一个类有
inner class
,则会有该属性。结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_classes | 1 |
inner_classes_info | inner_classes | number_of_classes |
inner_classes_info
的结构如下:
类型 | 名称 | 数量 | 介绍 |
---|---|---|---|
u2 | inner_class_info_index | 1 | 指向常量池,内部类的符号引用 |
u2 | outer_class_info_index | 1 | 指向常量池,宿主类的符号引用 |
u2 | inner_name_index | 1 | 指向常量池,内部类的名称 |
u2 | inner_class_access_flags | 1 | 内部类的访问标识 |
内部类的访问标识:
标识名称 | 标识位 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否 public |
ACC_PRIVATE | 0x0002 | 是否 private |
ACC_PROTECTED | 0x0004 | 是否 protected |
ACC_STATIC | 0x0008 | 是否 static |
ACC_FINAL | 0x0010 | 是否 final |
ACC_INTERFACE | 0x0020 | 是否为 interface |
ACC_ABSTRACT | 0x0400 | 是否 abstract |
ACC_SYNTHETIC | 0x1000 | 是否并发用户代码生成的 |
ACC_ANNOTAITON | 0x2000 | 是否注解 |
ACC_ENUM | 0x4000 | 是否枚举 |
Deprecated / Synthetic
都属于布尔属性,状态只存在有或无的区别,没有具体属性值。
Deprecated
用于表示类、字段、或方法已经不再被推荐使用。
Synthetic
用于表示该方法并非从源码生成的。一般而言,至少需要设置该属性或
ACC_SYNTHETIC
之一,唯一例外有 <init>()
和 <clinit>()
。
结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
其中 attribute_length
肯定是为 0
的。
StackMapTable
该属性在 JDK 6 中加入,用于减少类加载验证步骤的耗时。
其中包括 0 到多个栈映射帧(Stack Map Frame),每个栈映射帧都代表了一个字节码偏移量,用于表示执行到该字节码时,局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈,来确定字节码指令是否满足逻辑约束。
结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | inner_class_info_index | 1 |
u2 | outer_class_info_index | 1 |
u2 | number_of_entries | 1 |
stack_map_frame | stack_map_frame_entries | number_of_entries |
在类文件 50.0(JDK 7)以上,如果没有 StackMapTable
属性,则默认为 number_of_entries = 0
的空表。一个
Code
属性至多有一个 StackMapTable
属性,否则会抛出 ClassFormatError
。
Signature
众所周知 Java 的泛型是被擦除了的。然而并非完全被擦除,某些情况下可以通过反射获取一部分信息。最终的来源也就是这里。比如以下代码(Kotlin):
class Reflection<out T> {
abstract {
init (javaClass.genericSuperclass)
println}
fun test(): T
abstract
}
class Impl() : Reflection<Impl>() {
override fun test(): Impl = this
}
fun main() {
(Impl())
println}
// Output:
// Reflection<Impl>
// Impl@1fb3ebeb
可以看到成功输出了泛型参数。
Signature
属性结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | signature_index | 1 |
signature_index
对应常量池的有效索引,为
CONSTANT_Utf8_info
类型。
BootstrapMethods
该属性是一个复杂的变长属性。用于保存 invokedynamic
指令引用的引导方法限定服。
这个属性的作用涉及到 InvokeDynamic
的原理,在此先略过。
值得一提的是,JDK 7 中虽然有此属性,但是没有
invokedynamic
所以没什么用。JDK 8
中才算正式开始使用了。
它的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | num_bootstrap_methods | 1 |
bootstrap_method | bootstrap_methods | num_bootstrap_methods |
其中 bootstrap_method
的结构为:
类型 | 名称 | 数量 | 介绍 |
---|---|---|---|
u2 | bootstrap_method_ref | 1 | 对常量池的索引,CONSTANT_M ethodHandle_info 类型 |
U2 | num_bootstrap_arguments | 1 | |
u2 | bootstrap_arguments | num_bootstrap_arguments | 对常量池的索引,可以为基本类型信息+String信息+MethodHandle+MethodType |
MethodParameters
该属性于 JDK 8 时加入,用于记录各个形参的信息。
最初,类文件出于节省空间的考虑,不储存方法参数名,因为运行时名字不重要,在源码中妥善命名即可。但这给程序二次传播带来了不便,无法使用 IDE 的智能提示功能。
LocalVariableTable
不是已经解决了这个问题吗?其实没有完全解决。LocalVariableTable
是 Code
的子属性,如果没有方法体,自然就不会有局部变量表,对于抽象方法和接口,这不太友好。所以有了这个属性。
结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | parameters_count | 1 |
parameter | parameters | parameters_count |
parameter
的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | name_index | 1 |
u2 | access_flags | 1 |
access_flags
的标识位有以下:
标识名称 | 标识位 | 含义 |
---|---|---|
ACC_FINAL | 0x0010 | 是否 final |
ACC_SYNTHETIC | 0x1000 | 表示是编译器自动生成的 |
ACC_MANDATED | 0x8000 | 表示是在源文件中隐式定义的,比如 this |
模块化相关属性
由于 module-info.java
最终要编译成一个独立的类文件。所以也有 Java 模块化相关的属性。
Module
Module
属性很复杂,除了名称、版本、标识信息之外,还有
requires
exports
opens
uses
provides
定义的全部内容,结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | module_name_index | 1 |
u2 | module_flags | 1 |
u2 | module_version_index | 1 |
u2 | requires_count | 1 |
require | requires | requires_count |
u2 | export_count | 1 |
export | exports | exports_count |
u2 | opens_count | 1 |
open | opens | opnes_count |
u2 | uses_count | 1 |
use | uses_index | uses_count |
u2 | provides_count | 1 |
provide | provides | provides_count |
module_flags
可用的标识位为:
标识名称 | 标识位 | 含义 |
---|---|---|
ACC_OPEN | 0x0020 | 表示该模块是开放的 |
ACC_SYNTHETIC | 0x1000 | 表示该模块是编译器自动生成的 |
ACC_MANDATED | 0x8000 | 表示该模块是在源文件中隐式定义的 |
module_version_index
指向常量池,代表了该模块的版本号。
后续的属性记录了 requires
exports
opens
uses
provides
的定义。这里介绍 exports
:
类型 | 名称 | 数量 |
---|---|---|
u2 | exports_index | 1 |
u2 | exports_flags | 1 |
u2 | exports_to_count | 1 |
export | exports_to_index | export_to_count |
export
代表一个被导出的模块包,exports_index
是常量池中
CONSTANT_Package_info
常量的索引。exports_flags
是标识位。可以表示以下状态:
标识名称 | 标识位 | 含义 |
---|---|---|
ACC_SYNTHETIC | 0x1000 | 表示该模块是编译器自动生成的 |
ACC_MANDATED | 0x8000 | 表示该模块是在源文件中隐式定义的 |
exports_to_count
是限定计数器,若为 0
则表示完全开放,如果不为 0,则后跟 export
集合表示允许访问的包。
ModulePackages
ModulePackages
用于描述模块中所有的包,无论是否
export
或 open
,定义如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | package_count | 1 |
u2 | package_index | package_count |
package_index
是常量池中
CONSTANT_Package_info
类型的索引,代表了模块中的一个包。
ModuleMainClass
用于描述模块主类,结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | main_class_index | 1 |
main_class_index
是 CONSTANT_Class_info
类型的索引值,代表了该模块的主类。
注解相关属性
为了储存源码中的注解信息,JDK 5 时,支持了注解特性,类文件同步添加了
RuntimeVisibleAnnotations
RuntimeInvisibleAnnotations
RuntimeVisibleParameterAnnotations
RuntimeVisibleParameterAnnotations
四个属性。JDK 8
时,又新增类型注解,所以又同步添加了
RuntimeVisibleTypeAnnotations
和
RuntimeInvisibleTypeAnnotations
。
这里就介绍一种
RuntimeVisibleAnnotations
。它记录了运行时可见的注解,当通过反射获取时,就是通过这个注解。结构如下所示:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | num_annotations | 1 |
annotation | annotations | num_annotations |
其中的 annotation
的结构又是:
类型 | 名称 | 数量 |
---|---|---|
u2 | type_index | 1 |
u2 | num_element_value_pairs | 1 |
element_value_pair | element_value_pairs | num_element_value_pairs |
type_index
是对常量池中 CONSTANT_Utf8_info
的索引值,以字符串形式表示一个注解。而 element_value_pair
是一个键值对,代表注解的参数和对应值。
本文就到这里,内容比较长,下节将介绍字节码指令(估计也挺长……)。