写在前面

本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和文献引用请见100个问题搞定Java虚拟机

解答

在这里插入图片描述

Class文件是一组以8位字节为基础单位的二进制流,不同的数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有任何空隙存在。

这些数据项目由无符号数和表来存储数据,按照顺序依次是:
1. 魔数和Class文件的版本
2. 常量池
3. 访问标志
4. 类索引、父类索引与接口索引集合
5. 字段表集合
6. 方法表集合
7. 属性表集合

Java代码之所以能够一直保持良好的向后兼容性,就是因为class类文件结构一直比较稳定。

补充

可能很多人觉得了解学习 class 类文件结构对于开发 Java 代码没有什么用处,深度学习了本文,下面的问题你就能自己回答了。

  1. Java为什么能一直保持良好的向后兼容性?
  2. Java虚拟机是怎样处理异常的?
  3. Java代码抛出异常堆栈的时候是如何打印出代码行号的?
  4. Java代码抛出异常堆栈的时候是怎样识别文件名称的?
  5. IDE调试的时候Java程序是如何识别断点的?
  6. IDE调试的时候是如何识别方法参数名称的?
  7. 类变量和实例变量是什么时候赋值的?
  8. Java中泛型是如何实现的?泛型信息是如何保存的?
  9. Java模块化功能是如何实现的?

无符号数

无符号数是基本的数据类型,u1、u2、u4、u8分别表示1个字节、2个字节、4 个字节和8个字节的无符号数。

无符号数可以用来表示数字、索引引用、数量值或者按照UTF-8编码构成字符串。

表用于描述有层次关系的复合结构的数据,由多个无符号数或者其他表构成。

所有表都习惯性地以"_info”结尾。

容器计数器

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用个前置的容量计数器加若干个连续的数据项的形式, 这时称这一系列连续的某一类型的数据为某一类型的集合。

魔数(Magic Number)

每个Class文件的头4个字节称为魔数,即:OXCAFEBABE(咖啡宝贝)

作用

确定这个文件是否是一个合法的Class文件。

因为文件扩展名可以随意地改动,所以使用魔数而不是扩展名来进行识别class文件更加的可靠。

Class文件的版本

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

5、6个字节是次版本号

7、8个字节是主版本号

Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1。

可以推导出 JDK8 的版本号是 52.0

JDK1.2~JDK12之间次版本号都为0。

JDK12以后考虑到一些复杂的特性需要公测,重新启用了次版本号。

高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的 Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

常量池(Constant Pool)

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件的资源仓库

常量池是Class文件结构中与其他数据项目关联最多的数据类型,通常也是占用 Class文件空间最大的数据项目之一,同时它还是在 Class文件中第一个出现的表类型数据项目。

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

与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。

常量池中主要存放两大类常量:字面量和符号引用

字面量(Literal)

字面量类似于Java语言里面的常量,包括文本字符串、声明为final的常量值等。

符号引用(Symbolic References)

符号引用包括了下面6类常量:

  1. 被模块导出或者开放的包(JDK9+)
  2. 类和接口的全限定名
  3. 字段的名称和描述符
  4. 方法的名称和描述符
  5. 方法句柄和方法类型
  6. 动态调用点和动态常量

关于 5、6 请见我的另一篇博客——invokedynamic是如何实现的?

Java代码需要在虚拟机加载Class文件的时候进行动态连接。

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

访问标志(Access Flags)

在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:

  1. 这个Class是类还是接口;
  2. 是否定义为public类型;
  3. 是否定义为abstract类型;
  4. 如果是类的话,是否被声明为final等

类索引、父类索引与接口索引集合(this class,super class and interfaces)

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合。

Class文件中由这三项数据来确定这个类的继承关系

类索引

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

父类索引与继承

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

接口索引集合

接口索引集合就用来描述这个类实现了哪些接口,被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。

字段表集合(Field Info)

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

字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。(方法内部的局部变量存储在栈帧的局部变量表内部。)

在Java中描述一个字段可以包括的信息有:

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

上述这些信息中,每个修饰符都是布尔值,要么有某个修饰符,要么没有,所以很适合使用标志位来表示。

而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

方法表集合

方法表的结构类似于字段表,依次包括了

  1. 访问标志
  2. 名称索引
  3. 描述符索引
  4. 属性表集合

