JVM的类文件结构,深入理解JVM必须趟过去的坎

类文件结构

​ 摘录于深入理解Java虚拟机 第六章

读者阅读本章时,大概会不可避免地感到 比较枯燥,但这部分内容又是Java虚拟机的重要基础之一,是了解虚拟机的必经之路,如果想比较深 入地学习虚拟机相关知识,这部分是无法回避的。

​ 软件领域的任何问题,都可以通过增加一个中间层来解决。

​ 字节码文件就是硬件和java之间的那个中间层。而且现在还有更多同样运行在JVM中的语言

​ 如Kotlin、Clojure、Groovy、JRuby、JPython、Scala等,随着时间的推移,

​ 实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不与包括Java语言在内的任何 程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机 指令集、符号表以及若干其他辅助信息。

Class类文件的结构

​ Java技术能够一直保持着非常良好的向后兼容性,Class文件结构的稳定功不可没,任何一门程序 语言能够获得商业上的成功,都不可能去做升级版本后,旧版本编译的产品就不再能够运行这种事情。时至今日对Class文件格式进行了几次更新,但基本上只是在原有结构基础上 新增内容、扩充功能,并未对已定义的内容做出修改。——是不是很熟悉,设计模式中的开闭原则,其实好的设计都是共通的。

​ Class有对应定义信息是充分而不必要条件。譬如类或接口也可以动态生成,直接送入类加载器中。

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

