JVM Specification notes 1 -Jvm Structure

摘要: Jvm Structure

正文:

Java 虚拟机结构

Class文件格式

数据类型

  • 原始类型(基本类型)
    • 数值类型{整数[byte8 short16 int32 long64 char16]、浮点[float32 double64]}
    • 布尔类型{boolean8}
    • returnAddress类型{表示一条字节码指令的操作码}
  • 引用类型

编译器应当在编译期间尽最大努力完成可能的类型检查,使得虚拟机在运行期间无需进行这些操作

编译器会在编译期或运行期会将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据

运行时数据区

程序运行期间会使用到的运行时数据区

PC寄存器

每一个虚拟机线程都有自己的PC寄存器,保存Java虚拟机正在执行的字节码指令的地址

Java 虚拟机栈

栈与线程同时创建,存储局部变量与一些过程结果的地方

Java堆

可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域

Java堆在虚拟机启动的时候就被创建,它存储了被自动内存管理系统所管理的各种对象,这些受管理的对象无需,也无法显式地被销毁

方法区

可供各条线程共享的运行时内存区域

方法区在虚拟机启动的时候被创建,存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法

虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集

运行时常量池

每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用

每一个运行时常量池都分配在Java虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来

本地方法栈

如果支持本地方法栈,则会在线程创建的时候按线程分配

栈帧(Frame)

用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派

栈帧随着方法调用而创建,随着方法结束而销毁,无论方法是正常完成还是异常完成

栈帧的存储空间分配在Java虚拟机栈

每一个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用

栈帧容量的大小仅仅取决于Java虚拟机的实现和方法调用时可被分配的内存

一条线程只有正在执行某个方法的栈帧是活动的,叫做当前栈帧,对应的方法叫当前方法,定义这个方法的类叫当前类。对局部变量表和操作数栈的各种操作,通常指的是当前栈帧进行的操作

栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一条线程的栈帧

如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。当一个新的方法被调用,则会新建一个栈帧并成为当前栈帧,当方法返回时会将结果(当前新的栈帧)返回给上一个栈帧,当前栈帧丢弃,上一个栈帧重新成为当前栈帧。

局部变量表

长度由编译期决定,存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用

  • 一个局部变量可以保存类型boolean、byte、char、short、float、reference和returnAddress的数据
  • 两个局部变量可以保存类型为long和double的数据

当方法被调用时候,参数将会传递至从0开始的连续的局部变量表里。如果是实例方法被调用则第0个局部变量一定是this

局部变量使用索引来进行定位访问,0-max

long和double这种需要两个局部变量的类型,索引取最小的那个局部变量。

操作数栈

同局部变量表,长度由编译期决定,存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用

操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。

Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果,例子参考初识jvm指令执行流程

一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度

动态链接

在Class文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用来表示的,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用

类加载的过程中将要解析掉尚未被解析的符号引用,并且将变量访问转化为访问这些变量的存储结构所在的运行时内存位置的正确偏移量

由于动态链接的存在,通过晚期绑定(Late Binding)使用的其他类的方法和变量在发生变化时,将不会对调用它们的方法构成影响

浮点算法

Java虚拟机采纳了《IEEE Standard for Binary Floating-Point Arithmetic》(ANSI/IEEE Std. 754-1985,New York)浮点算法规范中的部分子集

Java虚拟机和IEEE 754中的浮点算法

  • 在Java虚拟机中的浮点操作在遇到非法操作,如被零除(Divison By Zero)、上限溢出(Overflow)、下限溢出(Underflow)和非精确(Inexact)时,不会抛出exception、trap或者其他IEEE 754异常情况中定义的信号。

    1
    2
    double d = 1;
    System.out.println(d/0); //Infinity 非exception
  • Java虚拟机里面,将浮点数转化为整型数是使用向零舍入(去尾操作)

    1
    2
    double d = 1.61;
    System.out.println((int)d);//1 非2

