《深入理解java虚拟机》- 04 类文件结构

1、Class类文件的结构

class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,则按照高位在前的方式分割成若干个8位字节存储。

class文件格式只有两种数据类型:无符号数和表。

无符号数属于基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或按照UTF-8编码构成的字符串值。

表是由多个无符号数或其他表作为数据项构成的复合数据类型,以"_info"结尾,整个class文件就是一张表:

Class文件格式

1.1 魔数与Class文件版本

(1) Class文件的魔数值是:0XCAFEBABE(音译:咖啡宝贝)

(2) 紧接着魔数的4个字节是版本号,第5和第6个字节是次版本号(Minor Version),第7和第8是主版本号(Major Version)

1.2 常量池

(1) 版本号之后是常量池入口,常量池是Class文件中的资源仓库

(2) 常量池入口放置的是constant_pool_count,计数从1开始,第0项常量表示“不引用任何一个常量池项目”

(3) 常量池存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference),字面量包括文本字符串、声明为final的常量值等,而符号引用包括3类:类和接口的全限定名、字段的名称和描述符,方法的名称和描述符,共有14种结构不同的表数据:

常量池的项目类型

(4) 专门用于分析Class文件字节码的工具:javap

1.3 访问标志

在常量池之后,紧接着的两个字节是访问标志(access_flags),用于标识类或接口的访问信息,含义见下表:

访问标志

1.4 类索引、父类索引和接口索引

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型数据的集合,Class文件中由这三项数据来确定这个类的继承关系。查找过程:常量池索引值 -- CONSTANT_Class_info -- CONSTANT_Utf8_info :

1.5 字段表集合

字段表(field_info)用于描述接口或类中声明的变量,包括类级变量(含static关键字)和实例级字段,不包括方法类的局部变量。字段表结构如下:

字段表结构

access_flags如下图:

name_index和descriptor_index都是对常量池的引用,分别代表字段的简单名称和字段的描述符。描述符用来描述字段的数据类型,标识字符含义如下:

描述符标识字符含义

对于数组类型,每一维度使用前置"["表示,如String[][]将被记录为[[Ljava/lang/String,int[]表示[I

1.6 方法表集合

方法表含义

其中name_index(简单名称)代表没有类型和参数修饰的方法,descriptor_index(描述符)用来描述方法的参数列表(包括数量、类型和顺序)和返回值

方法访问标志

方法中的代码存放在"Code"属性表里,编译器自动添加的方法:类构造器<clinit>方法和实例构造器<init>方法

1.7 属性表集合

详见:https://www.cnblogs.com/flyingcr/p/10428285.html

2、字节码指令简介

字节码指令由一个字节长度,代表着某种特定操作含义的数字(操作码,Opcode),以及其后0至多个此操作所需参数(操作数,Operands)构成

2.1 字节码与数据类型

大多数的指令包含了其操作所对应的数据类型信息,如iload指令用于从局部变量表加载int型数据到操作数栈,而fload用于加载float型数据。助记符与数据类型对代应关系:i代表int,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。

2.2 加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。load代表将局部变量加载到操作数栈,store将一个数值从操作数栈存储到局部变量表,push和const代表将常量加载到操作数栈

2.3 运算指令

加法指令:iadd ladd fadd dadd

减法指令:isub lsub fsub dsub

乘法指令:imul lmul fmul dmul

除法指令:idiv ldiv fdiv ddiv

求余指令:irem lrem frem drem

取反指令:ineg lneg fneg dneg

位移指令:ishl ishr iushrl shl lshr lushr

按位或指令:ior lor

按位与指令:iand land

按位异或指令:ixor lxor

局部变量自增指令:iinc

比较指令:dcmpg dcmpl fcmpg fcmpl lcmp

2.4 类型转化指令

窄化类型转化时,需显示的使用转化指令:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f

2.5 对象创建和访问指令

创建类实例的指令:new

创建数组的指令:newarray(基本类型数组)、anewarray(引用类型数组)、multianewarray(多维度引用类型数组)

访问类字段的指令:getstatic、putstatic

访问实例字段的指令:getfield、putfield

将数组加载到操作数栈的指令:aload

将操作数栈的值存储到数组元素的指令:astore

取数组元素长度的指令:arraylength

检查实例类型的指令:instanceof、checkcast

2.6 操作数栈管理指令

pop:栈顶一个元素出栈,pop2:栈顶两个元素出栈

dup:复制栈顶一个数值并将复制值重新压入栈顶,dup2:复制栈顶两个数值并将双份复制值重新压入栈顶

swap:将栈顶的两个数值互换

2.7 控制转移指令

分支条件
switch分支
无条件分支

2.8 方法调用和返回指令

invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行分派

invokeinterface:用于调用接口方法,找到实现该接口合适的类的方法

invokespecial:用于调用实例初始化方法、私有方法和父类方法

invokestatic:用于调用static方法

invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法

2.9 异常处理指令

显示抛出异常的操作(throw语句)都由athrow指令实现

2.10 同步指令

Java虚拟机支持方法级和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。

方法级的同步是隐式,则无需通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否是同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程先持有管程,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他线程都无法再获得同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。

同步指令由synchronized块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要编译器与Java虚拟机两者协作支持。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容