Class类文件结构(JAVA8)

目录

Class类文件的结构

 魔数与Class文件的版本

常量池

访问标志

类索引、父类索引与接口索引集合

字段表集合

方法表集合

属性表集合

1.Code属性

2.Exceptions属性

3.LineNumberTable属性

4.LocalVariableTable及LocalVariableTypeTable属性

5.SourceFile及SourceDebugExtension属性

6.ConstantValue属性

7.InnerClasses属性

8.Deprecated及Synthetic属性

9.StackMapTable属性

10.Signature属性

11.BootstrapMethods属性

12.MethodParameters属性

13.模块化相关属性

14.运行时注解相关属性 


各种不同平台的Java虚拟机,以及所有平台都统一支持的程序存储格式——字节码(Byte Code) 是构成平台无关性的基石。

Java虚拟机发展到今天,尤其是在2018年,基于HotSpot扩展而来的GraalVM公开之后更好地支持其他语言运行于Java 虚拟机之上。

实现语言无关性的基础仍然是虚拟机和字节码存储格式。

Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息

图灵完备的字节码格式,保证了任意一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。

作为一个通用的、与机器无关的执行平 台,任何其他语言的实现者都可以将Java虚拟机作为他们语言的运行基础,以Class文件作为他们产品 的交付媒介。

Java语言中的各种语法、关键字、常量变量和运算符号的语义最终都会由多条字节码指令组合来 表达,这决定了字节码指令所能提供的语言描述能力必须比Java语言本身更加强大才行。因此,有一 些Java语言本身无法有效支持的语言特性并不代表在字节码中也无法有效表达出来,这为其他程序语言实现一些有别于Java的语言特性提供了发挥空间。

Java虚拟机提供的语言无关性


Class类文件的结构

Java技术能够一直保持着非常良好的向后兼容性,Class文件结构的稳定功不可没,任何一门程序 语言能够获得商业上的成功,都不可能去做升级版本后,旧版本编译的产品就不再能够运行这种事 情。

任何一个Class文件都对应着唯一的一个类或接口的定义信息[1],但是反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以动态生成,直接送入类加载器中)

[1]:其实也有反例,譬如package-info.class、module-info.class这些文件就属于完全描述性的。

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文 件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前[2]的方式分割 成若干个8个字节进行存储。

  • [2]:这种顺序称为“Big-Endian”,具体顺序是指按高位字节在地址最低位,最低字节在地址最高位来存 储数据,它是SPARC、PowerPC等处理器的默认多字节存储顺序,而x86等处理器则是使用了相反 的“Little-Endian”顺序来存储数据。

根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数 据,这种伪结构中只有两种数据类型:“无符号数”和“表”。后面的解析都要以这两种数据类型为基 础,所以这里笔者必须先解释清楚这两个概念。

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个 字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名 都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据整个Class文件本质上也可以视作是一张表,这张表由图所示的数据项按严格顺序排列构成。

 魔数与Class文件的版本

每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为 一个能被虚拟机接受的Class文件

不仅是Class文件,很多文件格式标准中都有使用魔数来进行身份识别的习惯,譬如图片格式,如GIF或者JPEG等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动。

文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过而且不会引起混淆。

紧接着魔数的4个字节存储的是Class文件的版本号

第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。

高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,因为《Java虚拟机规范》在Class文 件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class 文件。

常量池

紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class 文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它 还是在Class文件中第一个出现的表类型数据项目

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常 量池容量计数值(constant_pool_count)

与Java中语言习惯不同,这个容量计数是从1而不是0开始的常量池容量(偏移地址:0x00000008)为十六进制数0x0013,即十进制的19,这就代表常量池中有18项常量,索引值范围为1~18。

在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。

Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的 容量计数都与一般习惯相同,是从0开始。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。

字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。

而符号引用则属于编译原理方面的概念,主要包括下面几类常量:

  • 被模块导出或者开放的包(Package)
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符
  • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class 文件的时候进行动态连接。

也就是说,在Class文件中不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。

当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池的项目类型