初始化方法的特殊命名

  • <init>

    在Java虚拟机层面上,Java语言中的构造函数是以一个名为<init>的特殊实例初始化方法的形式出现的

    <init>这个方法名称是由编译器命名的,因为它并非一个合法的Java方法名字,不可能通过程序编码的方式实现。实例初始化方法只能在实例的初始化期间,通过Java虚拟机的invokespecial令来调用,只有在实例正在构造的时候,实例初始化方法才可以被调用访问

  • <clinit>

    类或者接口是通过<clinit>方法完成初始化的,这个名字也是由编译器命名的,没有任何虚拟机字节码指令可以调用这个方法,只有在类的初始化阶段中会被虚拟机自身调用

异常的处理

抛异常的本质实际上是程序控制权的一种即时的、非局部(Nonlocal)的转换——从异常抛出的地方转换至处理异常的地方

  • 同步异常

    当前线程执行的某个操作所导致的异常

  • 异步异常

    其他线程中出现的异常

由Java虚拟机执行的每一个方法都会配有零至多个异常处理器(Exception Handlers),异常处理器描述了其在方法代码中的有效作用范围(通过字节码偏移量范围来描述)、能处理的异常类型以及处理异常的代码所在的位置

当有异常被抛出时,Java虚拟机会搜索当前方法的包含的各个异常处理器,如果能找到可以处理该异常的异常处理器,则将代码控制权转向到异常处理器中描述的处理异常的分支之中

字节码指令

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
如果忽略异常处理,那Java虚拟机的解释器使用下面这个伪代码的循环即可有效地工作:

1
2
3
4
5
do { 
自动计算PC寄存器以及从PC寄存器的位置取出操作码;
if (存在操作数) 取出操作数;
执行操作码所定义的操作
} while (处理下一次循环);

如果要将一个16位长度的无符号整数使用两个无符号字节存储起来,如下所示

(byte1 << 8) | byte2

加载和存储指令

  • xload

    从局部变量加载到操作数栈

  • xstore

    从操作数栈存储到局部变量表

  • xpush,xdc,xconst

    将一个常量加载到操作数栈

  • wide

    扩充局部变量表的访问索引

运算指令

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

  • 整型数据运算

    如:iadd,isub,imul,idiv

  • 浮点型数据运算

    如:fadd,fsub,fmul,fdiv

类型转换指令

将两种Java虚拟机数值类型进行相互转换

  • 宽化类型转换

    小范围类型向大范围类型的安全转换,无需显式的转换指令

  • 窄化类型转换

    (i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l,d2f)可能会导致转换结果产生不同的正负号、不同的数量级,数值丢失精度等

注意:

  1. 浮点型转整型 NaN->0
  2. 尽管可能发生上限溢出、下限溢出和精度丢失等情况,但是Java虚拟机中数值类型的窄化转换永远不可能导致虚拟机抛出运行时异常

对象创建与操作

  • 创建类实例:new
  • 创建数组:newarray,anewarray,multianewarray
  • 访问类字段(static)和实例字段(!static):getfield,putfield,getstatic,putstatic
  • 将一个数组元素加载到操作数栈:xaload
  • 将一个操作数栈值储存到数组元素中:xastore
  • 取数组长度的指令:arraylenth
  • 检查类实例类型的指令:instanceof,checkcast

控制转移指令

  • 条件分支

    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

各种类型的比较最终都会转化为int类型的比较操作:

boolean类型、byte类型、char类型和short类型的条件分支比较操作,都使用int类型的比较指令来完成。而对于long类型、float类型和double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整形值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转

方法调用与返回指令

  • 方法调用

    invokevirtual(调用对象的实例方法);

    invokeinterface(调用接口方法->找到实现接口的对象->找出适合的方法;

    invokespecial(调用特殊处理的实例方法:初始化方法 私有方法 父类方法);

    invokestatic(调用类方法)

  • 方法返回

    return(void)

    xreturn(返回类型x)

抛出异常

在程序中显式抛出异常的操作会由athrow指令实现,除了这种情况,还有别的异常会在其他Java虚拟机指令检测到异常状况时由虚拟机自动抛出

同步

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步

  • 方法级同步

    隐式,无需通过字节码指令来控制

  • 指令集序列同步

    通常是由Java语言的synchronized块来表示,java虚拟机的指令集有monitorenter和monitorexit两条指令来支持synchronized关键字的语义

  • 结构化锁定

    方法调用期间每一个管程退出都与前面的管程进入相匹配的情形,持有与释放次数相等