【有书共读】《深入理解java虚拟机》读书笔记031

第三部分:虚拟机调用子系统

§1 - Java的类文件结构

Author:Sirice

Sirice-Github

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

Java语言中的各种变量、关键字和运算符号的语义最终都是有多条字节码指令组合而成,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更加强大。

Class类文件的结构

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(比如类或接口可以通过类加载器直接生成)
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这就使得Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的为结构来存储数据,这种微结构中只有两种数据类型:无符号数和表。

  1. 无符号数

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

表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性的以"_info"结尾。表用于描述有层次关系的符合结构的数据,整个Class文件本质上就是一张表,其构成成分就是如下的数据项:
魔数与Class文件的版本

1.魔数0XCAFEBABE

2.次版本号和主版本号

常量池
1.类和接口的全限定名

2.字段的名称和描述符

3.方法的名称和描述符

访问标志
1.类的访问信息

2.接口的访问信息

类索引、父类索引

和接口索引集合

存储类、父类、接口的

文件索引

字段表集合
1.字段作用域

2.是否static

3.可变性

4.并发可见性

5.可否被序列化

6.字段数据类型

7.字段名称

方法表集合
1.访问标志

2.名称索引

3.描述符索引

4.属性表集合

属性表集合
1.Code属性

2.Exceptions属性

3.LocalVariableTable属性

4.LineNumberTable属性

5.SourceFile属性

6.ConstantValue属性

7.InnerClasses属性

8.Deprecated和Synthetic属性

9.StackMapTable属性

10.Signature属性

11.BootstrapMethods属性

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。Class文件的结构不象XML等描述语言,由于它没有任何分隔符号,所以上面表格中的数据项,无论是顺序还是数量,甚至于数据存储的字节这样的细节都是被***限定的,哪个字节代表什么含义,长度多少,先后顺序如何,都不允许改变。

魔数与Class文件的版本

每个Class文件的头4个字节称为魔数(Magic Number),他的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。之所以用魔数来表示主要是因为考虑到安全性,因为拓展名很容易修改啊。Class文件的魔数值是:0xCAFEBABE(咖啡宝贝?这是java语言发展的有趣的历史)。紧接着魔数的4个字节是Class文件的版本号:第五和第六个字节是次版本号,第七和第八识主版本号,Java的版本号是从45开始的。

常量池

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

由于常量池中常量的数量是不固定的,所以需要在常量池的入口放置一项u2类型的数据,代表常量池容量计数值,与Java中语言习惯不一样的是,这个容量计数是从1开始而不是从0开始。常量池中主要存放两大类常量:字面量(Literial)和符号引用(Symbolic Reference),字面量比较接近于Java语言层面的常量概念,比如文本字符串、声明为final的常量值等,而符号引用则属于编译原理方面的概念,包括下面三类常量。

(1)类和接口的全限定名
(2)字段的名称和描述符
(3)方法的名称和描述符

Java代码在进行Javac编译的时候,并不像C和C++那样有连接这一步骤,而是在虚拟机加载Class文件的时候进行动态链接,也就是说,在Class文件中不会保存各个方法字段的最终内存布局信息,因此这些字段、方法符号引用不经过运行期转换的话无法得到真正的内存地址,也就无法直接被虚拟机使用,当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池中每一项常量都是一张表,目前总共有14种常量,这14种常量类型所代表的具体含义如下表所示:

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 长整型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 标识方法类型
CONSTANT_InvokeDynamic_info 18 标识一个动态方法调用点

访问标志

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

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

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合石一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于Java语言不允许多重继承,所以父类索引只有一个,除了Object之外,所有的类都有父类。

类索引、父类索引、接口集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,他们各自指向一个类型为CONSTANT_CLASS_info的类描述符常量。

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级别变量以及实例级别变量,但不包括在方法内部声明的局部变量。我盟可以想象一下在Java中描述一个字段可以包含什么信息?可以包含的信息有:字段的作用域、是实例变量还是类变量,可否被序列化,字段数据类型等。

方法表集合

如果理解了上面的字段表集合的内容,那么理解其方法表集合就会很简单,Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,包括了访问标志、名称索引、描述符索引、属性表集合几项。

属性表集合

前面的Class文件、字段表和方法表都可以携带自己的属性信息,这个信息用属性表进行描述,用于描述某些场景专有的信息。在属性表中没有类似Class文件的数据项目类型和顺序的严格要求,只要新的属性不与现有的属性名重复,任何人都可以向属性表中写入自己定义的属性信息。

  1. Code属性