​ 根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。后面的解析都要以这两种数据类型为基础,所以这里笔者必须先解释清楚这两个概念。无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个 字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串 值。 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名 都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视 作是一张表,这张表由表6-1所示的数据项按严格顺序排列构成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-74bulDro-1668008455077)(https://raw.githubusercontent.com/13884566853/eck-article-imgs/main/img/某文章image-20221003204206408.png)]

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的 容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集合”。

魔数与Class文件的版本

​ 每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为 一个能被虚拟机接受的Class文件。使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动。 紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后 的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能 向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,因为《Java虚拟机规范》在Class文 件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class 文件。 例如,JDK 1.1能支持版本号为45.0~45.65535的Class文件,无法执行版本号为46.0以上的Class文 件,而JDK 1.2则能支持45.0~46.65535的Class文件。本章后面的内容都将以这段程序使用JDK11编译输出的Class文件为基础来进行讲解,建议读者不妨用较新版本的JDK跟随 本章的实验流程自己动手测试一遍。

本机环境

openjdk version "11.0.16" 2022-07-19
OpenJDK Runtime Environment GraalVM CE 22.2.0 (build 11.0.16+8-jvmci-22.2-b06)
OpenJDK 64-Bit Server VM GraalVM CE 22.2.0 (build 11.0.16+8-jvmci-22.2-b06, mixed mode, sharing)

源码

public class TestClass {
    private int m;
    public int inc() {
        return m + 1;
    }
}

​ 编译后的class文件用编辑器打开

cafe babe 0000 0037 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 000e 5465 7374 436c 6173 732e
6a61 7661 0c00 0700 080c 0005 0006 0100
0954 6573 7443 6c61 7373 0100 106a 6176
612f 6c61 6e67 2f4f 626a 6563 7400 2100
0300 0400 0000 0100 0200 0500 0600 0000
0200 0100 0700 0800 0100 0900 0000 1d00
0100 0100 0000 052a b700 01b1 0000 0001
000a 0000 0006 0001 0000 0001 0001 000b
000c 0001 0009 0000 001f 0002 0001 0000
0007 2ab4 0002 0460 ac00 0000 0100 0a00
0000 0600 0100 0000 0400 0100 0d00 0000
0200 0e

​ 见开头4个字 节的十六进制表示是0xCAFEBABE,代表次版本号的第5个和第6个字节值为0x0000,而主版本号的值 为0x0037,也即是十进制的55,该版本号说明这个是可以被JDK11或以上版本虚拟机执行的Class文件。

常量池

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

​ 由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始 的,如上文件所示,常量池容量(偏移地址:0x00000008)为十六进制数0a00 ,即十进制的22,这就 代表常量池中有21项常量,索引值范围为1~21。在Class文件格式规范制定之时,设计者将第0项常量 空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下 需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。Class文件结构中只有 常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的 容量计数都与一般习惯相同,是从0开始。

​ Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class 文件的时候进行动态连接(具体类加载的那两篇文章中)。也就是说,在Class文件中不会保存各个方法、字段最终 在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的 内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号 引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接的内容, 在下一章介绍虚拟机类加载过程时再详细讲解。

Tips

​ 顺便提一下,String是可以定义无限长的吗,起初我以为是根据内存来限制的,后来发现长度其实不刻意超过65535,这个数字只要学计算机的应该很敏感,2的16次方。是因为由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名 称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的 最大长度就是length的最大值,既u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB 英文字符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译。

分析字节码

​ 一般情况我们是不会肉眼看class文件的,都是借助工具进行分析的

​ 在JDK的bin目录中,javap工具使用命令

javap -verbose TestClass

结果如下

D:\>javap -verbose TestClass
Classfile /D:/TestClass.class
  Last modified 2022103; size 275 bytes
  MD5 checksum a1e1d229da8f919d2d65ef0503db863c
  Compiled from "TestClass.java"
public class TestClass
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #3                          // TestClass
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // TestClass.m:I
   #3 = Class              #17            // TestClass
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               TestClass.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               TestClass
  #18 = Utf8               java/lang/Object
{
  public TestClass();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public int inc();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 4: 0
}
SourceFile: "TestClass.java"

​ 从代码清单中可以看到,计算机已经帮我们把整个常量池的21项常量都计算了出来。仔细看一下会发现,其中有些常量似乎从来 没有在代码中出现过,如“I”“V”“”“LineNumberTable”“LocalVariableTable”等,这些看起来在源代 码中不存在的常量是哪里来的?

​ 这部分常量的确不来源于Java源代码,它们都是编译器自动生成的,会被后面即将讲到的字段表 (field_info)、方法表(method_info)、属性表(attribute_info)所引用,它们将会被用来描述一些不 方便使用“固定字节”进行表达的内容,譬如描述方法的返回值是什么,有几个参数,每个参数的类型 是什么。因为Java中的“类”是无穷无尽的,无法通过简单的无符号数来描述一个方法用到了什么类, 因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。

​ 这部分内容将在后面进一步详细阐述。最后,笔者将17种常量项的结构定义总结为表6-6。

访问标志

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

# 示例代码
public class TestClass {
        private int m;
        public int inc() {
        return m + 1;
    }
}

flags: (0x0021) ACC_PUBLIC, ACC_SUPER
# 抽象类示例代码
public abstract class AbstractTestClass {
        private int m;
        public int inc() {
        return m + 1;
    }
}

 flags: (0x0421) ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PSUKRW59-1668008455078)(https://raw.githubusercontent.com/13884566853/eck-article-imgs/main/img/某文章image-20221003222319009.png)]

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

  this_class: #3                          // TestClass
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1

字段表集合

Tips

为什么java不通过不同的返回值重载方法的原因就在下面红色字体标记中

java源文件

private int m;

javap

   #2 = Fieldref           #3.#16         // TestClass.m:I

对应class文件中

0100         0200         0500       0600
fidlds_count access_flags name_index descriptor_index

方法表集合

​ 方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,如表6-11所示。这些数据项目的含义也与字段表中的非常类似,仅在访问标 志和属性表集合的可选项中有所区别。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aWHpXtOz-1668008455079)(https://raw.githubusercontent.com/13884566853/eck-article-imgs/main/img/某文章image-20221003224423093.png)]

java源文件

public class TestClass {
    private int m;
    public int inc() {
        return m + 1;
    }
}

javap

 public int inc();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 4: 0

对应的class文件中

0200         0100          0700       0800             0100             0900
methods_count access_flags name_index descriptor_index attributes_count attributes_name_index

​ 与字段表集合相对应地,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出 现来自父类的方法信息。但同样地,有可能会出现由编译器自动添加的方法,最常见的便是类构造 器“()”方法和实例构造器“()”方法[1]。 在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名[2]。特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合,也正是因为返回值不会包含在特征签名之中,所以Java语言里面是无法仅仅依靠返回值 的不同来对一个已有方法进行重载的。但是在Class文件格式之中,特征签名的范围明显要更大一些, 只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签 名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。

属性表集合

​ 属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以 携带自己的属性表集合,以描述某些场景专有的信息。

字节码指令简介

​ Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode) 以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)构成。由于Java虚拟机采用面向操作数栈而不是面向寄存器的架构(这两种架构的执行过程、区别和影响将在后续虚拟机执行引擎文章中探讨),所 以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。

