深入理解JVM虚拟机——6. 类文件结构

6.1 概述

我们的计算机至今仍然只能认识0和1,所以那些老式的语言例如c/c++,编写的程序直接编译成本地机器码(Native Code),但是最近十年,虚拟机发展的越来越好,所以很多的程序语言选择与操作系统机器指令集无关的、平台中立的格式作为程序编译后的存储格式。


6.2 无关性的基石

Java刚诞生时的口号是一次编写,到处运行,而与平台无关最终实现在了操作系统的应用层上:各种平台上都有自己的虚拟机,而这些虚拟机都可以载入和执行同一种与平台无关的字节码,从而实现上面的口号。

所以字节码是构成平台无关性的基石,基于此,可以运行在Java虚拟机上的语言还有很多,而不是只有Java。
简单来说,虚拟机不与任何语言绑定,它只与**字节码(Byte Code)**绑定,虚拟机可以执行任何语言编译的字节码,它并不关心字节码出自哪里。

6.3 Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目紧凑的排列在Clss文件中,中间没有任何分隔符,所以Class文件中存储的几乎全部是程序运行的必要数据。

Class文件格式采用一种类似于C语言结构体的伪结构来存储数据。

  • 无符号数: 属于基本数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者字符串值。
  • 表: 表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有的表都以"_info"结尾。表用来描述有层次关系的复合结构的数据。

整个Class文件本质上就是一张表,由下表的数据项构成。

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count-1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

当需要描述同一类型但数量不一定的多个数据时,经常会使用一个前置的容量计数加若干个连续的数据项的形式,这时称为某一类型的集合。

6.3.1 魔数与Class文件的版本

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件,而魔数的值为0xCAFEBABE(咖啡宝贝?)

紧接的4个字节存储的是版本号:5和6是次版本号(Minor Version),7和8是主版本号(Major Version),可以向下兼容,但不能运行高版本

书中有笔者写的小例子。

6.3.2 常量池

紧接的是常量池入口,它是关联最多的数据类型,也是占用空间最大的资源之一。

由于常量池数量是不固定的,所有常量池入口放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。这个容量计数是从1开始的,也只有它是从1开始的,而0项空出来是为了后面某些指向常量池索引值得数据在特定情况下需要表达 “不引用任何一个常量池项目” 得含义。
常量池中每一个常量都是一个表,一共有14种表,表的开头都有一个u1类型的标志位(tag),并且14种常量类型都有自己的表结构,代表当前常量属于哪种常量类型。

6.3.3 访问标志

常量池结束后,紧接着的两个字节是访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息。

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

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,接口索引集合是一组u2类型的数据的集合,class文件由这三项确定类的继承关系。

类索引确定类的全限定名,父类索引只有一个(Java不允许多继承),除了Object类,其他所有类的父类索引都不为0。接口索引集合用来描述实现了哪些接口。

6.3.5 字段表集合

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

包括的信息有字段作用域(public,private,protected)、是实例变量还是类变量(static)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型(基本类型、对象、数组)、字段名称。上述信息各个修饰符都是bool值,要么有,要么没有。

6.3.6 方法表集合

与字段的描述几乎一致,方法表的结构如同字段表一样包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes) 几项。

6.3.7 属性表集合

在class文件、字段表、方法表、都可以携带自己的属性表集合。

属性表集合不要求各个属性表具有严格的顺序,只要不与已有属性名重复,Java虚拟机运行时会忽略掉它不认识的属性。

  1. Code属性: 程序中的方法体代码通过编译后变为字节码存储在Code属性中。但是像接口或者抽象类就不存在这个属性。
  2. Exceptions属性: 与Code属性平级,作用是列举出方法中可能抛出的受查异常(Checked Exceptions),也就是thorw关键字后面列举的异常。
  3. LineNumberTable属性: 用于描述Java源码行号与字节码行号(offest)之间的对应关系。
  4. LocalVariableTable属性: 用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。
  5. SourceFile属性: 用于记录生成这个Class文件的源码文件名称。
  6. ConstantValue属性: 通知虚拟机自动为静态变量赋值。只有static修饰的变量才有这个属性。
  7. InnerClasses属性: 用于记录内部类与宿主类之间的关联,如果一个类定义了内部类,编译器将会为它以及所包含的内部类生成InnerClasses属性。
  8. Deprecated及Syntheic属性: 都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值,Deprecated属性用于表示某个类、字段或方法已经被程序作者定位推荐不再使用(@deprecated),Syntheic代表此字段或者方法不是由Java源码直接产生的,而是由编译器自行田间的。
  9. StackMapTable属性: 这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流 分析的类型推导验证器。
  10. Signature属性: 任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则Signatrue会为它记录泛型签名信息。
  11. BootstrapMethods属性: 用于保存invokedynamic指令引用的引导方法限定符。

