GinoBeFunny

《深入理解Java虚拟机》读书笔记4:类文件结构

国内JVM相关书籍NO.1,Java程序员必读。读书笔记第四部分对应原书的第六章,主要介绍类文件结构的组成,并通过一个实例一步步了解各个部分的结构。

第三部分 虚拟机执行子系统

第六章 类文件结构

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

6.1 概述

由于最近十年内虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。

6.2 无关性的基石

  • Java刚诞生的宣传口号:一次编写,到处运行(Write Once, Run Anywhere)。其最终实现在操作系统的应用层:Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码。
  • 字节码(ByteCode)是构成平台无关的基石;
  • 另外虚拟机的语言无关性也越来越被开发者所重视,JVM设计者在最初就考虑过实现让其他语言运行在Java虚拟机之上的可能性,如今已发展出一大批在JVM上运行的语言,比如Clojure、Groovy、JRuby、Jython、Scala;
  • 实现语言无关性的基础仍是虚拟机和字节码存储格式,Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,这使得任何语言的都可以使用特定的编译器将其源码编译成Class文件,从而在虚拟机上运行。

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

6.3 Class类文件的结构

  • Class文件是一组以8个字节为基础单位的二进制流(可能是磁盘文件,也可能是类加载器直接生成的),各个数据项目严格按照顺序紧凑地排列,中间没有任何分隔符;
  • Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,其中只有两种数据类型:无符号数和表;
  • 无符号数属于基本的数据类型,以u1、u2、u4和u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值;
  • 表是由多个无符号数获取其他表作为数据项构成的复合数据类型,习惯以“_info”结尾;
  • 无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据未某一类型的集合。

Class文件格式

下面我以自己本机写的一个简单的Java文件来学习其中各个部分的含义:
TestClass.java

使用javac编译成TestClass.class文件,使用16进制打开:
TestClass.class

使用javap命令输出Class文件信息:
javap TestClass

6.3.1 魔数和版本

  • Class文件的头4个字节,唯一作用是确定文件是否为一个可被虚拟机接受的Class文件,固定为“0xCAFEBABE”。
  • 第5和第6个字节是次版本号,第7和第8个字节是主版本号(0x0034为52,对应JDK版本1.8);能向下兼容之前的版本,无法运行后续的版本;

6.3.2 常量池

  • 常量池可以理解为Class文件之中的资源仓库,是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项之一;
  • 由于常量池中的常量数量不固定,因此需要在常量池前放置一项u2类型的数据来表示容量,该值是从1开始的,上图的0x0013为十进制的19,代表常量池中有18项常量,索引值范围为1~18;
  • 常量池主要存放两大类常量:字面量(Literal,笔记接近Java的常量概念,比如文本字符串和final常量等)和符号引用(Symbolic References,主要包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符);
  • Java代码在javac编译时不会有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接;所以在Class文件不会保存各个方法、字段和最终内存布局信息;当虚拟机运行时需要从常量池获取对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中;
  • JDK 1.7中常量池共有14种不同的表结构数据,这些表结构开始的第一位是一个u1类型的标志位,代表当前常量的类型,具体如下图所示:

常量池的项目类型

  • 之所以说常量池是最繁琐的数据就是因为这14种常量类型都有自己的结结构。可以结合下图中各个表结构的说明和之前使用javap解析的文件内容一起看。

