jvm

JVM学习笔记(二)——Class文件结构

2017-07-10  本文已影响41人  李牙刷儿

Class文件是Java程序跨平台的保证,正是由于有了Class文件架起源码和机器码之间的中间桥梁,JVM虚拟机才可以在各种平台上按照统一的规范标准加载Java代码。

作为“写给虚拟机看的”Java代码,Class文件结构必须设计得足够完善,同时由于Java虚拟机规范并不只针对Java,Class文件又不能引入过多细节。本篇博客我们就来介绍下Class文件的结构。

一个Class文件对应一个Java Class,所以一个Class文件记录着一个类的全部信息,JVM通过Class文件将对应的类加载入内存。

Class文件的结构主要分为以下几部分:

1 魔数

每个Class文件的头4个字节成为魔数(Magic Number),它的唯一作用就是确定这个文件是否能作为一个Class文件被接受。很多文件都以魔数进行类型识别,如gif、jpeg等图片文件。之所以使用魔数而不是扩展名是处于安全考虑,文件扩展名可以所以改动。Class文件的魔数是0xCAFEBABE。

紧接着魔数的4个字节存储的是Class文件的版本号,5、6字节为次版本号,7、8字节为主版本号。不同版本的虚拟机可以接受不同版本的class文件,所以虚拟机通过主次版本号判断是否可以加载目标class文件。

2 常量池

常量池可以看做是Class文件的资源仓库,也是Class文件中占用空间最大的部分。常量池主要存放两大类常量:字面量、符号引用。

字面量比较接近Java语言层面的常量,如文本字符串、生命为final的常量等。

符号引用属于编译范畴中的概念,主要包括三类常量:

Java语言不同于C、C++等语言在编译阶段即进行链接,相应的链接都放到了运行时阶段。所以Class文件中不可能包含各个方法、字段在内存中的布局。Java虚拟机在运行阶段加载类时,将符号引用转换成真正的内存入口地址,对应类才算可以工作。

常量池中的每一项代表一个常量,JDK目前共有14中类型的常量,而每一个常量又有自己的内部结构。类或接口符号索引是其中较为简单的一项,接下来以类索引为例做简单介绍。类符号索引对应的类型为CONSTANT_Class_info,其结构如下:

类型 名称 数量
u1 tag 1
u2 name_index 1

tag是标志位,表明类型。CONSTANT_Class_info的tag为7。name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,此常量代表了这个类的全限定名。

CONSTANT_Utf8_info的结构如下所示:

类型 名称 数量
u1 tag 1
u2 length 1
u1 bytes length

bytes字段的内容就是类的全限定名。

3 访问标志

常量池之后的两个字节代表访问标志(accss_flags),用于识别类或接口的层次访问信息:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public
ACC_FINAL 0x0010 是否被声明为final
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义
ACC_INTERFACE 0x0200 是否为接口
ACCS_ABSTRACT 0x0400 是否为abstract类型
ACC_SYNTHETIC 0x1000 标示该类并非由用户代码产生
ACC_ANNOTATION 0x2000 标示这是一个注解
ACC_ENUM 0x4000 标示这是一个枚举

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

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

类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个CONSTANT_Class_info类描述符常量,通过CONSTANT_Class_info类型的常量索引值可以找到定义在CONSTAN_Utf8_info类型的常量中的类全限定名。

对于接口索引集合,入口的第一项u2类型的数据为接口计数器(interfaces_count)表示索引表的容量。每个接口的同样由一个u2类型数据指向一个CONSTANT_Class_info。

5 字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量和实例级变量,但不包括定义在方法内部的局部变量。每个字段的结构如下图所示:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attribute_count

5.1 访问标识

标志名称 含义
ACC_PUBLIC 是否为public
ACC_PRIVATE 是否为private
ACC_PROTECTED 是否为protected
ACC_STATIC 是否为static
ACC_FINAL 是否为final
ACC_VOLATILE 是否为volatile
ACC_TRANSIENT 是否为transient
ACC_SYNTHETIC 是否为编译器自动产生
ACC_ENUM 是否为enum

字段的访问标识access_flags与类访问标识类似。

5.2 name_index

name_index标识字段的简单名称。简单名称和全限定名的区别在于:全限定名是类的全路径名,如org/fenixsoft/clazz/TestClass,只是把类全名中的"."替换成“/”而已。简单名称指的是没有类型和参数修饰的方法或者字段名称,如一个类中含有一个字段"m",则其简单名称为"m"。