javac <options> <source files>
  -g                         生成所有调试信息
  -g:none                    不生成任何调试信息
  -g:{lines,vars,source}     只生成某些调试信息
  -nowarn                    不生成任何警告
  -verbose                   输出有关编译器正在执行的操作的消息
  -deprecation               输出使用已过时的 API 的源位置
  -classpath <路径>            指定查找用户类文件和注释处理程序的位置
  -cp <路径>                   指定查找用户类文件和注释处理程序的位置
  -sourcepath <路径>           指定查找输入源文件的位置
  -bootclasspath <路径>        覆盖引导类文件的位置
  -extdirs <目录>              覆盖所安装扩展的位置
  -endorseddirs <目录>         覆盖签名的标准路径的位置
  -proc:{none,only}          控制是否执行注释处理和/或编译。
  -processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
  -processorpath <路径>        指定查找注释处理程序的位置
  -parameters                生成元数据以用于方法参数的反射
  -d <目录>                    指定放置生成的类文件的位置
  -s <目录>                    指定放置生成的源文件的位置
  -h <目录>                    指定放置生成的本机标头文件的位置
  -implicit:{none,class}     指定是否为隐式引用文件生成类文件
  -encoding <编码>             指定源文件使用的字符编码
  -source <发行版>              提供与指定发行版的源兼容性
  -target <发行版>              生成特定 VM 版本的类文件
  -profile <配置文件>            请确保使用的 API 在指定的配置文件中可用
  -version                   版本信息
  -help                      输出标准选项的提要
  -A关键字[=值]                  传递给注释处理程序的选项
  -X                         输出非标准选项的提要
  -J<标记>                     直接将 <标记> 传递给运行时系统
  -Werror                    出现警告时终止编译
  @<文件名>                     从文件读取选项和文件名
javac -g TestClass.java
javap -c -verbose TestClass.class
Classfile /E:/workspace/cloud-demo/eurekaserver/src/main/java/com/example/eurekaserver/TestClass.class
  Last modified 2022-10-18; size 412 bytes
  MD5 checksum d1f4f11591a6eb61b40dad46ab199816
  Compiled from "TestClass.java"
public class com.example.eurekaserver.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#19         // com/example/eurekaserver/TestClass.m:I
   #3 = Class              #20            // com/example/eurekaserver/TestClass
   #4 = Class              #21            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/example/eurekaserver/TestClass;
  #14 = Utf8               inc
  #15 = Utf8               ()I
  #16 = Utf8               SourceFile
  #17 = Utf8               TestClass.java
  #18 = NameAndType        #7:#8          // "<init>":()V
  #19 = NameAndType        #5:#6          // m:I
  #20 = Utf8               com/example/eurekaserver/TestClass
  #21 = Utf8               java/lang/Object
{
  public com.example.eurekaserver.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field m:I
         9: return
      LineNumberTable:
        line 3: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/example/eurekaserver/TestClass;

  public int inc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/example/eurekaserver/TestClass;
}
SourceFile: "TestClass.java"
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 000e 5465 7374 436c 6173 732e
6a61 7661 0c00 0700 080c 0005 0006 0100
2263 6f6d 2f65 7861 6d70 6c65 2f65 7572
656b 6173 6572 7665 722f 5465 7374 436c
6173 7301 0010 6a61 7661 2f6c 616e 672f
4f62 6a65 6374 0021 0003 0004 0000 0001
0002 0005 0006 0000 0002 0001 0007 0008
0001 0009 0000 0026 0002 0001 0000 000a
2ab7 0001 2a04 b500 02b1 0000 0001 000a
0000 000a 0002 0000 0003 0004 0004 0001
000b 000c 0001 0009 0000 001f 0002 0001
0000 0007 2ab4 0002 0460 ac00 0000 0100
0a00 0000 0600 0100 0000 0700 0100 0d00
0000 0200 0e

CONSTANT_Class_info型常量的结构

 此类型的常量代表一个类或者接口的符号引用。tag是标志位,它用于区分常量类型;name_index是常量池的索引值,它指向常量池中一个 CONSTANT_Utf8_info类型常量,此常量代表了这个类(或者接口)的全限定名。

