为JAVA虚拟机编译(第2部分)

3.1 算术运算

Java虚拟机通常基于操作数栈来进行算术运算(只有iinc指令例外,它直接对局部变量进行自增操作),算术运算使用到的操作数都是从操作数栈中弹出的,运算结果被压回操作数栈中。在内部运算时,中间运算(Arithmetic Subcomputations)的结果可以被当作操作数使用

譬如下面的align2grain()方法,它的作用是将int值对齐到2的指定幂次:

int align2grain(int i, int grain) { 
    return ((i + grain-1) & ~(grain-1)); 
}

编译代码如下:

Method int align2grain(int,int) 
0 iload_1 
1 iload_2 
2 iadd 
3 iconst_1 
4 isub 
5 iload_2 // Push grain
6 iconst_1 // Push int constant 1 立即操作数int数值1
7 isub // Subtract; push result
8 iconst_m1 // Push int constant −1
9 ixor // Do XOR; push result (因为~x == −1^x)
10 iand 
11 ireturn

3.4 访问运行时常量池

ldc和ldc_w指令用于访问运行时常量池中的对象 int类型 byte、char、short 类型,包括String实例

当使用的运行时常量池的项的数目过多时(多于256个,1个字节能表示的范围),需要使用ldc_w

ldc2_w 指令用于访问类型为double和long的运行时常量池项,这条指令没有非宽索引的版本(即没有ldc2指令)。

对于整型常量,包括byte、char、short和int使用bipushsipushiconst_<i>指令进行访问。某些浮点常量也可以编译进代码,使用fconst_<f>dconst_<d>指令进行访问。

void useManyNumeric() { 
    int i = 100; 
    int j = 1000000; 
    long l1 = 1; 
    long l2 = 0xffffffff; 
    double d = 2.2; 
    ...do some calculations... 
}

编译后代码如下:

Method void useManyNumeric() 
0 bipush 100 // Push a small int with bipush 
2 istore_1 
3 ldc #1 // Push int constant 1000000; a larger int 
// value uses ldc 
5 istore_2 
6 lconst_1 // A tiny long value uses short, fast lconst_1 
7 lstore_3 
8 ldc2_w #6 // Push long 0xffffffff (that is, an int −1); any 
// long constant value can be pushed using ldc2_w 
11 lstore 5 
13 ldc2_w #8 // Push double constant 2.200000; uncommon 
// double values are also pushed using ldc2_w 
16 dstore 7 
...do those calculations...

3.5 更多的控制结构示例

void whileInt() { 
    int i = 0; 
    while (i < 100) { 
        i++; 
    } 
}

编译后代码如下:

Method void whileInt() 
0 iconst_0 
1 istore_1 
2 goto 8 
5 iinc 1 1 
8 iload_1 
9 bipush 100 
11 if_icmplt 5
14 return

有两条比较指令:对于float类型是fcmplfcmpg指令,对于double是dcmpldcmpg指令 在接 ifltifle

int lessThan100(double d) { 
    if (d < 100.0) { 
        return 1; 
    } else { 
        return -1; 
    } 
}

编译后代码如下:

Method int lessThan100(double) 
0 dload_1 
1 ldc2_w #4 // Push double constant 100.0 
4 dcmpg // Push 1 if d is NaN or d > 100.0; 
// push 0 if d == 100.0 
5 ifge 10 // Branch on 0 or 1 
8 iconst_1 
9 ireturn 
10 iconst_m1 
11 ireturn

3.6 接收参数

传递了n个参数给某个实例方法,则当前栈帧会按照约定的顺序接收这些参数,将它们保存为方法的第1个至第n个局部变量之中

按照约定,实例方法需要传递一个自身实例的引用作为第0个局部变量。在Java语言中自身实例可以通过this关键字来访问。

类(static)方法不需要传递实例引用,所以它们不需要使用第0个局部变量来保存this关键字。所以从0开始 。

3.7 方法调用

int add12and13() { 
    return addTwo(12, 13); 
}

编译后代码如下:

Method int add12and13() 
0 aload_0 // Push local variable 0 (this) 
1 bipush 12 // Push int constant 12 
3 bipush 13 // Push int constant 13 
5 invokevirtual #4 // Method Example.addtwo(II)I 运行时常量池在该索引4含 内部二进制名称、方法名称和方法描述符
8 ireturn // Return int on top of operand stack; it is the int result of addTwo()   

