深入理解Java虚拟机(2)之十-类文件结构

Class类文件的结构

Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上的空间的数据项时,则会按照高位在前的方式分隔成若干个8位字节进行存储。

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据结构:无符号数

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

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

对于一个简单的Java文件:

1
2
3
4
5
6
public class Java4 {

public static void main(String[] args) {
System.out.println("Hello World");
}
}

编译之后用010Editor打开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 	
00000000: CA FE BA BE 00 00 00 34 00 1D 0A 00 06 00 0F 09 J~:>...4........
00000010: 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07 ................
00000020: 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 .....<init>...()
00000030: 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E V...Code...LineN
00000040: 75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D 61 69 umberTable...mai
00000050: 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 n...([Ljava/lang
00000060: 2F 53 74 72 69 6E 67 3B 29 56 01 00 0A 53 6F 75 /String;)V...Sou
00000070: 72 63 65 46 69 6C 65 01 00 0A 4A 61 76 61 34 2E rceFile...Java4.
00000080: 6A 61 76 61 0C 00 07 00 08 07 00 17 0C 00 18 00 java............
00000090: 19 01 00 0B 48 65 6C 6C 6F 20 57 6F 72 6C 64 07 ....Hello.World.
000000a0: 00 1A 0C 00 1B 00 1C 01 00 05 4A 61 76 61 34 01 ..........Java4.
000000b0: 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 ..java/lang/Obje
000000c0: 63 74 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 53 ct...java/lang/S
000000d0: 79 73 74 65 6D 01 00 03 6F 75 74 01 00 15 4C 6A ystem...out...Lj
000000e0: 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 ava/io/PrintStre
000000f0: 61 6D 3B 01 00 13 6A 61 76 61 2F 69 6F 2F 50 72 am;...java/io/Pr
00000100: 69 6E 74 53 74 72 65 61 6D 01 00 07 70 72 69 6E intStream...prin
00000110: 74 6C 6E 01 00 15 28 4C 6A 61 76 61 2F 6C 61 6E tln...(Ljava/lan
00000120: 67 2F 53 74 72 69 6E 67 3B 29 56 00 21 00 05 00 g/String;)V.!...
00000130: 06 00 00 00 00 00 02 00 01 00 07 00 08 00 01 00 ................
00000140: 09 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7 00 .............*7.
00000150: 01 B1 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 .1..............
00000160: 00 01 00 09 00 0B 00 0C 00 01 00 09 00 00 00 25 ...............%
00000170: 00 02 00 01 00 00 00 09 B2 00 02 12 03 B6 00 04 ........2....6..
00000180: B1 00 00 00 01 00 0A 00 00 00 0A 00 02 00 00 00 1...............
00000190: 04 00 08 00 05 00 01 00 0D 00 00 00 02 00 0E ...............

Class文件结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic; // 魔数
u2 minor_version; // 次版本号
u2 major_version; // 主版本号
u2 constant_pool_count; // 常量池容量计数器
cp_info constant_pool[constant_pool_count-1]; // 常量池
u2 access_flags; // 访问标志
u2 this_class; // 类索引
u2 super_class; // 父类索引
u2 interfaces_count; // 接口索引集合
u2 interfaces[interfaces_count]; // 接口索引集合
u2 fields_count; // 字段表集合
field_info fields[fields_count]; // 字段表集合
u2 methods_count; // 方法表集合
method_info methods[methods_count]; // 方法表集合
u2 attributes_count; // 属性表集合
attribute_info attributes[attributes_count]; // 属性表集合
}

1、魔数(magic)与Class文件的版本(minor_versionmajor_version)

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

第5和6个字节是次版本号。

第7和8个字节是主版本号。

2、常量池(constant_pool)

紧跟主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时也是Class文件中第一个出现的表类型数据项目。

由于常量池中常量的数量不是固定的,所以常量池入口需要放置一项u2类型的数据,代表常量池中常量计数器(constant_pool_count)。与Java中语言习惯不一样,这个容量计数计数器是从1开始的而不是0开始。在Class文件格式规范制定之时,设计者将第0项空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特殊情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以把索引值置为0来表示。Class文件中只有常量池的容量计数是从1开始。

常量池中主要存放两大类常量:

  • 字面量(Literal):接近于Java语言层面的常量的概念,如文本字符串、声明为final的常量值等。

  • 符号引用(Symbolic References):属于编译原理方面的概念:

    类和接口的全限定名(Fully Qualified Name)

    字段的名称和描述符(Descriptor)

    方法的名称和描述符

    Java在进行Javac编译的时候,是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

3、访问标志(access_flags)

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

4、类索引(this_class)、父类索引(super_class)与接口索引集合(interfaces)

类索引(this_class)、父类索引(super_class)和接口索引集合(interfaces)都是来确定这个类的继承关系。

5、字段表集合(fields)

字段表(fields)用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

可包括的信息有:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型,这些都没法固定,只能引用常量池的常量来描述。

6、方法表集合(methods)

Class文件存储格式对方法的描述与对字段的描述几乎采用完全一致的方式。仅在访问标志和属性表集合的可选项中有所区别。

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

7、属性表集合(attributes)

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

  1. Code属性:Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性中。Code属性出现在方法表的属性集合之中,但并非所有分方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。
  1. Exceptions属性:该属性的作用是列举出方法中可能抛出的受查异常(Checked Exceptions),也就是方法描述时在throws关键字后列举的异常。
  1. LineNumberTable属性:该属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none-g:lines选项来取消或要求生成这项信息。如果选择不生成该属性,对程序运行产生最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
  1. LocalVariableTable属性:该属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none-g:vars选项来取消或要求生成这项信息。如果没有这项属性,最大的影响就是当其他人引用这个方法时,所有的参数都会丢失,IDE将会使用诸如arg0,arg1之类的占位符代替原来的参数名,这对程序运行没有影响,但对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获取参数值。

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

  1. InnerClasses属性:该属性用于记录内部类与宿主类之间的关联。如果一个类定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性。
  1. Deprecated及Synthetic属性:这两个属性都是属于标志类型的布尔属性。

    • Deprecated用于表示某个类、字段或者方法,已经被程序坐着定位不再推荐使用,可以通过在代码中使用@Deprecated注释进行设置。
    • Synthetic代表此字段或者方法不是由Java源码直接产生的,而是由编译器自行添加的,在JDK 1.5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们的访问标志中的ACC_SYNTHETIC标志位。
  1. StackMapTable属性:该属性在JDK 1.6发布后增加到了Class文件规范中,它是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于替代以前比较消耗性能的基于数据流分析的类型推导验证器。
  1. Signature属性:该属性在JDK 1.5发布后增加到Class文件规范之中,是一个可选的定长属性,可出现在类、属性和方法表结构的属性表中。在JDK 1.5中大幅增强Java语言的语法,再次之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。之所以要专门使用这样一个属性去记录泛型类型,是因为Java语言的泛型采用擦除法实现的伪泛型,在字节码(Code属性)中,泛型信息编译(类型变量、参数化类型)之后都统统被擦除掉。擦除法的好处是实现简单、非常容易实现Backport,运行期也能节省一些类型所占的内存空间。但坏处是无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待。Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取泛型类型,最终的数据来源也就是这个属性。
  1. BootstrapMethods属性:该属性在JDK 1.7发布后增加到Class文件规范之中,是一个复杂的变长属性,位于类文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。

该文章来源《深入理解Java虚拟机》


以上

LeoQin wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
0%