文章内容来自《深入理解JVM》和网络资料整理
class文件是一组以8位字节为基础单位的二进制流,其与Java虚拟机指令集和符号表以及若干其他辅助信息相对应。
该设计有如下优点:
注:如果遇到8位字节以上空间的数据,则会按照高位在前的方式分割成若干个8位字节进行存储(Big-Endian,具体是指最高位字节在地址最低位、最低位字节在地址最高位的顺序来存储数据,它是SPARC、PowerPC等处理器的默认多字节顺序,而x86等处理器则是使用了相反的 Little-Endian 顺序来存储数据)
Class文件采用了类似C语言结构体的伪结构来存储数据,主要有以下几个特点:
定义:class文件基本的数据类型,用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表现形式:以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数。
组成:由无符号数或者其他表作为数据项构成的复合数据类型。
特征:以_info 结尾。
功能:用于描述有层次关系复合结构的数据。
整个Class文件本质上就是一张表
先来看一下Class文件的整体结构(也即Class文件中字节码的顺序):
ClassFile {
u4 magic; //魔数,固定值0xCAFEBABE
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]; //具体的属性内容
}
接下来按照Class文件中字节码的顺序来介绍数据项。
魔数后4个字节,第5个和第6个字节是次版本号(Minor Version),第7个和第8个字节是主版本号(Major Version)。
Java版本号从45开始,每个大版本发布版本号 +1
虚拟机拒绝超过其版本号的Class文件
可以说常量池是Class文件的资源仓库,主要存放两类常量:字面量和符号引用。
常量池结构:容量计数器(u2类型) + 常量。
容量计数从1开始,第0项腾出来满足后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”的意思,这种情况就可以把索引值置为0来表示。
常且池中每一项常量都是一个表,这些表都有一个共同的特点,就是表开始的第一位是一个ul类型的标志位,代表当前这个常量属于哪种常量类型。
以CONSTANT_Class_info类型为例,上表中的tag用来区分常量类型,name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,代表了这个类(或者接口)的全限定名。
常量池之后两个字节标识类的访问标志,用于识别一些类或者接口层次的访问信息,主要包括:
Class文件中由这三项数据来确定类的继承关系。
在Java中一般通过如下几项描述一个字段:字段作用域(public、protected、private修饰符)、是类级别变量还是实例级别变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、可序列化与否(transient修饰符)、字段数据类型(基本类型、对象、数组)以及字段名称。在字段表中,变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但可能列出原本Java代码之中不存在的字段,譬如,在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
Java语言中字段是无法重载的,必须使用不同的名称,但是对于字节码来说,字段可以重名,只要字段的描述符不一致
解释一下“简单名称”、“描述符’‘以及前面出现过多次的“全限定名”这三种特殊字符串的概念:
Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,只是在访问标志和属性表集合的可选项中有所区别。
方法表的访问标志中没有ACC_VOLATILE 和 ACC_TRANSIENT 标志,增加了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP、ACC_ABSTRACT 标志。
如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息,但有可能出现由编译器自动添加的方法,如类构造器
方法和实例构造器
方法
Class文件、字段表、方法表都可以有自己的属性表集合,用于描述某些场景的专有信息。属性表集合的限制更宽松一些,不要求各个属性表具有严格顺序,并且只要不与已有属性名重复即可。
接下来对其中一些属性中的关键常用的部分进行介绍。
最常用的属性恐怕就是Code属性了,因为大多数的方法都会有编译后的字节码指令,这些指令就存储在方法表中的Code属性中。如果一个Java程序的信息可以分为代码(方法体中的代码)和元数据(包括类、字段、方法定义以及其它信息),那么Code属性存储的就是代码,其它所有的结构存储的都是元数据。不过并非所有的方法表都有这个Code属性,比如接口或抽象类中的方法表就不存在Code属性(JDK 1.8中的接口也可以定义方法了)。Code属性的结构如下:
其中attribute_name_index和attribute_length前面已经介绍过了。
max_stack代表了操作数栈的最大深度。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机执行时需要根据这个值来分配栈帧中的操作栈深度。
max_locals代表了局部变量表所需要的存储空间。在这里,max_locals的单位是slot,在之前的文章中了解了HotSpot虚拟机在分配对象时使用的单位就是slot。方法参数(包括隐式参数this)、显式异常处理器的参数(try-catch块中catch块中定义的异常)以及方法体中定义的局部变量都需要局部变量表来存放。需要注意的是,由于局部变量表中的slot可以重用,所以并不是所有的局部变量的总slot就是max_locals。编译器会根据变量的作用域来分配slot给各个变量使用,然后计算max_locals的大小。
code_length和code用来存储字节码指令。Java的字节码指令的长度都是一个字节,即最多可以有256个指令,实际上一共有大约200条指令。