编译器在方法调用时不会处理参数的类型转换问题,只是简单地将参数的压入操作数栈,且不改变其类型。

编译器在生成invokevirtual指令时,也会生成这条指令所引用的描述符,这个描述符提供了方法参数和返回值的信息。

说明

为JAVA虚拟机编译(第1部分)

本章只涉及到从使用Java语言编写的源代码编译为Java虚拟机指令集的编译器。

3.1 示例的格式说明

本章节的示例主要包括有源文件和Java虚拟机代码注解列表(Annotated Listings),其中,Java虚拟机的代码注解列表是由Oracle的1.0.2版本的JDK的javac编译器生成。

Java虚拟机代码将使用Oracle的javap工具所生成的非正式的“虚拟机汇编语言“格式来描述。

<index> <opcode> [<operand1> [<operand2>...]] [<comment>]

是code[]数组中的指令的操作码的索引,也可以认为是相对于方法起始处的字节偏移量 。为指令的操作码的助记符号,是指令的操作数,一条指令可以有0至多个操作数。为行尾的语法注释,譬如:

8 bipush 100 // Push int constant 100

3.2 常量、局部变量的使用和控制结构

spin()是一个很简单的方法,它进行了100次空循环

void spin() { 
    int i; 
    for (i = 0; i < 100; i++) { 
        ; // Loop body is empty 
    } 
}

编译后代码如下:

Method void spin() 
0 iconst_0 // Push int constant 0 
1 istore_1 // Store into local variable 1 (i=0) 
2 goto 8 // First time through don’t increment 
5 iinc 1 1 // Increment local variable 1 by 1 (i++) 
8 iload_1 // Push local variable 1 (i) 
9 bipush 100 // Push int constant 100 
11 if_icmplt 5 // Compare and loop if less than (i < 100)
14 return // Return void when done

Java虚拟机是基于栈架构设计的,它的大多数操作是从当前栈帧的操作数栈取出1个或多个操作数,或将结果压入操作数栈中。每调用一个方法,都会创建一个新的栈帧,并创建对应方法所需的操作数栈和局部变量表。每条线程在运行时的任意时刻,都会包含若干个由不同方法嵌套调用而产生的栈帧,当然也包括了若干个栈帧内部的操作数栈,但是只有当前栈帧中的操作数栈才是活动的。

istore_1指令作用是从操作数栈中弹出一个int型的值,并保存在第一个局部变量中。

iload_1指令作用是将第一个局部变量的值压入操作数栈。

iinc指令的作用是对局部变量加上一个长度为1字节有符号的递增量。

if_icmplt指令将100从操作数栈中弹出值并与i进行比较,如果满足条件(即i的值小于 100),将转移到索引为5的指令继续执行

double类型的值占用两个局部变量的空间。譬如下面例子展示了double类型值的访问:

double doubleLocals(double d1, double d2) { 
    return d1 + d2; 
}

编译后代码如下:

Method double doubleLocals(double,double) 
0 dload_1 // First argument in local variables 1 and 2 
1 dload_3 // Second argument in local variables 3 and 4 
2 dadd 
3 dreturn

注意:局部变量表中使用了一对局部变量来存储doubleLocals()方法中的double值,这对局部变量不能被分开来进行单个操作。

spin()方法的for循环语句中,对于int型值的判断可以统一用if_icmplt指令实现;但是,在Java虚拟机指令集合中,对于double类型的值的没有这样的指令。如果把spin方法变量改为double,必须在dcmpg指令后面再联合iflt指令来实现。

如果把spin方法变量改为double型,编译后代码如下:

Method void spin() 
0 dconst_0 // Push double constant 0.0 
1 dstore_1 // Store into local variables 1 and 2 
2 goto 9 // First time through don’t increment 
5 dload_1 // Push local variables 1 and 2 
6 dconst_1 // Push double constant 1.0 
7 dadd // Add; there is no dinc instruction 
8 dstore_1 // Store result in local variables 1 and 2 
9 dload_1 // Push local variables 1 and 2 
10 ldc2_w #4 // Push double constant 100.0 
13 dcmpg // There is no if_dcmplt instruction 
14 iflt 5 // Compare and loop if less than (i < 100.0)
17 return // Return void when done

如果把变量改为short型,编译后的代码:

Method void spin() 
0 iconst_0 
1 istore_1 
2 goto 10 
5 iload_1 // The short is treated as though an int 
6 iconst_1 
7 iadd 
8 i2s // Truncate int to short 
9 istore_1 
10 iload_1 
11 bipush 100 
13 if_icmplt 5 
16 return

Java虚拟机支持下,对int类型的数据的大部分操作可以直接进行。这在一定程度上是考虑到了Java虚拟机操作数栈和局部变量表的实现效率。当然也有考虑到了大多数程序都会对int型数据进行频繁操作的原因。

在Java虚拟机中,缺乏对byte、char和short类型数据直接操作的支持所带来的问题并不大,因为这些类型的值都在编译过程中就自动被转换为int类型

Java虚拟机对于long和浮点类型(float和double)提供了中等程度的支持,比起int类型数据所支持的操作,它们仅缺少了条件转移指令部分,其他操作都与int类型具有相同程度的支持。

说明

Java虚拟机结构(第4部分)字节码指令集简介

2.11 字节码指令集简介

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。

如果忽略异常处理,那Java虚拟机的解释器使用下面这个伪代码的循环即可有效地工作:

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

操作数的数量以及长度取决于操作码

字节码指令流应当都是单字节对齐的,只有“tableswitch”和“lookupswitch”两条指令例外

限制Java虚拟机操作码的长度为一个字节,并且放弃了编译后代码的参数长度对齐

2.11.1 数据类型与Java虚拟机

在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息

助记符 :i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference

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

类型 long double 属于分类二 其他是分类一

有部分对操作栈进行操作的Java虚拟机指令(例如pop和swap指令)是与具体类型无关的,不过这些指令也必须受到运算类型分类的限制

2.11.2 加载和存储指令

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输

  • 将一个局部变量加载到操作栈的指令包括有:iloadiload_<n>lloadlload_<n>floadfload_<n>dloaddload_<n>aloadaload_<n>

  • 将一个数值从操作数栈存储到局部变量表的指令包括有:istoreistore_<n>lstorelstore_<n>fstorefstore_<n>dstoredstore_<n>astoreastore_<n>

  • 将一个常量加载到操作数栈的指令包括有:bipushsipushldcldc_wldc2_waconst_nulliconst_m1iconst_<i>lconst_<l>fconst_<f>dconst_<d>

有一部分是以尖括号结尾的(例如iload_<n>),这些指令助记符实际上是代表了一组指令(例如iload_<n>,它代表了iload_0iload_1iload_2iload_3这几条指令)这几组指令都是某个带有一个操作数的通用指令。还有些在尖括号之间的字母制定了指令隐含操作数的数据类型。

2.11.3 运算指令

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

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

规定了在处理整型数据时,只有除法指令(idiv和ldiv)以及求余指令(irem和lrem)出现除数为零时会导致虚拟机抛出异常,如果发生了这种情况,虚拟机将会抛出ArithmeitcException异常。

Java虚拟机在处理浮点数时完全支持IEEE 754中定义的非正规浮点数值(Denormalized Floating-Point Numbers,§2.3.2)和逐级下溢(Gradual Underflow)。

Java虚拟机要求在进行浮点数运算时IEEE 754规范中的默认舍入模式,称为向最接近数舍入模式

在把浮点数转换为整数时使用IEEE 754标准中的向零舍入模式

Java虚拟机在处理浮点数运算时,不会抛出任何运行时异常。溢出时,将会使用有符号的无穷大来表示,如果某个操作结果没有明确的数学定义的话,将会时候NaN值来表示

在对long类型数值进行比较时,虚拟机采用带符号的比较方式,采用IEEE 754规范说定义的无信号比较(Nonsignaling Comparisons)方式。

2.11.4 类型转换指令

类型转换指令可以将两种Java虚拟机数值类型进行相互转换,这些转换操作一般用于实现用户代码的显式类型转换操作,或者用来处理Java虚拟机字节码指令集中指令非完全独立独立的问题

Java虚拟机直接支持(译者注:“直接支持”意味着转换时无需显式的转换指令)以下数值的宽化类型转换(Widening Numeric Conversions,小范围类型向大范围类型的安全转换):

  • int类型到long、float或者double类型
  • long类型到float、double类型
  • float类型到double类型

窄化类型转换(Narrowing Numeric Conversions)指令包括有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。

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

NAN --> 0(int) NAN-->NAN(float)

2.11.5 对象创建与操作

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

