JVM之类文件结构

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

无关性的基石

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

虚拟机发展到JDK 1.7~1.8的时候,JVM设计者通过JSR-292实现了其他语言运行与JVM上
unattached

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
    classfile

魔数(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

常量池的项目类型
这里写图片描述

表的结构组成

常量池的表除了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文件的工具,运行例子如下图

javap

访问标志(Access Flags)

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

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

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

下图表
访问标志

类索引、父类索引与接口索引集合(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就结束了

下图

字段表结构

方法表集合(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_SYNCHRONIZED0x0020是否为synchronized
ACC_BRIDGE0x0040是否是由编译器产生的桥接方法
ACC_VARARGS0x0080是否接受不定参数
ACC_NATIVE0x0100是否为native
ACC_ABSTRACT0x0400是否为abstract
ACC_STRICPFP0x0800是否为strictfp

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

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

属性表集合(Attributes)

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

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

名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表final定义的常量值
Exceptions方法表方法抛出的异常
EnclosingMethod类文件仅当一个类为内部类,或匿名类时才有的属性,用于标识包着这个类的外围方法
InnerClass类文件内部类列表
LineNumberTableCode属性Java源码与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
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_<n>,lload,lload_<n>,fload,fload_<n>,dload,dload_<n>,aload,aload_<n>
  • 将1个数值从操作数栈存储到局部变量表

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

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

    • 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代码生成技术
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值