Back
Featured image of post JVM 学习笔记 10 - 类文件结构

JVM 学习笔记 10 - 类文件结构

导航页

上篇文章结束了自动内存管理的篇章,本文开始将会讲解类文件、类加载相关的内容。

Java 类文件的结构一直很稳定。(虽说以类文件代称,但实质上不一定是文件形式,可以是任意字节流)

和其他二进制格式类似,Class 文件没有任何分隔符。Class 文件使用 Big-Endian 编码。结构如下所示,u 表示无符号,后面的数字是字节数量:

类型名称数量
u4Magic1
u2小版本号1
u2大版本号1
u2常量池个数1
cp_info常量池常量池个数 - 1
u2访问 flags1
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 VersionBytecode Version
Java 1.045.0
Java 1.145.3
Java 1.246.0
Java 1.347.0
Java 1.448.0
Java 549.0
Java 650.0
Java 751.0
Java 852.0
Java 953.0
Java 1054.0
Java 1155.0
Java 1256.0
Java 1357.0
Java 1458.0
Java 1559.0
Java 1660.0
Java 1761.0
Java 1862.0
Java 1963.0
Java 2064.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_PUBLIC0x0001标识 public 类型
ACC_FINAL0x0010标识 final 类型,只有类可设置
ACC_SUPER0x0020是否允许使用 invokespecial 字节码的新语义(因为在 JDK 1.0.2 发生过改变)
ACC_INTERFACE0x0200标识接口
ACC_ABSTRACT0x0400标识 abstract 类型,对于接口和抽象类为真
ACC_SYNTHETIC0x1000标识非用户代码生成的类
ACC_ANNOTATION0x2000标识注解
ACC_ENUM0x4000标识枚举
ACC_MODULE0x8000标识模块

一共有 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)、类型(基本类型、对象、数组)、字段名称。上述信息中,修饰符非常适合用标识位表示,而名称和类型则应该放到常量池。

字段表的结构如下

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

其中 access_flags 包括:

名称标识位解释
ACC_PUBLIC0x0001是否 public
ACC_PRIVATE0x0002是否 private
ACC_PROTECTED0x0004是否 protected
ACC_STATIC0x0008是否 static
ACC_FINAL0x0010是否 final
ACC_VOLATILE0x0040是否 volatile
ACC_TRANSIENT0x0080是否 transient
ACC_SYNTHETIC0x1000是否不是由用户代码生成的
ACC_ENUM0x4000类型是否为枚举

显然的,ACC_PUBLIC ACC_PRIVATE ACC_PROTECTED 只能三者选一个。同时 ACC_VOLATILEACC_FINAL 也不能同时出现。和之前一样,未使用的标识位保持 0。

紧随 access_flags 后面的就是 name_indexdescriptor_index,查常量池即可知道名称和描述符了。此时可以介绍几个概念:

  • 全限定名称(qualified name):如 moe/sdl/clazz/TestClass 就是这个类的全限定名称,. 换为 / 即可。一般还会在结尾加上 ; 避免混淆。
  • 简单名称(simple name):没有类型和参数修饰的方法、字段。比如 inc() 和字段 m 的简单名称就分别是 incm
  • 描述符(descriptor):用于描述字段的类型,方法的参数列表和返回值。标识字符可以见下表:
标识字符含义标识字符含义
B基本类型 byteJ基本类型 long
C基本类型 charS基本类型 short
D基本类型 doubleZ基本类型 boolean
F基本类型 floatV特殊类型 void
I基本类型 intL对象类型,如 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 中字段无法重载,但类文件中是允许的。只要描述符不完全相同,就允许重载。

方法表

字段表结构和方法表结构是一样的:

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

不一样的是标识位:

标识名称标识位含义
ACC_PUBLIC0x0001是否 public
ACC_PRIVATE0x0002是否 private
ACC_PROTECTED0x0004是否 protected
ACC_STATIC0x0008是否 final
ACC_SYNCHRONIZED0x0020是否 synchronized
ACC_BRIDGE0x0040是否是编译器产生的桥接方法
ACC_VARARGS0x0080是否接受不定参数
ACC_NATIVE0x0100是否 native
ACC_ABSTARCT0x0400是否 abstract
ACC_STRICT0x0800是否 strictfp
ACC_SYNTHETIC0x1000是否由编译器自动产生

方法表只存储元数据,真正的逻辑存储在属性表名为 Code 的属性里。