5.3 descriptor_index

descriptor_index为字段或方法的描述符。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。基本数据类型以及代表无返回值的void以及对象类型均由一个大写字符来代替:

标志字段 含义
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void
L 对象类型,如Ljava/lang/object

对于数组类型,每一个维度用一个"["来描述,比如定义一个“java.lang.String[][]”类型的二维数组,将被记录为“[[Ljava/lang/string”。

方法描述符按照先参数列表后返回值的顺序描述,参数列表按照参数顺序放在一组"()"之内。如方法int indexOf(char[] source, int sourceOffest, int sourceCount, char[] target, int targetOffest, int targetCount, int fromIndex)的描述符为"([CII[CIII)I"。

5.4 attributes_count attribute_info

在描述符之后还有数量为attributes_count的attribute_info,attribute_info描述字段的额外信息,但这些额外信息最终存放在属性表中。如“final static int m = 123;”,那就可能会存在一项名称为ConstantValue的属性,其值指向常量123。

6 方法表集合

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attribute_count

方法表和字段表结合几乎一样,理解了字段表,方法表就非常简单了。

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attribute_count

由于volatile和transient不能修饰方法,所以方法表的访问标识中没有了ACC_VOLATILE,ACC_TRANSIENT标识。但同时又增加了代表synchronized native strictfp abstract的ACC_SYNCHRONIZED ACC_NATIVE ACC_STRICTFP ACC_ABSTRACT。

需要说明的是,方法表集合中并不包含方法里面的代码。方法代码经过编译后存放在方法属性集合中的一个名为"Code"的属性里面。例如某方法的属性表计数器attributes_count为1,则表示方法的属性表集合有一项属性,属性索引名称为0x0009,对应常量为code,说明此属性是方法的字节码描述。

7 属性表集合

Class文件、字段表、方法表都可以有自己的属性表,Java7里面定义了21种属性。

Code属性

并非所有方法表都有Code属性,比如接口和抽象类的方法就没有。结构如下:

类型 名称 数量 含义
u2 attribute_name_index 1 属性名的索引,对Code属性而言恒为”Code”
u4 attribute_length 1 属性值长度,相当于整个属性表长度长度减6(u2+u4)
u2 max_stack 1 操作数栈深度最大值。JVM运行时根据此值分配栈桢的操作栈深度
u2 max_locals 1 局部变量表所需存储空间,单位是Slot,double和long占用2个Slot、其他基本类型1Slot,Slot空间可以重用(变量作用域问题)
u4 code_length 1 编译后的字节码长度,理论上最长2^32-1,实际上JVM规定一个方法不允许超过65535条字节码指令
u1 code code_length 代码编译后的字节码
u2 exception_table_length 1 异常表长度
exception_info exception_table exception_table_length 异常表,记录字节码在start_pc到end_pc行之间如果出现类型为catch_type或其子类的异常则跳转到handler_pc行继续处理
u2 attibutes_count 1 属性表计数器
attribute_info attributes attibutes_count 属性额外描述,比如描述变量初始化值在常量池中的索引

字节码值得注意的一个地方是,javac编译时将this关键字作为一个普通方法参数由JVM调用时自动传入。

Exceptions属性

描述方法可能抛出的受检异常。

LineNumberTable属性

描述Java远吗行号与字节码行号之间映射关系,也就是为什么抛异常的时候可以显示源码哪一行抛出的。

LocalVariableTable属性

描述栈桢中局部变量表与Java源码中变量的关系,以保证编译后的代码被其他代码调用时,IDE可以显示参数名(否则被arg0、arg1之类的变量名代替)

SourceFile属性

描述生成当前Class文件的源文件名称,也是抛异常时可以显示源文件名字的原因。但内部类不会生成这个属性。

ConstantValue属性

static关键字修饰的变量可以使用这个属性。对于Sun javac编译器,final static的变量采用ConstantValue属性初始化,其他static变量在<clinit>(类构造器)中初始化。

InnerClasses属性

记录内部类和宿主类的关联。内部类和宿主类的Class文件都会有这个属性。

Signature属性

记录泛型签名信息。Java的泛型是使用擦除式实现的伪泛型,编译后擦除泛型,这个属性为了弥补此缺陷,方便反射API可以拿到泛型类型。

上一篇下一篇

猜你喜欢

热点阅读