2.11.6 操作数栈管理指令

Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2和swap。

2.11.7 控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件地从指定指令而不是控制转移指令的下一条指令继续执行程序。

  • 条件分支: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类型的比较操作,基于int类型比较的这种重要性,Java虚拟机提供了非常丰富的int类型的条件分支指令。

所有int类型的条件分支转移指令进行的都是有符号的比较操作。

2.11.8 方法调用和返回指令

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

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

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

  • invokestatic指令用于调用类方法(static方法)。

而方法返回指令则是根据返回值的类型区分的,包括有ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。

2.11.9 抛出异常

显式抛出异常的操作会由athrow指令实现

2.11.10 同步

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程Monitor来支持的。

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作)之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。

同步一段指令集序列通常是由Java语言中的synchronized块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持。需要编译器与Java虚拟机两者协作支持。

结构化锁定(Structured Locking)是指在方法调用期间每一个管程退出都与前面的管程进入相匹配的情形。

假设T代表一条线程,M代表一个管程的话:

  • T在方法执行时持有管程M的次数必须与T在方法完成(包括正常和非正常完成)时释放管程M的次数相等。

  • 找方法调用过程中,任何时刻都不会出现线程T释放管程M的次数比T持有管程M次数多的情况。

2.12 类库

Java虚拟机必须对不同平台下Java类库的实现提供充分的支持,因为其中有一些类库如果没有Java虚拟机的支持的话是根本无法实现的。

可能需要Java虚拟机特殊支持的类库包括有:

  • 反射,譬如在java.lang.reflect包中的各个类和java.lang.Class类
  • 类和接口的加载和创建,最显而易见的例子就是java.lang.ClassLoader类
  • 类和接口的链接和初始化,上一点的例子也适用于这点
  • 安全,譬如在java.security包中的各个类和java.lang.SecurityManager等其他类
  • 多线程,譬如java.lang.Thread类
  • 弱引用,譬如在java.lang.ref包中的各个类

2.13 公有设计,私有实现

(第二章 Java虚拟机结构 全部部分小节) 本书简单描绘了Java虚拟机应有的共同外观:Class文件格式以及字节码指令集等。

Java虚拟机实现必须能够读取Class文件并精确实现包含在其中的Java虚拟机代码的语义。

只要优化后Class文件依然可以被正确读取,并且包含在其中的语义能得到保持,那实现者就可以选择任何方式去实现这些语义,虚拟机后台如何处理Class文件完全是实现者自己的事情,只要它在外部接口上看起来与规范描述的一致即可

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

  • 将输入的Java虚拟机代码在加载时或执行时翻译成另外一种虚拟机的指令集
  • 将输入的Java虚拟机代码在加载时或执行时翻译成宿主机CPU的本地指令集(有时候被称Just-In-Time代码生成或JIT代码生成)

说明

Java虚拟机结构(第3部分)栈帧、对象、浮点、异常

2.6 栈帧

栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。

栈帧随着方法调用而创建,随着方法结束而销毁

栈帧的存储空间分配在Java虚拟机栈之中,每一个栈帧都有自己的局部变量表(Local Variables)、操作数栈(Operand Stack)和指向当前方法所属的类的运行时常量池的引用。

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

2.6.1 局部变量表

存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用。

局部变量使用索引来进行定位访问,第一个局部变量的索引值为零,局部变量的索引值是从零至小于局部变量表最大容量的所有整数。

当一个实例方法被调用的时候,第0个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即Java语言中的“this”关键字)

2.6.2 操作数栈

每一个栈帧内部都包含一个称为操作数栈的后进先出(Last-In-First-Out,LIFO)栈

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

2.6.3 动态链接

每一个栈帧内部都包含一个指向运行时常量池的引用来支持当前方法的代码实现动态链接

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

2.6.4 方法正常调用完成

方法正常完成发生在一个方法执行过程中遇到了方法返回的字节码指令的时候,使用哪种返回指令取决于方法返回值的数据类型(如果有返回值的话)。

当前栈帧承担着回复调用者状态的责任,其状态包括调用者的局部变量表、操作数栈和被正确增加过来表示执行了该方法调用指令的程序计数器等

2.6.5 方法异常调用完成

某些指令导致了Java虚拟机抛出异常,并且虚拟机抛出的异常在该方法中没有办法处理,或者在执行过程中遇到了athrow字节码指令显式地抛出异常,并且在该方法内部没有把异常捕获住

