JVM之类文件结构

  • Class类文件的结构
    • 魔数Magic Number与Class文件的版本Version
    • 常量池Constant Pool
    • 访问标志Access Flags
    • 类索引父类索引与接口索引集合This ClassSuper Class Interfaces
    • 字段表集合Fields
    • 方法表集合Methods
    • 属性表集合Attributes
  • 字节码指令
    • 加载和存储指令
    • 运算指令
    • 对象创建与访问指令
    • 操作数栈管理指令
    • 控制转移指令
    • 方法调用和返回指令
    • 异常处理指令
    • 同步指令
  • 公有设计私有实现

JVM不和Java在内任何语言绑定,只与class文件,这种特定的二进制文件格式关联。Class字节码文件中包含虚拟机指令集和符号表,以及若干其它辅助信息

无关性的基石

  • 虚拟机
  • 字节码存储格式

虚拟机发展到JDK 1.7~1.8的时候,JVM设计者通过JSR-292实现了其他语言运行与JVM上
JVM之类文件结构_第1张图片

Class类文件的结构

class文件是一组以8位字节为基础单位的二进制流,紧凑,中间无分隔符。

当遇到需要占用8位字节以上空间的数据项,按Big-Endian方式分割成若干个8位字节

Class文件格式,是一种类似于C语言结构体的伪结构来存储数据。这种伪结构只有两种数据类型

无符号数
可以描述 数字、索引引用、数量值、按照UTF-8编码构成字符串值
描述有层次关系的符合结构的数据。多个无符号数或者其他表作为数据项构成的复合数据类型,所有表,习惯性地以”_info”结尾
  • 无符号数包括:
    • u1–>1 byte(8 bits)
    • u2–>2 byte2(16 bits)
    • u4–>4 bytes(32 bits)
    • u8–>8 bytes(64 bits)
  • 表包括:
    • 多个无符号数
    • 其他表

整个Class文件,本质上就是一张表

结构总览

  • Magic Number
  • Version
  • Constant Pool
  • Access Flags
  • This Class Name
  • Super Class Name
  • Interfaces
  • Fields
  • Methods
  • Attributes
    JVM之类文件结构_第2张图片

魔数(Magic Number)与Class文件的版本(Version)

每个字节码文件的头4个字节,一定是0xCAFEBABE,这个数值叫做Magic Number

u4这一无符号数(是一种数据类型,看上文)可以表示magic

作用
魔数的唯一作用,就是确定这个文件是否为一个能被虚拟机接收的字节码文件

紧接着4个字节的魔数之后,第5和第6个字节是次版本号(Minor Version)

第7和第8个字节是Class文件的主版本号(Major Version)

常量池(Constant Pool)

接着第8个字节的,是常量迟Constant Pool的入口

  • 可以理解為Class文件中的资源仓库
  • 与其他项目关联最多的数据类型
  • 占用Class文件空间最大的数据类型
  • Class文件中第一个出现的表类型数据项目

由于常量池数量不固定,所以在入口,需要一项u2类型的计数器,constant_pool_count,这是第9个字节

这个技术器跟习惯的不一样,从1开始算,不是00带表”不引用任何一个常量池项目”