​ 字节码指令集可算是一种具有鲜明特点、优势和劣势均很突出的指令集架构,这就意味着虚拟机在处理那些超过一个字节的数据时,不得不在运行时从字节中重建出具体数据的结构,这种操作在某种程度上会导致解释执行字节码时将损失一些性能,但这样做的优势也同样明显: 放弃了操作数长度对齐[1],就意味着可以省略掉大量的填充和间隔符号;用一个字节来代表操作码, 也是为了尽可能获得短小精干的编译代码。这种追求尽可能小数据量、高传输效率的设计是由Java语 言设计之初主要面向网络、智能家电的技术背景所决定的,并一直沿用至今。

字节码与数据类型

在Java虚拟机的指令集中,大多数指令都包含其操作所对应的数据类型信息,iload指 令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。这两 条指令的操作在虚拟机内部可能会是由同一段代码来实现的,但在Class文件中它们必须拥有各自独立 的操作码。因为Java虚拟机的操作码长度只有一字节,所以包含了数据类型的操作码就为指令集的设计带来 了很大的压力:如果每一种与数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那么 指令的数量恐怕就会超出一字节所能表示的数量范围了。

加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈(栈帧后面会做介绍)之间来回传输,这类指令包括:

将一个局部变量加载到操作栈:iload、iload_、lload、lload_、fload、fload_、dload、 dload_、aload、aload_

将一个数值从操作数栈存储到局部变量表:istore、istore_、lstore、lstore_、fstore、 fstore_、dstore、dstore_、astore、astore_

将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、 iconst_*、lconst_、fconst_、dconst_

扩充局部变量表的访问索引的指令:wide

存储数据的操作数栈和局部变量表主要由加载和存储指令进行操作,除此之外,还有少量指令,如访问对象的字段或数组元素的指令也会向操作数栈传输数据。

运算指令

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

·加法指令: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

类型转换指令

类型转换指令可以将两种不同的数值类型相互转换,转换过程很可能会导致数值的精度丢失。

对象创建与访问指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令(在下一章会讲到数组和普通类的类型创建过程是不同的)。

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

创建类实例的指令: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

操作数栈管理指令

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

将操作数栈的栈顶一个或两个元素出栈:pop、pop2

复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、 dup2_x1、dup_x2、dup2_x2

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

控制转移指令

控制指令就是在有条件或无条件地修改PC寄存 器的值。控制转移指令包括:

​ 条件分支: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

方法调用和返回指令

方法调用(分派、执行过程)将在第8章具体讲解,这里仅列举以下五条指令用于方法调用:

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

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

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

​ invokestatic指令:用于调用类静态方法(static方法)。

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

​ 前面四条调用指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。 方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返 回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一 条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。

异常处理指令

​ 在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现,除了用throw语句显式抛 出异常的情况之外,检测到异常状况时自动抛出,就是最常见的runtime异常。

​ 而在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和 ret指令来实现,现在已经不用了),而是采用异常表来完成。