2.7 对象的表示

Java虚拟机规范不强制规定对象的内部结构应当如何表示。

2.8 浮点算法

Java虚拟机采纳了IEEE 754浮点算法规范中的部分子集。

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

主要差别有:

  • 在Java虚拟机中的浮点操作在遇到非法操作不会抛出exception、trap或者其他IEEE 754异常情况中定义的信号。

  • 在Java虚拟机中不支持IEEE 754中的信号浮点比较

  • 在Java虚拟机中,舍入操作永远使用IEEE 754规范中定义的向最接近数舍入模式,无法精确表示的结果将会舍入为最接近的可表示值来保证此值的最低有效位为零;浮点数转化为整型数是使用向零舍入

  • 在Java虚拟机中不支持IEEE 754的单精度扩展和双精度扩展格式

2.8.2 浮点模式

每一个方法都有一项属性称为浮点模式(Floating-Point Mode),取值有两种,FP-strict模式/非FP-strict模式。

2.8.3 数值集合转换

在一些特定场景下,支持扩展指数集合的Java虚拟机实现数值在标准浮点数集合与扩展指数集合之间的映射关系是允许和必要的,这种映射操作就称为数值集合转换。数值集合转换并非数据类型转换,而是在同一种数据类型之中不同数值集合的映射操作。

2.9 初始化方法的特殊命名

构造函数,是以一个名为的特殊实例初始化方法的形式出现的

实例初始化方法只能在实例的初始化期间,通过Java虚拟机的invokespecial指令来调用,只有在实例正在构造的时候,实例初始化方法才可以被调用访问

一个类或者接口最多可以包含不超过一个类或接口的初始化方法,类或者接口就是通过这个方法完成初始化的,一个不包含参数的静态方法,名为

类或接口的初始化方法由Java虚拟机自身隐式调用,没有任何虚拟机字节码指令可以调用这个方法,只有在类的初始化阶段中会被虚拟机自身调用。

2.10 异常

Java虚拟机里面的异常使用Throwable或其子类的实例来表示,抛异常的本质实际上是程序控制权的一种即时的、非局部(Nonlocal)的转换——从异常抛出的地方转换至处理异常的地方。

绝大多数的异常的产生都是由于当前线程执行的某个操作所导致的,这种可以称为是同步的异常。与之相对的,异步异常是指在程序的其他任意地方进行的动作而导致的异常。Java虚拟机中异常的出现总是由下面三种原因之一导致的:

  • 虚拟机同步检测到程序发生了非正常的执行情况,这时异常将会紧接着在发生非正常执行情况的字节码指令之后抛出

  • athrow字节码指令被执行。

  • 异步异常:

    • 调用了Thread或者ThreadGroup的stop方法。

    • Java虚拟机实现的内部程序错误。

《Java虚拟机规范》允许在异步异常被抛出时额外执行一小段有限的代码,允许代码优化器在不违反Java语言语义的前提下检测并把这些异常在可处理它们的地方抛出①。

抛出异常的动作在Java虚拟机之中是一种被精确定义的程序控制权转移过程。

搜索异常处理器时的搜索顺序是很关键的,在Class文件里面,每个方法的异常处理器都存储在一个表中(§4.7.3)。在运行时,当有异常出现之后,Java虚拟机就按照Class文件中的异常处理器表描述异常处理器的先后顺序,从前至后进行搜索。

说明

Java虚拟机结构(第2部分)运行时数据区

2.5 运行时数据区

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

2.5.1 PC寄存器

每一条Java虚拟机线程都有自己的PC(Program Counter)寄存器。

PC寄存器的容量至少应当能保存一个returnAddress类型的数据或者一个与平台相关的本地指针的值。

  • 如果有个非native方法正被线程执行,寄存器就保存Java虚拟机正在执行的字节码指令的地址。
  • 如果该方法是native的,那PC寄存器的值是undefined

2.5.2 Java虚拟机栈

每一条Java虚拟机线程都有自己私有的Java虚拟机栈(Java Virtual Machine Stack),这个栈与线程同时创建,用于存储栈帧。

Java虚拟机栈的作用:用于存储局部变量与一些过程结果的地方。另外,它在方法调用和返回中也扮演了很重要的角色。

因为除了栈帧的出栈和入栈之外,Java虚拟机栈不会再受其他因素的影响,所以栈帧可以在堆中分配