存放数据类型:

  • 字面量(literal).
    • 文本字符串
    • final修饰的常量
  • 符号引用(Symbolic References),编译原理方面的概念
    • 类和接口的全限定名(Fully Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符

Java代码进行javac编译时,不像C/C++那样,有”连接”的步骤。

而是在虚拟机加载class文件的时候,进行动态连接。即,Class文件不会保存各个方法、字段的最终内存布局信息。因此,这些字段、方法的引用,不经过运行期转换的话,无法得到真正内存的入口地址,也就无法直接被虚拟机使用

常量池中的每一个常量,都是表

JDK 1.7 为更好支持 动态语言调用,新增CONSTANT_MethodHandle_info,CONSTANT_MethodType_info,CONSTANT_InvokeDynamic_info

JVM之类文件结构_第3张图片
JVM之类文件结构_第4张图片

表的结构组成

常量池的表除了tag,还有自己不同的结构

  • u1类型的标志位tag,取值见上表
  • 各自类型自己的结构

例如:
CONSTANT_MethodHandle_info
- tag(u1类型)
- name_index(u2类型) 索引值,指向CONSTANT_Utf8_info,代表类或接口的全限定名

class文件中,方法和字段,都需要引用CONSTANT_Utf8_info型常量来描述名称,所以utf8_info常量的最大长度,就是Java中方法、字段名的最大长度,u2类型,2个字节,最大值为65535。所以,当定义了超过64K英文字符的变量或方法名,JVM无法编译

JDK的bin目录下有个javap工具,是Oracle为我们准备的查看class文件的工具,运行例子如下图

JVM之类文件结构_第5张图片

访问标志(Access Flags)

常量池结束后,接着,是2个字节的访问标志access_flags
access_flags一共有16个标志位,当前只定义了8个

这个标志用于识别访问权限、访问层次的信息,例如:

  • 这个class是类还是接口
  • 是否public类型
  • 是否abstract
  • 如果是类,是否声明为final

下图表
JVM之类文件结构_第6张图片

类索引、父类索引与接口索引集合(This Class,Super Class, Interfaces)

访问标志之后,是类索引(this_class)、父类索引(super_class)、接口索引集合(interfaces)。前面两个是u2类型数据,接口则是u2类型数据的集合

class文件由这3项数据来确定这个类的继承关系

除了java.lang.Object外,所有类的父类索引都不为0

类索引/父类索引–>持有2个u2类型–>指向CONSTANT_Class_info的类描述符常量–>找到定义在Utf_info中的全限定名

接口索引集合,入口的第一项,是u2类型的数据,接口计数器(interfaces_count),表示索引的容量,如果类没有实现接口,那么计数器的值为0

字段表集合(Fields)

字段表(field_info)用于描述接口,或者类中声明的变量

字段表结构

  • access_flag(u2,分清上面的对类定义的access_flag)
  • name_index(u2)
  • descriptor_index(u2)
  • attributes_count(u2)
  • attributes(attribute_info)

field包括类变量和实例变量,但不包括方法内部声明的局部变量

Java中描述一个字段(field)的标签

  • 作用域(public,protect,default,private)
  • 类变量还是实例变量(static)
  • 可变性(final)
  • 并发可见性(volatile,是否强制从主存读写)
  • 是否参与序列化(transient)
  • 类型(primitive,reference,array)
  • 名称(name)

名称、数据类型适合引用常量池的常量来描述,而其他剩下的,适合用布尔值描述

字段表包含的固定数据项目,到descriptor_index就结束了

下图

JVM之类文件结构_第7张图片

方法表集合(Methods)

理解了上面的字段表的内容,接下来,方法表可以如法炮制。

Java中描述一个方法的标签

  • 作用域(public,protect,default,private)
  • 类方法还是实例方法(static)
  • 可变性(final)
  • 并发同步性(synchronized)
  • 是否严格精度(strictfp)
  • 是否抽象(abstract)
  • 是否native(native)
  • 名称(name)

方法表结构

  • access_flag(u2)
  • name_index(u2)
  • descriptor_index(u2)
  • attributes_count(u2)
  • attributes(attribute_info)

方法没有volatile和transient关键字,所以access_flag就没有这两个标志了,跟方法对应的,增加6个标志

标志名称 标志值 含义
ACC_SYNCHRONIZED 0x0020 是否为synchronized
ACC_BRIDGE 0x0040 是否是由编译器产生的桥接方法
ACC_VARARGS 0x0080 是否接受不定参数
ACC_NATIVE 0x0100 是否为native
ACC_ABSTRACT 0x0400 是否为abstract
ACC_STRICPFP 0x0800 是否为strictfp

方法里的代码,经过编译器编译成字节码指令,存放在方法属性表集合的”code”属性中

重载overload一个方法,除了要有与原方法相同的简单名称之外,还必须有一个与原方法不同的特征签名特征签名,是一个方法中,各个参数在常量池中的字段符号的引用的集合,也就是说,方法参数列表要不一样

属性表集合(Attributes)

属性表attribute_info,在class文件、字段表、方法表内,都可以存在,描述某些场景专有的信息

属性表的数据项目比较多,这里列出一些重要的

名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final定义的常量值
Exceptions 方法表 方法抛出的异常
EnclosingMethod 类文件 仅当一个类为内部类,或匿名类时才有的属性,用于标识包着这个类的外围方法
InnerClass 类文件 内部类列表
LineNumberTable Code属性 Java源码与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
BootStrapMethods 类文件 JDK 1.7新增,保存invokedynamic指令引用的引导方法限定符

属性表结构

  • attribute_name_index(u2)
  • attribute_length(u4)
  • info(u1)

Code属性

Java方法体中的代码,经过编译器处理后,最终变为字节码指令,存储在Code属性中

构成

  • attribute_name_index(u2)
    • 指向CONTANT_Utf8_info类型常量索引,值为”Code”,
  • attribute_length(u4)
  • max_stack
    • 代表操作数栈(Operand Stacks)深度的最大值
    • 虚拟机根据这个值来分配栈帧中的操作栈
  • max_locals
    • 局部变量表所需的存储空间
    • 存储基本单位是Slot(32位)
    • 局部变量表中的Slot可重用
    • 方法参数、方法体中定义的局部变量
  • code_length
  • code
  • exception_table_length
  • exception_table
  • attributes_count
  • attributes

ConstantValue属性

只有类变量/静态变量才有的属性。虚拟机对类变量和实例变量的赋值方式以及时机有所不同

实例变量的赋值

  • 在实例构造器中方法中进行

类变量的赋值

  • 可以在类构造器方法
  • 使用ConstantValue属性

字节码指令

Java虚拟机的指令构成:

  • 1个字节长度的操作码(Opcode)
  • 0到多个操作数(Oprands,代表此操作所需参数)

採用面向操作数栈,而不是寄存器的架构

Java虚拟机的解释器,基本执行模型

do {
    自动计算PC寄存器的值,+1;
    根据PC寄存器的指示位置,从字节码流中取出操作码;
    if (字节码存在操作数)
        从字节码流中取出操作数;
    执行操作码定义的操作
} while(字节码流长度 > 0)

加载和存储指令

用于 栈帧中的局部变量表和操作数栈 之间的来回传输

  • 将1个局部变量加载到操作栈

    • iload, iload_,lload,lload_,fload,fload_,dload,dload_,aload,aload_
  • 将1个数值从操作数栈存储到局部变量表

    • istore,istore_,lstore,lstore_,fstore,ftore_,dstore,dstore_,astore,astore_
  • 将1个常量加载到操作数栈

    • bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_,lconst_,fconst_,dconst_
  • 扩充局部变量表的访问索引的指令

    • wide

运算指令

运算或算术指令,是对两个操作数栈上的值,做某种特定运算,并将运算结果重新存入操作数栈的栈顶

没有直接byte,char,short,boolean类型的算术指令,用操作int类型的指令代替

整型与浮点型在溢出和被0整除时,有不同行为表现

指令包括
加、减、乘、除、求余、取反、位移(i,l)、按位或、按位与、按位异或、局部变量自增(i)、比较(d,f,l)

对象创建与访问指令

虽然类实例和数组都是对象,但虚拟机对这两者的创建与操作,使用了不同的字节码指令

  • 创建类实例的指令: new
  • 创建数组:newarray,anewarray,multianewarray
  • 访问类变量和实例变量:getfield,putfield,getstatic,putstatic
  • 将1个数组元素加载到操作数栈:baload,caload,saload,iaload,laload,faload,daload,aaload
  • 将1个操作数栈的值存储到数组元素中的指令:bastore,castore,sastore,iastore,fastore,dastore,aastore
  • 取得数组长度:arraylength
  • 检测类实例类型:instanceofcheckcast

操作数栈管理指令

操作数栈的操作,无非是压栈、弹栈

  • 将操作数栈的栈顶1个或2个元素出栈:poppop2
  • 复制栈顶1个或2个数值,并将复制值或2份复制值,重新压入栈顶:dupdup2dup_x1,dup2_x1,dup_x2,dup2_x2
  • 栈的最顶端两个数值互换:swap

控制转移指令

使得Java虚拟机有条件,或无条件,从指定的位置指令,继续执行程序,而不是控制转移指令的下一条指令。

概念模型上,可以理解为,在有条件/无条件地修改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

  • 复合条件分支:tableswitchlookupswitch

  • 无条件分支:goto,goto_w,jsr,jsr_w,ret

方法调用和返回指令

  • invokevirtual: 调用对象实例方法
  • invokeinterface:调用接口方法
  • invokespecial:调用需要特殊处理的实例方法,如实例初始化方法、私有方法、父类方法
  • invokestatic:调用类方法
  • invokedynamic:运行时,动态解析出调用点限定符所引用的方法,并执行该方法;分派逻辑由用户设定的引导方法决定

异常处理指令

使用异常表

同步指令

Java虚拟机支持方法级别的同步,以及某段代码(指令序列)的同步,通过管程(Monitor)来支持,叫监视器不挺好的嘛。

方法级别的同步是隐式的,无须通过字节码指令来控制,方法的常量池的方法表结构中的访问标志ACC_SYNCHRONIZED,可以得知,方法是否为同步方法

同步代码块,需要monitorenter,monitorexit2条关键指令

公有设计,私有实现

虚拟机的实现方式,主要有

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

你可能感兴趣的:(Java,JVM)