CONSTANT_Utf8_info型常量的结构

length值说明了这个UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连 续数据是一个使用UTF-8缩略编码表示的字符串。

UTF-8缩略编码与普通UTF-8编码的区别是:

从'\u0001'到'\u007f'之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,

从'\u0080'到'\u07ff'之间的所有字符的缩略编码用两个字节表示,

从'\u0800'开始到'\uffff'之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。

顺便提一下,由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名 称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。

而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB 英文字符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译。

常量池中的17种数据类型的结构总表

常量池中的17种数据类型的结构总表续1
常量池中的17种数据类型的结构总表续2


访问标志

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract 类型;如果是类的话,是否被声明为final;等等。

访问标志

 access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一 律为零。


类索引、父类索引与接口索引集合

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合 (interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。

由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0。

接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是 extends关键字)后的接口顺序从左到右排列在接口索引集合中。

对于接口索引集合,入口的第一项u2类型的数据为接口计数器(interfaces_count),表示索引表 的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。


字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。

Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。读者可以回忆一下在Java语言中描述一个 字段可以包含哪些信息。

字段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否 强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、 字段名称。

字段表结构

 字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型 。

很明显,由于语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最 多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所导致的。

跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符。

全限定名:“org/fenixsoft/clazz/TestClass;”;简单名称:inc

描述符的作用是用来描述字段 的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类 型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大 写字符来表示,而对象类型则用字符L加对象的全限定名来表示