6.4 字节码指令简介

Java虚拟机的指令由一个字节长度的操作码和后续零个或多个操作数构成,而Java虚拟机采用的是面向操作数栈,所以大多数指令都只有操作码。

  • 优势: 放弃操作数长度对齐,省略很多填充和间隔符号,编译代码短小精干,传输效率高。
  • 劣势: 长度一个字节(0~255)意味操作码总数不超过256条,处理超过一个字节的数据时就要在运行时,从字节中重建出具体数据的结构,而这也会使得解释执行时损失性能。

6.4.1 字节码与数据类型

大多数指令包含了操作所对应的数据类型信息例如i开头的部分指令用于操作int类型,f开头的部分指令用于操作float类型。

由于Java虚拟机的操作指令是有限的(256)所以指令集被设计为不是每种类型都有对应的每种操作,有一些在必要时会进行类型转换,例如boolean、byte、short和char类型就会转换为int类型进行操作。

6.4.2 加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。

  • 将一个局部变量加载到操作栈: iload、iload_<n>、lload、lload_<n>、fload、fload_ <n>、dload、dload_<n>、aload、aload_<n>。
  • 将一个数值从操作数栈存储到局部变量表: istore、istore_<n>、lstore、lstore_<n>、 fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。
  • 将一个常量加载到操作数栈: bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、 iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
  • 扩充局部变量表的访问索引的指令: wide。

6.4.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、iushr、lshl、lshr、lushr。
  • 按位或指令:ior、lor。
  • 按位与指令:iand、land。
  • 按位异或指令:ixor、lxor。
  • 局部变量自增指令:iinc。
  • 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。

6.4.4 类型转换指令

类型转换指令可以将两种不同的数值类型进行转换。

Java虚拟机直接支持(无需显示指令)小范围类型到大范围类型的安全转换。

  • int到long、float、double
  • long到float、double
  • float到double

处理窄化类型转换必须显示的使用转换指令。
在将int或long转换为整数T类型时,仅仅是丢弃除最低位N(T类型数据长度)个字节以外的内容。
浮点值转换为整数T类型时,遵循以下原则:

  • 如果浮点值是NaN,转换结果是int或long型的0
  • 如果浮点值不是无穷大,浮点值使用IEEE754向零舍入模式取整,获得整数值v,v在T表示范围内结果就是v

double到float的窄化过程与IEEE754定义的一致,

6.4.5 对象的创建与访问指令

对象创建后就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素。

  • 创建类实例的指令: new。
  • 创建数组的指令: newarray、anewarray、multianewarray。
  • 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变 量)的指令: getfield、putfield、getstatic、putstatic。
  • 把一个数组元素加载到操作数栈的指令: baload、caload、saload、iaload、laload、 faload、daload、aaload。
  • 将一个操作数栈的值存储到数组元素中的指令: bastore、castore、sastore、iastore、 fastore、dastore、aastore。
  • 取数组长度的指令: arraylength。
  • 检查类实例类型的指令: instanceof、checkcast。

6.4.6操作数栈管理指令

用于直接操作操作数栈的指令

  • 将操作数栈的栈顶一个或两个元素出栈: pop、pop2。
  • 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶: dup、dup2、 dup_x1、dup2_x1、dup_x2、dup2_x2。
  • 将栈最顶端的两个数值互换: swap。

6.4.7 控制转移指令

可以让Java虚拟机有条件或无条件的从指定位置的指令开始执行。

  • 条件分支: ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、 if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
  • 复合条件分支: tableswitch、lookupswitch。
  • 无条件分支: goto、goto_w、jsr、jsr_w、ret。

6.4.8 方法调用和返回指令

  • invokevirtual指令 用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派)。
  • invokeinterface指令 用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
  • invokespecial指令 用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
  • invokestatic指令 用于调用类方法(static方法)。
  • invokedynamic指令 用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻 辑是由用户所设定的引导方法决定的。

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

6.4.9 异常处理指令

在Java程序中显示抛出异常的操作(throw语句)都由athrow指令来实现。
catch语句采用异常表来完成的。

6.4.10 同步指令

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,通过管程支持。

方法级的同步是隐式的,无需通过字节码来控制,实现在方法调用和返回操作中。


6.5 公有设计和私有实现

可以自己在满足Java虚拟机规范的情况下对具体实现做出修改和优化。

虚拟机实 现的方式主要有以下两种:

  • 将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集。
  • 将输入的Java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(即JIT代码生 成技术)。

猜你喜欢

转载自blog.csdn.net/MoForest/article/details/85111440