类文件结构及字节码指令

一、Class类文件的结构

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


Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数

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

Class文件格式:

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count-1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
filed_infofiledsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attributes_count1
attribute_infoattributesattributes_count

Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在其中的数据项,无论是顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。


1.1、魔数与Class文件的版本

每个Class文件的头4个字节(0xcafebabe)称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。

这里我们编写一段简单的Java代码,编译得到Class文件,使用Sublime编辑器进行打开查看

public class Client {
    public int calc() {
        int a = 100;
        int b = 200;
        int c = 300;
        return (a + b) * c;
    }
}

在这里插入图片描述


紧接着魔数的4个字节存储的是Class文件的版本号:第5个和第6个是次版本号(Minor Version),第7个和第8个是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
在这里插入图片描述

JDK版本号Class版本号16进制
1.145.000 00 00 2D
1.246.000 00 00 2E
1.347.000 00 00 2F
1.448.000 00 00 30
1.549.000 00 00 31
1.650.000 00 00 32
1.751.000 00 00 33
1.852.000 00 00 34

1.2、常量池

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


常量池中主要存放两大类常量:字面量(Literal) 和 符号引用(Symbolic References)。

  • 字面量: 比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
  • 符号引用: 则属于编译原理方面的概念,包括了下面三类常量:
    • 类和接口的全限定名(Fully Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符。

常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。(因为第0项常量空出来是有特殊考虑的,目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值的数据置为0来表示)
在这里插入图片描述
上图中的常量的数量为 0x0016,也就是十进制的22,所以我们这个Class文件中,一共有21个常量,就是紧接着下面的就是常量,这里直接看较为晦涩,其实我们JDK中也为我们提供了相应的工具 javap,我们在cmd中切换至class文件所在的目录下,然后就可以使用 javap -verbose 命令查看class文件。
在这里插入图片描述


1.3、访问标志

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

标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类可设置
ACC_SUPER0xxx20是否允许使用invokespecial字节码指令的新语意,
invokespecial指令在JDK1.2发生过改变,
为了区别这条指令使用哪种语意,
JDK1.2之后编译出来的类这个标志都必须为真
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,
此标志值为真,其他类值为假
ACC_SYNCTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举

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

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合。
在这里插入图片描述

这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名父类索引用于确定这个类的父类的全限定名

由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。


1.5、字段表集合

字段表(field_info)描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量。

而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。


1.6、方法表集合

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

与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”


1.7、属性表集合

存储Class文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在 Code 属性表中、final关键字定义的常量值就存储在 ConstantValue 表中,除此之外我们的属性表中还有很多很多其他的信息,如记录方法签名信息的Signature、记录源文件的SourceFile、内部类列表的InnerClasses等等。



二、字节码指令

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。

由于限制了Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过256条。


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

大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的int类型作为运算类型。


2.1、加载和存储指令

用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这类指令包括如下内容:

  • 将一个局部变量加载到操作栈: iloadiload_<n>lloadlload_<n>floadfload_<n>dloaddload_<n>aloadaload_<n>
  • 将一个数值从操作数栈存储到局部变量表: istoreistore_<n>lstorelstore_<n>fstorefstore_<n>dstoredstore_<n>astoreastore_<n>
  • 将一个常量加载到操作数栈: bipushsipushldcdc_wldc2_waconst_nulliconst_m1iconst_<i>lconst_<l>fconst_<f>dconst_<d>
  • 扩充局部变量表的访问索引的指令: wide

2.2、运算指令

用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。

  • 加法指令: iaddladdfadddadd
  • 减法指令: isublsubfsubdsub
  • 乘法指令: imullmulfmuldmul
  • 除法指令: idivldivfdivddiv
  • 取余指令: iremlremfremdrem
  • 取反指令: ineglnegfnegdneg
  • 位移指令: ishlishriushrlshllshrlushr
  • 按位或指令: iorlor
  • 按位与指令: iandland
  • 按位异或指令: ixorlxor
  • 局部变量自增指令: iinc
  • 比较指令: dcmpgdcmplfcmpgfcmpllcmp

2.3、类型转换指令

可以将两种不同的数值类型进行相互转换

Java虚拟机直接支持以下数值类型的宽化类型转换(即小范围类型向大范围类型的安全转换):
int 类型到 long、float 或者 double 类型。
long 类型到 float、double 类型。
float 类型到 double 类型。

处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,这些转换指令包括:i2bi2ci2sl2if2if2ld2id2ld2f


2.4、对象创建及访问指令

  • 创建类实例的指令: new
  • 创建数组的指令: newarrayanewarraymultianewarray
  • 访问字段指令: getfieldputfieldgetstaticputstatic
  • 把一个数组元素加载到操作数栈的指令: baloadcaloadsaloadialoadlaloadfaloaddaloadaaload
  • 将一个操作数栈的值存储到数组元素中的指令: bastorecastoresastoreiastorefastoredastoreaastore
  • 取数组长度的指令: arraylength
  • 检查类实例类型的指令: instanceofcheckcast

2.5、操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:将操作数栈的栈顶一个或两个元素出栈:poppop2

复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dupdup2dup_x1dup2_x1dup_x2dup2_x2

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


2.6、控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。控制转移指令如下。

条件分支: ifeqifltifleifneifgtifgeifnullifnonnullif_icmpeqif_icmpneif_icmpltif_icmpgtif_icmpleif_icmpgeif_acmpeqif_acmpne
复合条件分支: tableswitchlookupswitch
无条件分支: gotogoto_wjsrjsr_wret


2.7、方法调用指令

invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。

invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

invokestatic指令用于调用类方法(static方法)。

invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。


方法调用指令与数据类型无关。


2.8、方法返回指令

是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturnfreturndreturnareturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。


2.9、异常处理指令

在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现


2.10、同步指令

monitorentermonitorexit两条指令来支持synchronized关键字的语义

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值