方法表存储的是方法的元数据信息,真正的代码存储在方法属性表中的 Code 属性里面。

属性表集合

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

《Java虚拟机规范》不再要求各个属性表具有严格顺序,只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,

Java虚拟机运行时会忽略掉它不认识的属性。

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
Exceptions 方法表 方法抛出的异常列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 记录源文件名称
ConstantValue 字段表 由 final 关键字定义的常量值
InnerClasses 类文件 内部类列表
Signature(JDK5+) 类、方法表、字段表 用于支持泛型情况下的方法签名
BootstrapMethods(JDK7+) 类文件 用于保存 invokedynamic 指令引用的引导方法限定符
MethodsParameters(JDK8+) 方法表 支持将方法名称编译进 Class 文件中,可以运行时获取
Module/ModulePackages/ModuleMainClass(JDK9+) 支持模块化相关功能

Code

Java程序方法体中的代码经过 Javac 编译器处理后,最终变为字节码指令存储在 Code 属性内。

Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性, 譬如接口或者抽象类中的方法就不存在Code属性,

Code属性是 Class 文件中最重要的一个属性,

如果把一个Java程序中的信息分为代码(code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,

那么在整个 Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

异常表(可选)

在字节码指令之后的是这个方法的显式异常处理表(下文简称异常表)集合,异常表对于Code属性来说并不是必须存在的

关于异常表的详细内容请参考我的另一篇博客——JVM是如何处理异常的?

Exceptions

这里的Exceptions属性是在方法表中与Code属性平级的一项属性,上面的异常表是 Code 属性里面的内容。

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

LineNumberTable(可选)

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。

默认会生成到 Class文件之中,可以在Javac中分别使用-g:none 或-g:lines选项来取消或要求生成这项信息。

如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,

并且在调试程序的时候,也无法按照源码行来设置断点。

LocalVariableTable属性(可选)

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,

默认会生成到 Class文件之中,可以在 Javac 中分别使用-g.none或-g:vars选项来取消或要求生成这项信息。

如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,

这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。

SourceFile属性(可选)

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

可以分别使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息。

在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。

如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值

只有被static关键字修饰的变量(类变量)才可以使用这项属性。

实例变量什么时候进行初始化?

对于实例变量的赋值是在实例构造器方法中进行的;

类变量什么时候初始化?

对于类变量,则有两种方式可以选择

  1. 在类构造器方法中
  2. 使用ConstantValue属性

目前Oracle实现的Java编译器的选择是

  1. 如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成 ConstantValue 属性来进行初始化,
  2. 如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在方法中进行初始化。

InnerClasses属性

InnerClasses 属性用于记录内部类与宿主类之间的关联。

如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成 InnerClasses属性。

Signature属性

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

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

擦除法实现泛型

之所以要专门使用这样一个属性去记录泛型类型,是因为Java语言的泛型采用的是擦除法实现的伪泛型,

在字节码(Code属性)中,泛型信息编译(类型变量、参数化类型)之后都通通被擦除掉。

擦除法的好处与坏处

使用擦除法的好处是实现简单(主要修改Javac编译器,虚拟机内部只做了很少的改动)、非常容易实现Backport(即将一个软件的补丁应用到比此补丁所对应的版本更老的版本的行为),运行期也能够节省一些类型所占的内存空间。

但坏处是运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得到泛型信息。

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

BootstrapMethods 属性

BootstrapMethods属性在JDK1.7发布后增加到了 Class文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。

这个属性用于保存 invokedynamic 指令引用的引导方法限定符。

MethodParameters属性

作用是记录方法的各个形参名称和信息。

编译器可以(编译时加上-parameters 参数)将方法名称写入 Class 文件。

MethodParameters VS LocalVariableTable

LocalVariableTable是Code属性的子属性(抽象方法和接口方法没有方法体就没有对应的 Code属性)。

JDK8 以前要获取方法名称(比如 IDE 的代码提示)只能通过 JavaDoc 得到。

MethodParameters是方法表的属性,和 Code 平级。

模块化相关属性

JDK9的最重要的功能是提供 Java 的模块化功能,因为模块描述文件(module-info.java)最终要编译成一个独立的 Class 文件来存储的。

所以 Class 文件格式也扩展了 Module/ModulePackages/ModuleMainClass 三个属性用于支持 Java 模块化的相关功能。

上一篇 下一篇