同步指令(重点,锁的原理就在这)

​ Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将它称为“锁”)来实现的。

方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成 还是非正常完成)时释放管程。

​ 在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取 到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。 同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中 有monitorenter和monitorexit两条指令来支持synchronized关键字的语义>,正确实现synchronized关键字 需要Javac编译器与Java虚拟机两者共同协作支持,譬如有代码清单6-6所示的代码。

void onlyMe(Foo f) {
	synchronized(f) {
		doSomething();
	}
}

​ 编译后,这段代码生成的字节码序列如下:

Method void onlyMe(Foo)
0 aload_1 // 将对象f入栈
1 dup // 复制栈顶元素(即f的引用)
2 astore_2 // 将栈顶元素存储到局部变量表变量槽 2中
3 monitorenter // 以栈定元素(即f)作为锁,开始同步
4 aload_0 // 将局部变量槽 0(即this指针)的元素入栈
5 invokevirtual #5 // 调用doSomething()方法
8 aload_2 // 将局部变量Slow 2的元素(即f)入栈
9 monitorexit // 退出同步
10 goto 18 // 方法正常结束,跳转到18返回
13 astore_3 // 从这步开始是异常路径,见下面异常表的Taget 13
14 aload_2 // 将局部变量Slow 2的元素(即f)入栈
15 monitorexit // 退出同步
16 aload_3 // 将局部变量Slow 3的元素(即异常对象)入栈
17 athrow // 把异常对象重新抛出给onlyMe()方法的调用者
18 return // 方法正常返回
Exception table:
FromTo Target Type
		4 10 13 any
		13 16 13 any

​ 编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须有其对 应的monitorexit指令,而无论这个方法是正常结束还是异常结束。

​ 从代码清单6-6的字节码序列中可以看到,为了保证在方法异常完成时monitorenter和monitorexit指 令依然可以正确配对执行,编译器会自动产生一个异常处理程序,这个异常处理程序声明可处理所有 的异常,它的目的就是用来执行monitorexit指令。

公有设计,私有实现

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

​ 将输入的Java虚拟机代码在加载时或执行时翻译成另一种虚拟机的指令集;

​ 将输入的Java虚拟机代码在加载时或执行时翻译成宿主机处理程序的本地指令集(即即时编译器 代码生成技术)。

​ 精确定义的虚拟机行为和目标文件格式,不应当对虚拟机实现者的创造性产生太多的限制,Java 虚拟机是被设计成可以允许有众多不同的实现,并且各种实现可以在保持兼容性的同时提供不同的新的、有趣的解决方案。

Class文件结构的发展

​ 在JDK 5到JDK 12发展过程中一共增加了20项新属性,这些属性大部分是用于支持Java中许多新出现 的语言特性,如枚举、变长参数、泛型、动态注解等。还有一些是为了支持性能改进和调试信息,譬 如JDK 6的新类型校验器的StackMapTable属性和对非Java代码调试中用到的SourceDebugExtension属 性。 Class文件格式所具备的平台中立(不依赖于特定硬件及操作系统)、紧凑、稳定和可扩展的特 点,是Java技术体系实现平台无关、语言无关两项特性的重要支柱。

这种只增不改的变动恰恰体现了六大设计原则中——开放封闭原则(OCP,Open Closed Principle)的优秀特性。

本章总结

​ Class文件是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础支柱之一。了解Class文件 的结构对后面进一步了解虚拟机执行引擎有很重要的意义。 本章详细讲解了Class文件结构中的各个组成部分,以及每个部分的定义、数据结构和使用方法。 通过代码清单6-1的Java代码及其Class文件样例,以实战的方式演示了Class的数据是如何存储和访问 的。之前的类加载机制的文章中,我们将以动态的、运行时的角度去看了字节码流在虚拟机执行引擎中是如何被解释执行的。结合看后会有更深的体悟,先用起来,再看原理是我一贯秉承的学习新技术的理念。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值