Java程序方法体中的代码经过javac编译最终编译成的字节码指令就保存在Code属性中。但是并非所有的方法表都必须存在这个属性。Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code)和元数据(Metadata,包括类、字段、方法定义及其其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有其他的数据项目都用于描述元数据。

  1. Exceptions属性

这个属性的作用是列举出方法中可能抛出的受查异常(Checked Exception),也就是描述throws 后的列举的异常

  1. LineNumberTable属性

主要用于描述Java源代码行号与字节码行号之间的对应关系。这个属性也不是必须的。如果没有这个属性,对程序的直接影响就是当抛出异常的时候无法显示对应的行号;并且在调试的时候无法通过设置断点的方法是调试程序。

  1. LocalVariableTable属性

用于描述栈帧中局部变量表中的变量与Java源码中定义的变量的之间的关系。也不属于必须的属性。如果没有这个属性,产生的直接影响就是当别人引用这个方法的时候,所有的参数名称都会丢失,IDE将会使用诸如args0、args1之类的参数进行显示。自然,当调试程序的时候,显示的参数名称是不可知的。

  1. SourceFile属性

用于记录这个Class文件的源码文件名称。如果不使用这个属性,那么当抛出异常的时候,堆栈中将不会显示出错代码所属的文件名。

  1. ConstantValue属性

作用是通知虚拟机自动为静态变量赋值。要注意的是,只有被static关键字修饰的额变量才可以使用这个属性(类变量)。对于非类变量,初始化是在方法中进行的;对于类变量可以选择两种方式进行变量的初始化:一是在类构造器方法中使用;二是是ConstantValue属性。目前Sun Hotspot的选择原则是:如果一个变量同时使用static和final关键字修饰,并且这个变量是基本数据类型或者java.lang.String类型的话,就使用ConstantValue属性进行初始化。如果没有被final修饰或者并非是基本数据类型,那么将会选择使用方法进行初始化。

  1. InnerClass属性

这个属性主要用于记录内部类与宿主类之间的关联关系。

  1. Deprecated以及Synthetic属性

这两个属性都属于标志类型的布尔属性,只存在有没有的区别。

Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,可以通过注解@deprecated实现

Synthetic属性代表此字段并不是由Java源码产生的,而是通过编译器自行添加的。

  1. StackMapTable属性

该属性的目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

  1. Signature属性

这个属性是专门用来记录泛型类型的,因为在Java语言采用的是擦除法实现的泛型,在字节码(Code属性)中,泛型信息编译之后会被擦除。擦除法的优点是能够节省泛型所占的内存空间,缺点是在运行期间无法通过反射得到泛型信息,而Signature属性则弥补了这一缺陷。现在的Java反射API已经能够得到泛型信息,功劳就在于这个属性。

  1. BootstrapMethods属性

这个属性用于保存invokedynamic指令引用的引导方法限定符。(该指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。)

字节码指令

Java虚拟机的指令是由一个字节长度、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数而构成,由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。

字节码与数据类型

在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。例如iload指令用于从局部变量表中加载int类型的数据到操作数栈中,而fload用于加载float类型的数据了。对于大部分与数据类型相关的字节码指令,他们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:ibiaoshiduiint类型的数据操作,l代表long。s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。

加载和存储指令

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

  1. 将一个局部变量加载到操作数栈:iload、iload、lload、lload、fload、fload、dload、dload、aload、aload_。
  2. 将一个数值从操作数栈存储到局部变量表:istore、istore_、lstore、lstore。。。。
  3. 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w等。
  4. 拓充局部变量表的访问索引的指令:wide。

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

运算指令

运算或算数指令用于对两个操作数栈上的值进行某种特定的运算,并把结果重新存入到操作数栈顶。大体运算指令可以分为两种:堆整型数据进行运算的指令与对浮点型数据进行运算的指令,无论是哪种算数指令,都使用Java虚拟机的数据类型,由于没有直接支持byte、short、char、boolean类型的算数指令,对于这些类型数据的运算,应使用操作int类型的指令代替。所有的算数指令如下

  1. 加法指令:iadd、ladd、fadd、dadd
  2. 减法指令:isub、lsub、fsub、dsub
  3. 乘法指令:imul、lmul、fmul、dmul
  4. 除法指令:idiv、ldiv、fdiv、ddiv
  5. 求余指令:irem、lrem、frem、drem
  6. 取反指令:ineg、lneg、fneg、dneg
  7. 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
  8. 按位或指令:ior、lor
  9. 按位与指令:iand、land
  10. 局部变量自增指令:iinc
  11. 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

对象创建与访问指令

虽然类实力和数组都是对象,但Java虚拟机对类实力和数组的创建与操作使用了不同的字节码指令。对象的创建指令如下:

  1. 创建类实力的指令:new
  2. 创建数组的指令:newarray、anewarray、multianewarray
  3. 访问类字段和实例字段:getfield、putfield、getstatic、putstatic。
  4. 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
  5. 讲一个操作数栈的值存储到数组中的指令:bastotr、castore、sastore、iastore、fastore、dastore、aastore
  6. 取数组长度的指令:arraylenght
  7. 检查类实力的指令:instanceof、checkcast

操作数栈管理指令

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

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

控制转移指令

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

  1. 条件分支:ifeq、iflt、ifle、ifne、ifge、ifnull、ifnonull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_campge和if_acmpne
  2. 符合条件分支:tableswitch、lookupswitch
  3. 无条件分支:goto、goto_w、jsr、jsr_w、ret

Java虚拟机中有专门的指令集用来处理int和reference类型的条件分支比较操作,
方法调用和返回指令

  1. invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行分派(调用),这也是Java语言中最常见的犯法分派方式。
  2. invokeeinterface:用于调用接口方法,他会在运行时搜索一个实现了这个接口方法的对象,找出合适的方法进行调用。
  3. invokeespecial:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
  4. invokestatic:用于调用static方法。
  5. invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法,并执行方法。
    方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturtn和arrturn。

除了上述一些指令外,还有异常处理指令、同步指令,这里就不再多说。

#读书笔记##笔记#
全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务