常量池中14种常量项结构说明1
常量池中14种常量项结构说明2

  • 第1项:0x0A(15标志为方法句柄),0x0004(指向第4项的类描述符),0x000F(指向第15项的名称及类型描述符);
  • 第2项:0x09(9标志为字段符号引用),0x0003(指向第3项类描述符),0x0010(指向第16项的名称及类型描述符);
  • 第3项:0x07(7标志为类符号引用),0x0011(指向第17项全限定名常量项);
  • 第4项:0x07(7标志为类符号引用),0x0012(指向第18项全限定名常量项);
  • 第5项:0x01(1标志为UTF-字符串常量),0x0001(字符串占用1个字节),6D(字符“m”);
  • 第6项:0x01(1标志为UTF-字符串常量),0x0001(字符串占用1个字节),49(字符“I”);
  • 第7项:0x01(1标志为UTF-字符串常量),0x0006(字符串占用6个字节),3C 69 6E 69 74 3E(字符“”);
  • 第8项:0x01(1标志为UTF-字符串常量),0x0003(字符串占用3个字节),28 29 56(字符“()V”);
  • 第9项:0x01(1标志为UTF-字符串常量),0x0004(字符串占用4个字节),43 6F 64 65(字符“Code”);
  • 第10项:0x01(1标志为UTF-字符串常量),0x000F(字符串占用15个字节),4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65(字符“LineNumberTable”);
  • 第11项:0x01(1标志为UTF-字符串常量),0x0003(字符串占用3个字节),69 6E 63(字符“inc”);
  • 第12项:0x01(1标志为UTF-字符串常量),0x0003(字符串占用3个字节),28 29 49(字符“()I”);
  • 第13项:0x01(1标志为UTF-字符串常量),0x000A(字符串占用10个字节),53 6F 75 72 63 65 46 69 6C 65(字符“SourceFile”);
  • 第14项:0x01(1标志为UTF-字符串常量),0x000E(字符串占用14个字节),54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61(字符“TestClass.java”);
  • 第15项:0x0C(12标志为名称和类型符号引用),0x0007(指向第7项名称常量项), 0x0008(指向第8项描述符常量项);
  • 第16项:0x0C(12标志为名称和类型符号引用),0x0005(指向第5项名称常量项), 0x0006(指向第6项描述符常量项);
  • 第17项:0x01(1标志为UTF-字符串常量),0x001F(字符串占用31个字节),63 6F 6D 2F 67 69 6E 6F 62 65 66 75 6E 6E 79 2F 63 6C 61 7A 7A 2F 54 65 73 74 43 6C 61 73 73(字符“com/ginobefunny/clazz/TestClas”);
  • 第18项:0x01(1标志为UTF-字符串常量),0x0010(字符串占用16个字节),6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74(字符“java/lang/Object”);

6.3.3 访问标志

  • 紧接在常量池后面的是两个字节的访问标志,用于标识类或接口的访问信息;
  • 访问标志一个有16个标志位,但目前只采用了其中8位,本例子中的0x0021标识为一个public的普通类;

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

  • 类索引:u2类型的数据,用于确定类的全限定名。本例子中为0x0003,指向常量池中第3项;
  • 父类索引:u2类型的数据,用于确定父类的全限定名。本例子中为0x0004,指向常量池中第4项;
  • 接口索引计算器:u2类型的数据,用于表示索引集合的容量。本例子中为0x0000,说明没有实现接口;
  • 接口索引集合:一组u2类型的数据的集合,用于确定实现的接口(对于接口来说就是extend的接口)。本例子不存在。

6.3.5 字段表集合

字段表结构

  • 用于描述接口或者类中声明的变量,包括类级变量和实例级变量,但不包括方法内部声明的局部变量;它不会列出从父类和超类继承而来的字段;
  • 0x0001表示这个类只有一个字段表数据;
  • 字段修饰符放在access_flag中,是一个u2的数据类型,0x0002表示为private的属性;
  • 字段名称name_index,是一个u2的数据类型,0x0005表示该属性的名称为常量池的第5项;
  • 字段描述符descriptor_index,是一个u2的数据类型,0x0006表示该属性的描述符为常量池的第6项,其值“I”表示类型为整形;
  • 字段属性计算器和属性集合:0x0000表示该例子中不存在;