描述符标识字符含义

 对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型 的二维数组将被记录成“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录成“[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”。

字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不 存在的字段,譬如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字 段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使 用不一样的名称,但是对于Class文件格式来讲,只要两个字段的描述符不是完全相同,那字段重名就 合法的。


方法表集合

方法表的结构如同字段表一样,依 次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表 集合(attributes)几项

因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了 ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对,synchronized、native、strictfp和abstract 关键字可以修饰方法,方法表的访问标志中也相应地增加了ACC_SYNCHRONIZED、 ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。

方法访问标志

方法里的Java代码,经过Javac编译器编译成字节码指令之 后,存放在方法属性表集合中一个名为“Code”的属性里面

与字段表集合相对应地,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出 现来自父类的方法信息。但同样地,有可能会出现由编译器自动添加的方法,最常见的便是类构造 器“()”方法和实例构造器“()”方法。

在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求 必须拥有一个与原方法不同的特征签名[2]。

特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合,也正是因为返回值不会包含在特征签名之中,所以Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式之中,特征签名的范围明显要更大一些, 只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。

[2] 在《Java虚拟机规范》第2版的4.4.4节及《Java语言规范》第3版的8.4.2节中分别都定义了字节码层 面的方法特征签名以及Java代码层面的方法特征签名,Java代码的方法特征签名只包括方法名称、参数 顺序及参数类型而字节码的特征签名还包括方法返回值以及受查异常表,请读者根据上下文语境注 意区分。


属性表集合

Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。

属性表集合的限制稍微宽松一 些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

 对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示, 而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。

属性表结构

1.Code属性

Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。 Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。

Code属性表的结构

attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,此常量值固定为“Code”,它代表了该属性的属性名称。

attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为 6个字节,所以属性值的长度固定为整个属性表长度减去6个字节

max_stack代表了操作数栈(Operand Stack)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。

max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和 returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64 位的数据类型则需要两个变量槽来存放。

方法参数(包括实例方法中的隐藏参数“this”)、显式异常处理程序的参数(Exception Handler Parameter,就是try-catch语句中catch块中所定义的异常)、方法体中定义的局部变量都需要依赖局部变量表来存放。

注意,并不是在方法中用了多少个局部变量,就把这些局部变量所占变量槽数量之和作为max_locals的值,操作数栈和局部变量表直接决定一个该方法的栈 帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费。

Java虚拟机的做法是将局部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出max_locals的大小。

在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现非常简单,仅仅是通过在Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个变量槽位来存放对象实例的引用,所以实例方法参数值从1开始计算。这个处理只对实例方法有效,如果代码清单6-1中的inc()方法被声明为static,那Args_size就不会等于1而是等于0了。

code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度, code是用于存储字节码指令的一系列字节流。每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及后续的参数应当如何解析。

关于code_length,有一件值得注意的事情,虽然它是一个u4类型的长度值,理论上最大值可以达 到2的32次幂,但是《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令,即它 实际只使用了u2的长度,如果超过这个限制,Javac编译器就会拒绝编译。一般来讲,编写Java代码时 只要不是刻意去编写一个超级长的方法来为难编译器,是不太可能超过这个最大值的限制的。但是, 某些特殊情况,例如在编译一个很复杂的JSP文件时,某些JSP编译器会把JSP内容和页面输出的信息归 并于一个方法之中,就有可能因为方法生成字节码超长的原因而导致编译失败。

Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法 体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整 个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

在字节码指令之后的是这个方法的显式异常处理表(下文简称“异常表”)集合,异常表对于Code 属性来说并不是必须存在的。 如果存在异常表,那它的格式应如表6-16所示,包含四个字段,这些字段的含义为:

如果当字节码从第start_pc行[1]到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常 (catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当 catch_type的值为0时,代表任意异常情况都需要转到handler_pc处进行处理。 

// Java源码
public int inc() {
    int x;
    try {
        x = 1;
        return x;
    } catch (Exception e) {
        x = 2;
        return x;
    } finally {
        x = 3;
    }
}
// 编译后的ByteCode字节码及异常表
public int inc();
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 4
    5: iconst_3 // finaly块中的x=3
    6: istore_1
    7: iload 4 // 将returnValue中的值放到栈顶,准备给ireturn返回
    9: ireturn
    10: astore_2 // 给catch中定义的Exception e赋值,存储在变量槽 2中
    11: iconst_2 // catch块中的x=2
    12: istore_1
    13: iload_1 // 保存x到returnValue中,此时x=2
    14: istore 4
    16: iconst_3 // finaly块中的x=3
    17: istore_1
    18: iload 4 // 将returnValue中的值放到栈顶,准备给ireturn返回
    20: ireturn
    21: astore_3 // 如果出现了不属于java.lang.Exception及其子类的异常才会走到这里
    22: iconst_3 // finaly块中的x=3
    23: istore_1
    24: aload_3 // 将异常放置到栈顶,并抛出
    25: athrow
Exception table:
from to target type
0     5  10    Class java/lang/Exception
0     5  21    any
10    16 21    any

编译器为这段Java源码生成了三条异常表记录,对应三条可能出现的代码执行路径。从Java代码的 语义上讲,这三条执行路径分别为: ·如果try语句块中出现属于Exception或其子类的异常,转到catch语句块处理; ·如果try语句块中出现不属于Exception或其子类的异常,转到finally语句块处理; ·如果catch语句块中出现任何异常,转到finally语句块处理。


2.Exceptions属性

这里的Exceptions属性是在方法表中与Code属性平级的一项属性。

Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。

此属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示;exception_index_table是一个指向常量池中 CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。 


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源码行号。 


4.LocalVariableTable及LocalVariableTypeTable属性

LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中使用-g:none或-g:vars选项来取消或要求生成这项信息。

如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,譬如IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。

LocalVariableTable属性结构

 其中local_variable_info项目代表了一个栈帧与源码中的局部变量的关联

local_variable_info项目结构

start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖 的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。

name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了 局部变量的名称以及这个局部变量的描述符

index是这个局部变量在栈帧的局部变量表中变量槽的位置。当这个变量数据类型是64位类型时 (double和long),它占用的变量槽为index和index+1两个。

顺便提一下,在JDK 5引入泛型之后,LocalVariableTable属性增加了一个“姐妹属性”—— LocalVariableTypeTable。这个新增的属性结构与LocalVariableTable非常相似,仅仅是把记录的字段描述符的descriptor_index替换成了字段的特征签名(Signature)。

对于非泛型类型来说,描述符和特征签名能描述的信息是能吻合一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉[3],描 述符就不能准确描述泛型类型了。因此出现了LocalVariableTypeTable属性,使用字段的特征签名来完成泛型的描述。


5.SourceFile及SourceDebugExtension属性

SourceFile属性用于记录生成这个Class文件的源码文件名称

这个属性也是可选的,可以使用Javac 的-g:none或-g:source选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。

SourceFile属性结构

 sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名。

为了方便在编译器和动态生成的Class中加入供程序员使用的自定义内容,在JDK 5时,新增了 SourceDebugExtension属性用于存储额外的代码调试信息。典型的场景是在进行JSP文件调试时,无法 通过Java堆栈来定位到JSP文件的行号。

JSR 45提案为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension属性就可以用于存储这个标准所新加入的调试信息,譬如让程序员能够快速从异常堆栈中定位出原始JSP中出现问题的 行号。

其中debug_extension存储的就是额外的调试信息,是一组通过变长UTF-8格式来表示的字符串。一 个类中最多只允许存在一个SourceDebugExtension属性。

6.ConstantValue属性

ConstantValue属性的作用是 通知虚拟机自动为静态变量赋值
只有被static关键字修饰的变量(类 变量)才可以使用这项属性。
类似“int x=123”和“static int x=123”这样的变量定义在Java程序里面是非常常见的事情,但虚拟机对这两种变量赋值的方式和时刻都有所不同。
对非static类型的变量(也就是实例变量)的赋值是在实例构造器()方法中进行的;
而对于类变量,则有两种方式可以选择:在类构造器()方法中或者使用ConstantValue属性。
目前Oracle公司实现的Javac编译器的选择是,
如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就将会生成ConstantValue属性来进行初始化;
如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>()方法中进行初始化。

虽然有final关键字才更符合“ConstantValue”的语义,但《Java虚拟机规范》中并没有强制要求字段 必须设置ACC_FINAL标志,只要求有ConstantValue属性的字段必须设置ACC_STATIC标志而已,对 final关键字的要求是Javac编译器自己加入的限制。

而对ConstantValue的属性值只能限于基本类型和 String这点,其实并不能算是什么限制,这是理所当然的结果。因为此属性的属性值只是一个常量池的索引号,由于Class文件格式的常量类型中只有与基本属性和字符串相对应的字面量,所以就算 ConstantValue属性想支持别的类型也无能为力。

ConstantValue属性结构

从数据结构中可以看出ConstantValue属性是一个定长属性,它的attribute_length数据项值必须固定为2。constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面 量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、 CONSTANT_Integer_info和CONSTANT_String_info常量中的一种。


7.InnerClasses属性

InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将 会为它以及它所包含的内部类生成InnerClasses属性。

InnerClasses属性结构

数据项number_of_classes代表需要记录多少个内部类信息,每一个内部类的信息都由一个 inner_classes_info表进行描述。

inner_classes_info表的结构

inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分别代表了内部类和宿主类的符号引用。

inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称, 如果是匿名内部类,这项值为0。

inner_class_access_flags是内部类的访问标志,类似于类的access_flags


8.Deprecated及Synthetic属性

Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。

Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通 过代码中使用“@deprecated”注解进行设置。

Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的,在 JDK 5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的 ACC_SYNTHETIC标志位。

编译器通过生成一些在源代码中不存在的Synthetic方法、字段甚至是整个类的方式,实现了越权访问(越过private修饰器)或其他绕开了语言限制的功能,这可以算是一种早期优化的技巧,其中最典型的例子就是枚举类中自动生成的枚举元素数组和嵌套类的桥接方法 (Bridge Method)。

所有由不属于用户代码产生的类、方法及字段都应当至少设置Synthetic属性或者 ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器“<init>()”方法和类构造器“<clinit>()”方法。

Deprecated及Synthetic属性结构

9.StackMapTable属性

StackMapTable属性在JDK 6增加到Class文件规范之中,它是一个相当复杂的变长属性,位于Code 属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的 类型推导验证器。

新的验证器在同样能保证Class文件合法性的前提下,省略了在运行 期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而在编译阶段将一系列的验证类型 (Verification Type)直接记录在Class文件之中,通过检查这些验证类型代替了类型推导过程,从而大 幅提升了字节码验证的性能。

StackMapTable属性中包含零至多个栈映射帧(Stack Map Frame),每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型

类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。


10.Signature属性

Signature属性在JDK 5增加到Class文件规范之中,它是一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中。

在JDK 5里面大幅增强了Java语言的语法,在此之后,任何类、接口、初 始化方法或成员的泛型签名如果包含了类型变量(Type Variable)或参数化类型(Parameterized Type),则Signature属性会为它记录泛型签名信息。

之所以要专门使用这样一个属性去记录泛型类型,是因为Java语言的泛型采用的是擦除法实现的伪泛型字节码(Code属性)中所有的泛型信息编译(类型变量、参数化类型)在编译之后都通通被擦除掉

使用擦除法的好处是实现简单(主要修改 Javac编译器,虚拟机内部只做了很少的改动)、非常容易实现Backport,运行期也能够节省一些类型所占的内存空间。但坏处是运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得泛型信息。

Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取的泛型类型,最终的数据来源也是这个属性。

Signature属性结构

 其中signature_index项的值必须是一个对常量池的有效索引。

常量池在该索引处的项必须是 CONSTANT_Utf8_info结构,表示类签名或方法类型签名或字段类型签名。

如果当前的Signature属性 是类文件的属性,则这个结构表示类签名,

如果当前的Signature属性是方法表的属性,则这个结构表示方法类型签名,

如果当前Signature属性是字段表的属性,则这个结构表示字段类型签名。


11.BootstrapMethods属性

BootstrapMethods属性在JDK 7时增加到Class文件规范之中,它是一个复杂的变长属性,位于类 文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。

如果某个类文件结构的常量池中曾经出现 过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确的 BootstrapMethods属性,另外,即使CONSTANT_InvokeDynamic_info类型的常量在常量池中出现过多 次,类文件的属性表中最多也只能有一个BootstrapMethods属性。

BootstrapMethods属性结构

  BootstrapMethods属性里,num_bootstrap_methods项的值给出了bootstrap_methods[]数组中的引导方法限定符的数量。

而bootstrap_methods[]数组的每个成员包含了一个指向常量池 CONSTANT_MethodHandle结构的索引值,它代表了一个引导方法。还包含了这个引导方法静态参数的序列(可能为空)。bootstrap_methods[]数组的每个成员必须包含以下三项内容:

bootstrap_method属性结构
  • bootstrap_method_ref:bootstrap_method_ref项的值必须是一个对常量池的有效索引。常量池在该索引处的值必须是一个CONSTANT_MethodHandle_info结构。
  • num_bootstrap_arguments:num_bootstrap_arguments项的值给出了bootstrap_argu-ments[]数组成员的数量。
  • bootstrap_arguments[]:bootstrap_arguments[]数组的每个成员必须是一个对常量池的有效索引。
    • 常量池在该索引必须是下列结构之一:CONSTANT_String_info、CONSTANT_Class_info、 CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、 CONSTANT_Double_info、CONSTANT_MethodHandle_info或CONSTANT_MethodType_info。

12.MethodParameters属性

MethodParameters是在JDK 8时新加入到Class文件格式中的,它是一个用在方法表中的变长属性。 MethodParameters的作用是记录方法的各个形参名称和信息

JDK 8中新增的这个属性,使得编译器可以 (编译时加上-parameters参数)将方法名称也写进Class文件中,而且MethodParameters是方法表的属 性,与Code属性平级的,可以运行时通过反射API获取。

MethodParameters属性结构
parameter属性结构

 其中,name_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,代表了该参数的名 称。

而access_flags是参数的状态指示器,它可以包含以下三种状态中的一种或多种:

  • 0x0010(ACC_FINAL):表示该参数被final修饰。
  • 0x1000(ACC_SYNTHETIC):表示该参数并未出现在源文件中,是编译器自动生成的。
  • 0x8000(ACC_MANDATED):表示该参数是在源文件中隐式定义的。Java语言中的典型场景是 this关键字。

13.模块化相关属性

JDK 9的一个重量级功能是Java的模块化功能,因为模块描述文件(module-info.java)最终是要编译成一个独立的Class文件来存储的,所以,Class文件格式也扩展了Module、ModulePackages和 ModuleMainClass三个属性用于支持Java模块化相关功能。

 Module属性是一个非常复杂的变长属性,除了表示该模块的名称、版本、标志信息以外,还存储了这个模块requires、exports、opens、uses和provides定义的全部内容

其中,module_name_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,代表了该模块 的名称。而module_flags是模块的状态指示器,它可以包含以下三种状态中的一种或多种: 

  • 0x0020(ACC_OPEN):表示该模块是开放的。
  • 0x1000(ACC_SYNTHETIC):表示该模块并未出现在源文件中,是编译器自动生成的。
  • 0x8000(ACC_MANDATED):表示该模块是在源文件中隐式定义的。

module_version_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,代表了该模块的版 本号。

仅介绍其中的exports属性,后续的几个属性分别记录了模块的requires、exports、opens、uses和provides定义,它们的结构是基本相似的

exports属性结构

exports属性的每一元素都代表一个被模块所导出的包,exports_index是一个指向常量池 CONSTANT_Package_info常量的索引值,代表了被该模块导出的包。

exports_flags是该导出包的状态指示器,它可以包含以下两种状态中的一种或多种:

  • 0x1000(ACC_SYNTHETIC):表示该导出包并未出现在源文件中,是编译器自动生成的。
  • 0x8000(ACC_MANDATED):表示该导出包是在源文件中隐式定义的。

exports_to_count是该导出包的限定计数器,如果这个计数器为零,这说明该导出包是无限定的 (Unqualified),即完全开放的,任何其他模块都可以访问该包中所有内容。如果该计数器不为零, 则后面的exports_to_index是以计数器值为长度的数组,每个数组元素都是一个指向常量池中 CONSTANT_Module_info常量的索引值,代表着只有在这个数组范围内的模块才被允许访问该导出包的内容。

ModulePackages是另一个用于支持Java模块化的变长属性,它用于描述该模块中所有的包,不论是不是被export或者open的。

ModulePackages属性结构

package_count是package_index数组的计数器,package_index中每个元素都是指向常量池 CONSTANT_Package_info常量的索引值,代表了当前模块中的一个包。

最后一个ModuleMainClass属性是一个定长属性,用于确定该模块的主类(Main Class)

ModuleMainClass属性结构

其中,main_class_index是一个指向常量池CONSTANT_Class_info常量的索引值,代表了该模块的主类。


14.运行时注解相关属性 

JDK5提供了对注解(Annotation)的支 持。为了存储源码中注解信息,Class文件同步增加了RuntimeVisibleAnnotations、 RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations和RuntimeInvisibleParameterAnnotations四个属性。

到了JDK 8时期,进一步加强了Java语言的注解使用范围,又新增类型注解 (JSR 308),所以Class文件中也同步增加了RuntimeVisibleTypeAnnotations和 RuntimeInvisibleTypeAnnotations两个属性。

由于这六个属性不论结构还是功能都比较雷同,因此我们 把它们合并到一起,以RuntimeVisibleAnnotations为代表进行介绍。

RuntimeVisibleAnnotations是一个变长属性,它记录了类、字段或方法的声明上记录运行时可见注解,当我们使用反射API来获取类、字段或方法上的注解时,返回值就是通过这个属性来取到的。

RuntimeVisibleAnnotations属性结构

num_annotations是annotations数组的计数器,annotations中每个元素都代表了一个运行时可见的注解,注解在Class文件中以annotation结构来存储。

annotation属性结构

type_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,该常量应以字段描述符的形式 表示一个注解。

num_element_value_pairs是element_value_pairs数组的计数器,element_value_pairs中每个元素都是一个键值对,代表该注解的参数和值

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值