Java虚拟机栈所使用的内存不需要保证是连续的。

2.5.3 Java堆

在Java虚拟机中,堆(Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。

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

2.5.4 方法区

方法区(Method Area)是可供各条线程共享的运行时内存区域

方法区,存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法

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

2.5.5 运行时常量池

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

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

2.5.6 本地方法栈

Java虚拟机实现可能会使用到传统的栈来支持native方法的执行,这个栈就是本地方法栈(Native Method Stack)。

如果支持本地方法栈,那这个栈一般会在线程创建的时候按线程分配。

说明

Java虚拟机结构(第1部分)数据类型

本规范描述的是一种抽象化的虚拟机的行为,而不是任何一种被广泛使用的虚拟机实现。

2.1 Class 文件格式

编译后被Java虚拟机所执行的代码使用了一种平台中立(不依赖于特定硬件及操作系统的)的二进制格式来表示,并且经常(但并非绝对)以文件的形式存储,因此这种格式被称为Class文件格式。

2.2 数据类型

Java虚拟机可以操作的数据类型:原始类型(Primitive Types)和引用类型(Reference Types)。与之对应,也存在有原始值(Primitive Values)和引用值(Reference Values)两种类型的数值可用于变量赋值、参数传递、方法返回和运算操作

Java虚拟机是直接支持对象的(实例)

使用reference类型来表示对某个对象的引用

2.3 原始类型与值

Java虚拟机所支持的原始数据类型包括了数值类型布尔类型returnAddress类型三类。其中数值类型又分为整型类型和浮点类型两种

整数类型包括:

  • byte类型:值为8位有符号二进制补码整数,默认值为零。
  • short类型:值为16位有符号二进制补码整数,默认值为零。
  • int类型:值为32位有符号二进制补码整数,默认值为零。
  • long类型:值为64位有符号二进制补码整数,默认值为零。
  • char类型:值为使用16位无符号整数表示的、指向基本多文本平面(Basic Multilingual Plane,BMP)的Unicode值,以UTF-16编码,默认值为Unicode的null值('\u0000')。

浮点类型包括:

  • float类型:值为单精度浮点数集合中的元素,或者(如果虚拟机支持的话)是单精度扩展指数(Float-Extended-Exponent)集合中的元素。默认值为正数零。
  • double类型:取值范围是双精度浮点数集合中的元素,或者(如果虚拟机支持的话)是双精度扩展指数(Double-Extended-Exponent)集合中的元素。默认值为正数零。

布尔类型包括:

  • boolean类型:取值范围为布尔值true和false,默认值为false。

returnAddress类型包括:

  • returnAddress类型:表示一条字节码指令的操作码(Opcode)。在所有的虚拟机支持的原始类型之中,只有returnAddress类型是不能直接Java语言的数据类型对应起来的。

2.3.1 整型类型与整型值

2.3.2 浮点类型、取值集合及浮点值

IEEE 754标准的内容不仅包括了正负带符号可数的数值(Sign-Magnitude Numbers),还包括了正负零正负无穷大和一个特殊的“非数字”标识(Not-a-Number,下文用NaN表示)。NaN值用于表示某些无效的运算操作,例如除数为零等情况。

顺序将会是:负无穷,可数负数、正负零、可数正数、正无穷。

浮点数中,正数零和负数零是相等的,但是它们有一些操作会有区别。例如1.0除以0.0会产生正无穷大的结果,而1.0除以-0.0则会产生负无穷大的结果。

NaN是无序的,对它进行任何的数值比较和等值测试都会返回false的比较结果。任何数字与NaN进行非等值比较都会返回true。

2.3.3 returnAddress类型和值

2.3.4 boolean类型

在Java语言之中涉及到boolean类型值的运算,在编译之后都使用Java虚拟机中的int数据类型来代替。

Java虚拟机直接支持boolean类型的数组,虚拟机的newarray指令可以创建这种数组,数组类型的访问与修改共用byte类型数组的baload和bastore指令。

2.4 引用类型与值

三种引用类型:类类型(Class Types)、数组类型(Array Types)和接口类型(Interface Types)。

数组类型还包含一个单一维度(即长度不由其类型决定)的组件类型(Component Type),一个数组的组件类型也可以是数组。多层最里面的类型是数组类型的元素类型(Element Type),必须上面三个类型之一。

引用类型的默认值就是null。

说明