和属性表类似,如果父类方法没有被子类重写,是不会出现来自父类的信息的。但可能出现由编译器自动添加的方法,比如类构造器 <clinit>() 和实例构造器 <init>()

同样的,Java 不允许只有返回值不同的方法重载。但在类文件中这是允许的。

属性表

属性表(attribute_info)之前提到过几次了,Class 文件、字段表、方法表都可以携带自己的属性表。

属性表要求比较宽松,只要不和现有属性名重复,任何人都可以在属性表中定义自己的信息。JVM 会忽略掉不认识的属性。JVM 中定义的属性很多,这里就不列全表了,有兴趣的可以自己查看《JVM 规范》对应的章节

对于每个属性,都能看作以下定义:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1infoattribute_length

除了名称和长度需要明确以外,其他部分完全是自定义的。

Code

JVM 程序经过编译后,其中的代码逻辑就储存在 Code 属性中。然而并非所有方法表都必须存在该属性,比如接口、抽象类中的虚方法,就不需要 Code 属性。Code 属性结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2max_stack1
u2max_locals1
u4code_length1
u1codecode_length
u2exception_table_length1
exception_infoexception_tableexception_table_length
u2attributes_count1
attribute_infoattributesattributes_count

attribute_name_index 是指向常量池的索引,固定为 Code

max_stack 代表了操作数栈(Operand Stack)的最大深度。JVM 会根据该值分配栈帧(Stack Frame)。

max_locals 代表了局部变量需要的储存空间。max_locals 的单位是变量槽。对于 byte char int 等长度不超过 32 bit 的数据类型,只占用一个变量槽。而 longdouble 需要两个变量槽。方法参数(包括隐藏参数 this)、异常处理程序的参数(Exception Handler Parameter,即 try-catch 语句中定义的异常)、方法体中定义的局部变量等,都需要局部变量表来存放。此外,并非用了多少个变量,就将其数量作为 max_locals 的值,因为不必要的栈深度和变量槽数量会浪费内存。Javac 编译器会根据变量作用域来分配变量槽,而非单纯的加和。

code_lengthcode 就是编译后的字节码指令。每个指令都是 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,需要显式写明要传递 selfthis,然而值得一提的是,Python 却不用有无 self 来判断是否 static。需要打上类似 @classmethod @staticmethod 之类蹩脚的注解。

如果 inc() 被声明为 staticargs_size 就会变为 0。

若代码中使用了 try-catch 那么在字节码之后,就是异常表了。结构如下:

类型名称数量类型名称数量
u2start_pc1u2handler_pc1
u2end_pc1u2catch_type1

Java 正是使用异常表实现异常处理机制的,不妨再次查看字节码:

public class TestClass {
  public int inc() {
    int x;
    try {
      x = 1;
      return x;
    } catch (Exception e) {
      x = 2;
      return x;
    } finally {
      x = 3;
    }
  }
}
{ 
  // ...
  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 关键字后列举的异常,结构见表:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_exceptions1
u2exception_index_tablenumber_of_exception

其中 exception_index_table 指向常量池中 CONSTANT_class_info 类型的常量。

LineNumberTable

用于描述 Java 源码行号和字节码行号之间的对应关系。不是运行时的必要属性,但对错误处理和调试很有帮助。因此会默认生成,可以用 -g:none-g:lines 关闭。结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2line_number_table_length1
line_number_infoline_number_tableline_number_table_length

line_number_info 类型包含 start_pcline_number 两个 u2 数据,前者是字节码行号,后者是源码行号。

LocalVariableTable / LocalVariableTypeTable

LocalVariableTable 的结构和 LineNumberTable 类似,不过将 line_number_info 换为了 local_variable_info

类型名称数量
u2start_pc1
u2length1
u2name_index1
u2descriptor_index1
u2index1

LocalVariableTable 也不是运行时必须的属性。但默认会生成,也可以通过 -g:none-g:vars 关闭。如果没有生成该属性,最大的影响是,在别人调用时参数名称将会变为 arg1 arg2 等占位符。

start_pclength 共同表示了局部变量的作用域。

name_indexdescriptor_index 分别代表了变量名称和类型。

index 代表在局部变量表中变量槽的位置。若为 64 位的数据类型(double long),那么会占用 indexindex + 1 两个变量槽。

LocalVariableTypeTable 是在 JDK 1.5 加入泛型时加入的。用于记录泛型参数。只是将 descriptor_index 这一项换成了 signature

Source File / SourceDebugExtension

也是可选的,用于记录源代码文件名称。可以通过 -g:none-g:source 关闭。

如果不生成这项属性,抛出异常时不能显示出代码所属的文件名。该属性结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2sourcefile_index1

sourcefile_index 是常量池中的 CONSTANT_Utf8_info 类型常量的索引,即源码文件名。

SourceDebugExtension 是在 JDK 1.5 时加入的。用于储存额外的调试信息。结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1debug_extensionattribute_length

Constant Value

作用为通知 JVM 为静态变量赋值。只有 static 关键字的变量可以使用该属性。对于实例变量,可以在 <init>() 方法中实例化。而对于类变量,可以在 <clinit>() 中或使用 Constant Value 初始化。Oracle 的 Javac 编译器使用 Constant Value 来初始化 static final 且为基本类型或字符串类型的常量。如果不被 final 修饰,或者不是基本类型或字符串,就会在 <clinit>() 方法中初始化。

Constant Value 属性结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2constantvalue_index1

InnerClasses

用于记录内部类和宿主类的关系。如果一个类有 inner class,则会有该属性。结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_classes1
inner_classes_infoinner_classesnumber_of_classes

inner_classes_info 的结构如下:

类型名称数量介绍
u2inner_class_info_index1指向常量池,内部类的符号引用
u2outer_class_info_index1指向常量池,宿主类的符号引用
u2inner_name_index1指向常量池,内部类的名称
u2inner_class_access_flags1内部类的访问标识

内部类的访问标识:

标识名称标识位含义
ACC_PUBLIC0x0001是否 public
ACC_PRIVATE0x0002是否 private
ACC_PROTECTED0x0004是否 protected
ACC_STATIC0x0008是否 static
ACC_FINAL0x0010是否 final
ACC_INTERFACE0x0020是否为 interface
ACC_ABSTRACT0x0400是否 abstract
ACC_SYNTHETIC0x1000是否并发用户代码生成的
ACC_ANNOTAITON0x2000是否注解
ACC_ENUM0x4000是否枚举

Deprecated / Synthetic

都属于布尔属性,状态只存在有或无的区别,没有具体属性值。

Deprecated 用于表示类、字段、或方法已经不再被推荐使用。

Synthetic 用于表示该方法并非从源码生成的。一般而言,至少需要设置该属性或 ACC_SYNTHETIC 之一,唯一例外有 <init>()<clinit>()

结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1

其中 attribute_length 肯定是为 0 的。

StackMapTable

该属性在 JDK 6 中加入,用于减少类加载验证步骤的耗时。

其中包括 0 到多个栈映射帧(Stack Map Frame),每个栈映射帧都代表了一个字节码偏移量,用于表示执行到该字节码时,局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈,来确定字节码指令是否满足逻辑约束。

结构如下:

类型名称数量
u2inner_class_info_index1
u2outer_class_info_index1
u2number_of_entries1
stack_map_framestack_map_frame_entriesnumber_of_entries

在类文件 50.0(JDK 7)以上,如果没有 StackMapTable 属性,则默认为 number_of_entries = 0 的空表。一个 Code 属性至多有一个 StackMapTable 属性,否则会抛出 ClassFormatError

Signature

众所周知 Java 的泛型是被擦除了的。然而并非完全被擦除,某些情况下可以通过反射获取一部分信息。最终的来源也就是这里。比如以下代码(Kotlin):

abstract class Reflection<out T> {
    init {
        println(javaClass.genericSuperclass)
    }

    abstract fun test(): T
}

class Impl() : Reflection<Impl>() {
    override fun test(): Impl = this
}

fun main() {
    println(Impl())
}

// Output:
// Reflection<Impl>
// Impl@1fb3ebeb

可以看到成功输出了泛型参数。

Signature 属性结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2signature_index1

signature_index 对应常量池的有效索引,为 CONSTANT_Utf8_info 类型。

BootstrapMethods

该属性是一个复杂的变长属性。用于保存 invokedynamic 指令引用的引导方法限定服。

这个属性的作用涉及到 InvokeDynamic 的原理,在此先略过。

值得一提的是,JDK 7 中虽然有此属性,但是没有 invokedynamic 所以没什么用。JDK 8 中才算正式开始使用了。

它的结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2num_bootstrap_methods1
bootstrap_methodbootstrap_methodsnum_bootstrap_methods

其中 bootstrap_method 的结构为:

类型名称数量介绍
u2bootstrap_method_ref1对常量池的索引,CONSTANT_M ethodHandle_info 类型
U2num_bootstrap_arguments1
u2bootstrap_argumentsnum_bootstrap_arguments对常量池的索引,可以为基本类型信息+String信息+MethodHandle+MethodType

MethodParameters

该属性于 JDK 8 时加入,用于记录各个形参的信息。

最初,类文件出于节省空间的考虑,不储存方法参数名,因为运行时名字不重要,在源码中妥善命名即可。但这给程序二次传播带来了不便,无法使用 IDE 的智能提示功能。

LocalVariableTable 不是已经解决了这个问题吗?其实没有完全解决。LocalVariableTableCode 的子属性,如果没有方法体,自然就不会有局部变量表,对于抽象方法和接口,这不太友好。所以有了这个属性。

结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2parameters_count1
parameterparametersparameters_count

parameter 的结构如下:

类型名称数量
u2name_index1
u2access_flags1

access_flags 的标识位有以下:

标识名称标识位含义
ACC_FINAL0x0010是否 final
ACC_SYNTHETIC0x1000表示是编译器自动生成的
ACC_MANDATED0x8000表示是在源文件中隐式定义的,比如 this

模块化相关属性

由于 module-info.java 最终要编译成一个独立的类文件。所以也有 Java 模块化相关的属性。

Module

Module 属性很复杂,除了名称、版本、标识信息之外,还有 requires exports opens uses provides 定义的全部内容,结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2module_name_index1
u2module_flags1
u2module_version_index1
u2requires_count1
requirerequiresrequires_count
u2export_count1
exportexportsexports_count
u2opens_count1
openopensopnes_count
u2uses_count1
useuses_indexuses_count
u2provides_count1
provideprovidesprovides_count

module_flags 可用的标识位为:

标识名称标识位含义
ACC_OPEN0x0020表示该模块是开放的
ACC_SYNTHETIC0x1000表示该模块是编译器自动生成的
ACC_MANDATED0x8000表示该模块是在源文件中隐式定义的

module_version_index 指向常量池,代表了该模块的版本号。

后续的属性记录了 requires exports opens uses provides 的定义。这里介绍 exports

类型名称数量
u2exports_index1
u2exports_flags1
u2exports_to_count1
exportexports_to_indexexport_to_count

export 代表一个被导出的模块包,exports_index 是常量池中 CONSTANT_Package_info 常量的索引。exports_flags 是标识位。可以表示以下状态:

标识名称标识位含义
ACC_SYNTHETIC0x1000表示该模块是编译器自动生成的
ACC_MANDATED0x8000表示该模块是在源文件中隐式定义的

exports_to_count 是限定计数器,若为 0 则表示完全开放,如果不为 0,则后跟 export 集合表示允许访问的包。

ModulePackages

ModulePackages 用于描述模块中所有的包,无论是否 exportopen,定义如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2package_count1
u2package_indexpackage_count

package_index 是常量池中 CONSTANT_Package_info 类型的索引,代表了模块中的一个包。

ModuleMainClass

用于描述模块主类,结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2main_class_index1

main_class_indexCONSTANT_Class_info 类型的索引值,代表了该模块的主类。

注解相关属性

为了储存源码中的注解信息,JDK 5 时,支持了注解特性,类文件同步添加了 RuntimeVisibleAnnotations RuntimeInvisibleAnnotations RuntimeVisibleParameterAnnotations RuntimeVisibleParameterAnnotations 四个属性。JDK 8 时,又新增类型注解,所以又同步添加了 RuntimeVisibleTypeAnnotationsRuntimeInvisibleTypeAnnotations

这里就介绍一种 RuntimeVisibleAnnotations。它记录了运行时可见的注解,当通过反射获取时,就是通过这个注解。结构如下所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2num_annotations1
annotationannotationsnum_annotations

其中的 annotation 的结构又是:

类型名称数量
u2type_index1
u2num_element_value_pairs1
element_value_pairelement_value_pairsnum_element_value_pairs

type_index 是对常量池中 CONSTANT_Utf8_info 的索引值,以字符串形式表示一个注解。而 element_value_pair 是一个键值对,代表注解的参数和对应值。


本文就到这里,内容比较长,下节将介绍字节码指令(估计也挺长……)。

comments powered by Disqus