看了很多java源码,现在也开始看一些jvm的东西,字节码之类的,于是干脆直接看JVM规范了。
从JVM的指令开始:
1 基本数据类型:char,byte,short,int,boolean,float,double,long,reference,returnAddress
-
对于 byte类型,取值范围是从-128至127(-27至27-1),包括-128和127。
-
对于 short类型,取值范围是从−32768至32767(-215至215-1),包括−32768和 32767。
对于 char 类型,取值范围是从0至65535,包括0和65535。
Java 虚拟机直接支持boolean类型的数组,虚拟机的 newarray指令可以创建这种数组。
boolean 的数组类型的访问与修改共用 byte类型数组的baload和bastore指令1。
returnAddress 类型:表示一条字节码指令的操作码(Opcode)。
2 引用
Java虚拟机中有三种引用类型:类类型(Class Types)、数组类型(Array Types)和接口类型(Interface Types)。
引用类型的值分别由类实例、数组实例和实现了某个接口的类实例或数组实例动态创建。
数组类型还包含一个单一维度(即长度不由其类型决定)的组件类型(ComponentType),一个数组的组件类型也可以是数组。
数组类型的元素类型(Element Type)。数组的元素类型必须是原始类型、类类型或者接口类型之中的一种。
引用类型的默认值就是 null。
3 PC计数器
如果这个方法不是 native的,那PC寄存器就保存Java虚拟机正在执行的字节码指令的地址,如果该方法是native的,那PC寄存器的值是undefined。PC寄存器的容量至少应当能保存一个returnAddress类型的数据或者一个与平台相关的本地指针的值。
4 线程栈
Java 虚拟机栈可能发生如下异常情况:
如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一
个 StackOverflowError异常。
如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的
内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚
拟机将会抛出一个 OutOfMemoryError异常。
5 Java堆:
Java 虚拟机实现应当提供给程序员或者最终用户调节Java堆初始容量的手段,对于可以动态扩展和收缩 Java堆来说,则应当提供调节其最大、最小容量的手段。
Java 堆可能发生如下异常情况:
如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个
OutOfMemoryError异常。
6 方法区:
它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法 。
Java 虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段。
方法区可能发生如下异常情况:
如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个
OutOfMemoryError异常。
7 运行时常量池
它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池扮演了类似传统语言中符号表(SymbolTable)的角色
每一个运行时常量池都分配在 Java虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。
在创建类和接口的运行时常量池时,可能会发生如下异常情况:
当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最
大值,那 Java虚拟机将会抛出一个OutOfMemoryError异常。
8 本地方法栈
native方法
本地方法栈可能发生如下异常情况:
如果线程请求分配的栈容量超过本地方法栈允许的最大容量时,Java虚拟机将会抛出一个
StackOverflowError异常。
如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存
去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那 Java 虚拟机将会抛出一个OutOfMemoryError异常。
9 栈帧
栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。
栈帧的存储空间分配在 Java虚拟机栈之中,每一个栈帧都有自己的局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用。
栈帧中局部变量表的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用。
一个局部变量可以保存一个类型为 boolean、byte、char、short、float、reference和returnAddress的数据,两个局部变量可以保存一个类型为long和double的数据。 一个double类型的值存储在索引值为n的局部变量中,实际上的意思是索引值为n和n+1的两个局部变量都用来存储这个值。
------------------------
2.1 局部变量表
Java 虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将会传递至从0 开始的连续的局部变量表位置上。特别地,当一个实例方法被调用的时候,第0 个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即Java 语言中的“this”关键字)。后续的其他参数将会传递至从1 开始的连续的局部变量表位置上。
2.2 操作栈
每一个栈帧内部都包含一个称为操作数栈(Operand Stack)的后进先出(Last-In-First-Out,LIFO)栈。栈帧中操作数栈的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code 属性保存及提供给栈帧使用。
Java 虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。
举个例子,iadd、ladd、fadd和dadd这几条指令的操作含义都是将两个数值相加,并返个相加的结果,但是每一条指令都有自己的专属操作数类型,此处按顺序分别为:int、long、float和double。 iadd字节码指令的作用是将两个 int类型的数值相加,它要求在执行的之前操作数栈的栈顶已经存在两个由前面其他指令放入的 int 型数值。在iadd 指令执行时,2个 int值从操作栈中出栈,相加求和,然后将求和结果重新入栈。
在任意时刻,操作数栈都会有一个确定的栈深度,一个 long或者 double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度。
2.3 动态连接
每一个栈帧内部都包含一个指向运行时常量池的引用来支持当前方法的代码实现动态链接(Dynamic Linking)。在Class 文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用(Symbolic Reference)来表示的,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用。类加载的过程中将要解析掉尚未被解析的符号引用,并且将变量访问转化为访问这些变量的存储结构所在的运行时内存位置的正确偏移量。
由于动态链接的存在,通过晚期绑定(Late Binding)使用的其他类的方法和变量在发生变化时,将不会对调用它们的方法构成影响。
2.4 初始化
Java 语言中的构造函数在《Java语言规范 (第三版)》(下文简称JLS3)是以一个名为<init>的特殊实例初始化方法的形式出现的,<init>这个方法名称是由编译器命名的,因为它并非一个合法的Java 方法名字,不可能通过程序编码的方式实现。实例初始化方法只能在实例的初始化期间,通过 Java 虚拟机的invokespecial指令来调用,只有在实例正在构造的时候,实例初始化方法才可以被调用访问(JLS3)。
一个类或者接口最多可以包含不超过一个类或接口的初始化方法,类或者接口就是通过这个方法完成初始化的。这个方法是一个不包含参数的静态方法,名为<clinit>
2.5 异常
绝大多数的异常的产生都是由于当前线程执行的某个操作所导致的,这种可以称为是同步的异常。与之相对的,异步异常是指在程序的其他任意地方进行的动作而导致的异常。Java虚拟机中异常的出现总是由下面三种原因之一导致的:
1. 虚拟机同步检测到程序发生了非正常的执行情况,这时异常将会紧接着在发生非正常执行情况的字节码指令之后抛出。例如:
-
字节码指令所蕴含的操作违反了 Java语言的语义,如访问一个超出数组边界范围的元素。
-
类在加载或者链接时出现错误。
-
使用某些资源的时候产生资源限制,例如使用了太多的内存。
2. athrow 字节码指令被执行。
3. 由于以下原因,导致了异步异常的出现:
调用了Thread 或者 ThreadGroup的 stop方法。
Java虚拟机实现的内部程序错误。
当某条线程调用了 stop方法时,将会影响到其他的线程,或者在线程组中的所有线程。这时候其他线程中出现的异常就是异步异常,因为这些异常可能出现在程序执行过程的任何位置。虚拟机的内部异常也被认为是一种异步异常
搜索异常处理器时的搜索顺序是很关键的,在 Class文件里面,每个方法的异常处理器都存储在一个表中 。在运行时,当有异常出现之后,Java虚拟机就按照 Class文件中的异常处理器表描述异常处理器的先后顺序,从前至后进行搜索。
3 字节码指令
Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
do {
自动计算PC寄存器以及从PC寄存器的位置取出操作码;
if (存在操作数)取出操作数;执行操作码所定义的操作
} while (处理下一次循环);
操作数的数量以及长度取决于操作码,如果一个操作数的长度超过了一个字节,那它将会以Big-Endian顺序存储——即高位在前的字节序。举个例子,如果要将一个 16位长度的无符号整数使用两个无符号字节存储起来(将它们命名为 byte1和 byte2),那它们的值应该是这样的:
(byte1 << 8) | byte2
字节码指令流应当都是单字节对齐的,只有“tableswitch”和“lookupswitch”两条指令例外,由于它们的操作数比较特殊,都是以4 字节为界划分开的,所以这两条指令那个也需要预留出相应的空位来实现对齐。
3.1 加载指令
iload 指令用于从局部变量表中加载int 型的数据到操作数栈中,而 fload指令加载的则是float类型的数据。
大部分的指令都没有支持整数类型 byte、char和 short,甚至没有任何指令支持boolean 类型。
编译器会在编译期或运行期会将 byte和 short类型的数据带符号扩展(Sign-Extend)为相应的int 类型数据,将 boolean和 char类型数据零位扩展(Zero-Extend)为相应的int 类型数据。 在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的 int类型的字节码指令来处理。因此,大多数对于boolean、byte、short和 char类型数据的操作,实际上都是使用相应的对 int类型作为运算类型(Computational Type)
例如 load指令有操作 int类型的 iload,但是没有操作byte 类型的同类指令。
其中:i代表对 int类型的数据操作,l代表 long,s代表 short,b代表 byte,c代表 char,f代表 float,d代表 double,a代表 reference。
arraylength 指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。还有另外一些指令,例如无条件跳转指令goto 则是与数据类型无关的。
3.1.1 加载和存储指令
加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输:
将一个局部变量加载到操作栈的指令包括有:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
将一个数值从操作数栈存储到局部变量表的指令包括有:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
将一个常量加载到操作数栈的指令包括有:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
扩充局部变量表的访问索引的指令:wide
3.1.2 运算指令
加法指令: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异常
3.1.3 类型转换指令
int类型到 long、float或者 double类型
long类型到 float、double类型
float类型到 double类型
窄化类型转换(Narrowing Numeric Conversions)指令包括有:i2b、i2c、i2s、l2i、
f2i、f2l、d2i、d2l和 d2f。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级,转换过程很可能会导致数值丢失精度。
3.1.4 对象创建
-
创建类实例的指令: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
-
接操作操作数栈的指令,包括:pop、pop2、dup、dup2、
dup_x1、dup2_x1、dup_x2、dup2_x2和 swap。
3.1.6 条件转移
-
控制转移指令包括有:
-
条件分支: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
-
-
boolean 类型、byte类型、char类型和 short类型的条件分支比较操作,都使用 int类型的比较指令来完成,而对于long 类型、float类型和 double类型的条件分支比较操作,则会先执行相应类型的比较运算指令,运算指令会返回一个整形值到操作数栈中,随后再执行int类型的条件分支比较操作来完成整个分支跳转。由于各种类型的比较最终都会转化为int 类型的比较操作,基于int 类型比较的这种重要性,Java虚拟机提供了非常丰富的 int类型的条件分支指令。
3.1.7 方法调用与返回
以下四条指令用于方法调用:
invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。
invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(§2.9)、私有方法和父类方法。
invokestatic 指令用于调用类方法(static方法)。
而方法返回指令则是根据返回值的类型区分的,包括有 ireturn(当返回值是boolean、byte、char、short和 int类型时使用)、lreturn、freturn、dreturn和 areturn,另外还有一条return 指令供声明为 void的方法、实例初始化方法、类和接口的类初始化方法使用。
3.1.8 异常
athrow
3.1.10 同步
Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作 中
虚拟机可以从方法常量池中的方法表结构(method_info Structure )中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放管程。
同步一段指令集序列通常是由 Java 语言中的 synchronized 块来表示的,Java 虚拟机的指令集中有 monitorenter 和 monitorexit 两条指令来支持 synchronized 关键字的语义,正确实现 synchronized 关键字需要编译器与 Java 虚拟机两者协作支持
ava 虚拟机特殊支持的类库包括有:
反射,譬如在 java.lang.reflect 包中的各个类和 java.lang.Class 类
-
类和接口的加载和创建,最显而易见的例子就是 java.lang.ClassLoader 类
-
类和接口的链接和初始化,上一点的例子也适用于这点
-
安全,譬如在 java.security 包中的各个类和 java.lang.SecurityManager等其他类
-
多线程,譬如 java.lang.Thread 类
-
弱引用,譬如在 java.lang.ref 包中的各个类