6.3.6 方法表集合

  • 和字段表集合的方式几乎一样;
  • 方法里面的代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面;
  • 0x0002表示这个类有两个方法表数据,分别是编译器添加的实例构造器和源码中的方式inc();
  • 第一个方法的访问标志是0x0001(public方法),名称索引值为0x0007(常量池第7项,“”),描述符索引值为0x0008(常量池第8项,“()V”),属性表计算器为0x0001(有一项属性),属性名称索引为0x0009(常量池第9项,“Code”);
  • 根据“6.3.7.1 Code属性”说明,属性值的长度为23(0x0000001D表示29,但需要减去属性名称索引和属性长度固定的6个字节长度),操作数栈深度的最大值为1(0x0001,虚拟机运行时根据这个值来分配栈帧中操作栈深度),局部变量表所需要的存储空间为1个Slot(0x0001,Slot是内存分配的最小单位),字节码长度为5(0x00000005),分别为2A(aload_0,将第0个Slot中为reference类型的本地变量推送到操作数栈顶)、B7(invokespecial,以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它父类的方法,后面接着一个u2的参数指向常量池的方法引用)、0x0001(表示常量池的第1项,即Object类的方法)、B1(对应的指令为return,返回值为void);显式异常表为空(0x0000,计数器为0);该Code属性还内嵌1个属性(0x0001),属性的名称索引为0x000A(即“LineNumberTable”属性,用于记录对应的代码行数),该内嵌属性的长度为6(0x00000006),对应的行数信息为源码的第3行(0x000100000003);
  • 第二个方法的访问标志是0x0001(public方法),名称索引值为0x000B(常量池第11项,“inc”),描述符索引值为0x000C(常量池第12项,“()I”),属性表计算器为0x0001(有一项属性),属性名称索引为0x0009(常量池第9项,“Code”);
  • 根据“6.3.7.1 Code属性”说明,属性值的长度为25(0x0000001F表示31,但需要减去属性名称索引和属性长度固定的6个字节长度),操作数栈深度的最大值为2(0x0002),局部变量表所需要的存储空间为1个Slot(0x0001),字节码长度为7(0x00000007),分别为2A(aload_0)、B4(getfield,后面接着一个u2的参数指向常量池的属性引用)、0x0002(表示常量池的第2项,即TestClass类的m属性)、04(对应的指令为iconst_1)、60(对应的指令为iadd,整形求和)、AC(对应的指令为ireturn,返回值为整形);显式异常表为空(0x0000,计数器为0);该Code属性还内嵌1个属性(0x0001),属性的名称索引为0x000A(即“LineNumberTable”属性,用于记录对应的代码行数),该内嵌属性的长度为6(0x00000006),对应的行数信息为源码的第8行(0x000100000008);

6.3.7 属性表集合

  • 在Class文件、字段表、方法表都可以携带自己的属性表集合;
  • 属性表集合的限制较为宽松,不再要求严格的顺序,只要属性名不重复即可;
  • 以下是Java虚拟机规范里预定义的虚拟机实现应当能识别的属性:

虚拟机规范预定义的属性1
虚拟机规范预定义的属性2

  • 接着我们的例子的Class文件还有最后一段:0x0001表示该Class有一个属性,0x000D表示属性名索引为第13项(对应“SourceFile”),0x00000002表示该属性长度为2,0x000E表示该类的SourceFile名称为第14项(对应“TestClass.java”)。
6.3.7.1 Code属性

Java程序方法体中的代码经过javac编译后,字节码指令存放在Code属性,其属性表结构如下:

Code属性表的结构

6.3.7.2 Exceptions属性

方法描述时throws关键字后面列举的异常,和Code属性里的异常表不同。其属性表结构如下:

Exceptions属性表

6.3.7.3 LineNumberTable属性

用于描述Java源码行号与字节码行号之间的对应关系,它不是必须的,可以通过javac -g:none取消该信息。没有该信息的影响是运行时抛异常不会显示出错的行号,在代码调试时无法按照源码行来设置断点。

LineNumberTable属性

6.3.7.4 LocalVariableTable属性

用于描述栈帧中局部变量与Java源码中定义的变量之间的关系,它不是运行时必须的,可以通过javac -g:none取消该信息。如果没有这个属性,所有的参数名称都会丢失,取之以arg0、arg1这样的占位符来替代。

LocalVariableTable属性

其中local_variable_info项代表了一个栈帧与源码中局部变量的关联,如下所示:

local_variable_info结构

6.3.7.5 SourceFile属性

用于记录生成这个Class的源码文件名称,这个属性也是可选的。

SourceFile属性

6.3.7.6 ConstantValue属性

作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可以用这个属性。对于非static类型的变量的赋值是在实例构造器方法中进行的;而对于类变量有两种方式:在类构造器方法中或者使用ConstantValue属性。目前Sun javac编译器的选择是:同时使用final和static修饰的变量且为基本数据类型或String类型使用ConstantValue属性初始化,否则使用初始化。

ConstantValue属性

6.3.7.7 InnerClass属性

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

InnerClass属性

其中number_of_class代表需要记录多少个内部类信息,每个内部类的信息都由一个inner_class_info表进行描述。

inner_class_info表的结构

6.3.7.8 Deprecated及Synthetic属性

Deprecated(不推荐使用)和Synthetic(不是由Java源码直接产生编译器自行添加的,有两个例外是实例构造器和类构造器)这两个属性都属于布尔属性,只存在有和没有的区别,没有属性值的概念。在属性结构中attribute_length的数据值必须为0x00000000。

Deprecated及Synthetic属性

6.3.7.9 StackMapTable属性

这是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

StackMapTable属性

6.3.7.10 Signature属性

一个可选的定长属性,在JDK 1.5发布后增加的,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signature属性会为它记录泛型签名信息。这主要是因为Java的泛型采用的是擦除法实现的伪泛型,在字节码中泛型信息编译之后统统被擦除,在运行期无法将泛型类型与用户定义的普通类型同等对待。通过Signature属性,Java的反射API能够获取泛型类型。

Signature属性

6.3.7.11 BootstrapMethods属性

一个复杂的变长属性,位于类文件的属性表中,用于保存invokedynamic指令引用的引导方法限定符。

6.4 字节码指令简介

Java虚拟机的指令由一个字节长度的、代表着特定操作含义的数字(操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。

在指令集中大多数的指令都包含了其操作所对应的数据类型信息,如iload指令用于从局部变量表中加载int类型的数据到操作数栈中。

  • 加载和存储指令:iload/iload等(加载局部变量到操作栈)、istore/istore等(从操作数栈存储到局部变量表)、bipush/sipush/ldc/iconst_(加载常量到操作数栈)、wide(扩充局部变量表访问索引);
  • 运算指令:没有直接支持byte、short、char和boolean类型的算术指令而采用int代替;iadd/isub/imul/idiv加减乘除、irem求余、ineg取反、ishl/ishr位移、ior按位或、iand按位与、ixor按位异或、iinc局部变量自增、dcmpg/dcmpl比较;
  • 类型转换指令:i2b/i2c/i2s/l2i/f2i/f2l/d2i/d2l/d2f;
  • 对象创建与访问指令:new创建类实例、newarray/anewarray/multianewarray创建数组、getfield/putfield/getstatic/putstatic访问类字段或实例字段、baload/iaload/aaload把一个数组元素加载到操作数栈、bastore/iastore/aastore将一个操作数栈的值存储到数组元素中、arraylength取数组长度、instanceof/checkcast检查类实例类型;
  • 操作数栈管理指令:pop/pop2一个或两个元素出栈、dup/dup2复制栈顶一个或两个数组并将复制值或双份复制值重新压力栈顶、swap交互栈顶两个数值;
  • 控制转移指令:ifeq/iflt/ifnull条件分支、tableswitch/lookupswitch复合条件分支、goto/jsr/ret无条件分支;
  • 方法调用和返回指令:invokevirtual/invokeinterface/invokespecial/invokestatic/invokedynamic方法调用、ireturn/lreturn/areturn/return方法返回;
  • 异常处理指令:athrow
  • 同步指令:monitorenter/monitorexit

6.5 公有设计和私有实现

  • Java虚拟机的实现必须能够读取Class文件并精确实现包含在其中的Java虚拟机代码的含义;
  • 但一个优秀的虚拟机实现,通常会在满足虚拟机规范的约束下具体实现做出修改和优化;
  • 虚拟机实现的方式主要有两种:将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集或宿主主机CPU的本地指令集。

6.6 Class文件结构的发展

  • Class文件结构一直比较稳定,主要的改进集中向访问标志、属性表这些可扩展的数据结构中添加内容;
  • Class文件格式所具备的平台中立、紧凑、稳定和可扩展的特点,是Java技术体系实现平台无关、语言无关两项特性的重要支柱;

6.7 本章小结

本章详细讲解了Class文件结构的各个部分,通过一个实例演示了Class的数据是如何存储和访问的,后面的章节将以动态的、运行时的角度去看看字节码在虚拟机执行引擎是怎样被解析执行的。

系列读书笔记

Gino Zhang wechat
扫一扫,关注我的微信公众号