《Java虚拟机规范》阅读(一):简介和Java虚拟机结构
前言
说到学习jvm,其实我本人并不认为学习完以后会对目前工作有什么太大的帮助。但是为了深入了解java体系,使自己在看待问题上能够看到更本质的部分还是必须要学习的。同时对于自己的技术也是一个深入。
闲话少说,这个系列主要是阅读Java虚拟机规范的一些知识点的梳理和心得,后续可能还包括经典的《深入Java虚拟机》一书的系列。
首先提供一下《Java虚拟机规范(Java SE 7)》PDF中文版的下载,这个版本要感谢ITEYE上的几位牛人进行的翻译,不然只能去啃英文版的了。
下载:
引用下书里的概括:
Java SE 7版的《Java虚拟机规范》整合了自1999年《Java虚拟机规范(第二版)》发布以来Java世界所出现的技术变化。另外,还修正了第二版中许多的错误,以及对目前主流Java虚拟机实现来说已经过时的内容。最后还处理率了一些Java虚拟机和Java语言概念的不清晰之处。
关于虚拟机规范:
本规范描述的是一种抽象化的虚拟机的行为,而不是任何一种(译者注:包括Oracle公司自己的HotSpot和JRockit虚拟机)被广泛使用的虚拟机实现。
所有在虚拟机规范之中没有明确描述的实现细节,都不应成为虚拟机设计者发挥创造性的牵绊,设计者可以完全自主决定所有规范中不曾描述的虚拟机内部细节,例如:运行时数据区的内存如何布局、选用哪种垃圾收集的算法、是否要对虚拟机字节码指令进行一些内部优化操作(如使用即时编译器把字节码编译为机器码)。
Java体系和一些基本概念
先来看一下java平台的结构图:
JVM与JRE、JDK关系?
JVM:Java Virtual Machine(Java虚拟机),负责执行符合规范的Class文件
JRE: Java Runtime Environment (java运行环境),包含JVM和类库
JDK: Java Development Kit(java开发工具包),包含JRE和开发工具包,例如javac、javah
JVM所处的位置:
我们通常工作中所接触的基本是Java库和应用以及Java核心类库,知晓如何使用就可以了,但是归根结底代码都是要编译成class文件由Java虚拟机执行的,所产生的结果或者现象都可以通过Java虚拟机的运行机制来解释。一些相同的代码会由于虚拟机的实现不同而产生不同结果。
开始前
这个系列我们紧扣主题,针对Java虚拟机规范,至于Java虚拟机的一些特性,例如:平台无关性,安全性等等我不会在这个系列中讨论。另外可能也不会系统的说明Jvm的运行机制,毕竟只是规范,主要描述的都是Jvm中各个体系的规则和限制
Class文件格式
编译后被Java虚拟机所执行的代码使用了一种平台中立(不依赖于特定硬件及操作系统的)的二进制格式来表示,并且经常(但并非绝对)以文件的形式存储,因此这种格式被称为Class文件格式。Class文件格式中精确地定义了类与接口的表示形式,包括在平台相关的目标文件格式中一些细节上的惯例,例如字节序(Byte Ordering)等。
正如概念所说,Java为了能够实现平台无关性,制定了一套自己的二进制格式,并经常以文件的方式存储,称为Class文件。这样在不同平台上,只要都安装了Java虚拟机,那么都可以运行相同的Class文件。具体的Class文件格式将在后面的章节详细描述。
数据类型
与Java程序语言中的数据类型相似,Java虚拟机可以操作的数据类型可分为两类:原始类型(Primitive Types,也经常翻译为原生类型或者基本类型)和引用类型(Reference Types)。与之对应,也存在有原始值(Primitive Values)和引用值(Reference Values)两种类型的数值可用于变量赋值、参数传递、方法返回和运算操作。
基本类型和引用类型的具体情况见下图:
Java虚拟机希望更多的类型检查放在编译期就完成,在运行期不需要进行这些操作。其中基本类型达到了这样的要求,在运行期间不需要对其进行类型检查,也不用和引用类型区分开。这是通过虚拟机的字节码指令完成的,不同类型的字节码指令中都包含了相应的数据类型。关于字节码指令稍后介绍。
整形类型和整型值的取值范围如下:
对于byte类型,取值范围是从-128至127(-27至27-1),包括-128和127。
对于short类型,取值范围是从−32768至32767(-215至215-1),包括−32768和32767。
对于int类型,取值范围是从−2147483648至2147483647(-231至231-1),包括−2147483648和2147483647。
对于long类型,取值范围是从−9223372036854775808至9223372036854775807(-263至263-1),包括−9223372036854775808和9223372036854775807。
对于char类型,取值范围是从0至65535,包括0和65535
浮点类型、取值集合和浮点值:
浮点类型包含32位单精度的float类型和64位双精度的double类型两种,浮点数除了包括正负带符号可数的数值,还包括了正负零、正负无穷大和一个特殊的“非数字”标识(Not-a-Number,下文用NaN表示)。NaN值用于表示某些无效的运算操作,例如除数为零等情况。
所有Java虚拟机的实现都必须支持两种标准的浮点数值集合:单精度浮点数集合和双精度浮点数集合。
returnAddress类型和值:
returnAddress类型会被Java虚拟机的jsr、ret和jsr_w指令所使用。returnAddress类型的值指向一条虚拟机指令的操作码。与前面介绍的那些数值类的原始类型不同,returnAddress类型在Java语言之中并不存在相应的类型,也无法在程序运行期间更改returnAddress类型的值。
boolean类型:
Java虚拟机不提供操作boolean类型的字节码指令,程序在编译后boolean类型都转化成了int操作。但是Java虚拟机支持boolean类型的数组的访问和修改,共用byte类型数组的字节码指令。
运行时数据区
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
下图是Java虚拟机的逻辑构成:
可以看出Java虚拟机的运行时数据区包括了:方法区、Java堆、Java虚拟机栈、PC寄存器、本地方法栈。
PC寄存器:
每个Java虚拟机线程都有自己的PC寄存器。在某个线程被新建时,会获得一个PC寄存器。线程当前执行的方法称为当前方法,PC寄存器用来存放当前方法中当前执行的字节码指令的地址,如果当前方法是本地方法(Native),那么寄存器存放undefined。
寄存器的大小至少应该能够存放一个returnAddress类型的数据或者与平台相关的本地指针的值。
Java虚拟机栈:
每个Java虚拟机线程都有自己的Java虚拟机栈。Java虚拟机栈用来存放栈帧,而栈帧主要包括了:局部变量表、操作数栈、动态链接。
Java虚拟机使用局部变量表来完成方法调用时的参数传递。局部变量表的长度在编译期已经决定了并存储于类和接口的二进制表示中,一个局部变量可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,两个局部变量可以保存一个类型为long和double的数据。
Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。
每个栈帧中都包含一个指向运行时常量区的引用支持当前方法的动态链接。在Class文件中,方法调用和访问成员变量都是通过符号引用来表示的,动态链接的作用就是将符号引用转化为实际方法的直接引用或者访问变量的运行是内存位置的正确偏移量。
总的来说,Java虚拟机栈是用来存放局部变量和过程结果的地方。
Java虚拟机栈可能发生如下异常情况:
如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量时,Java虚拟机将会抛出一个StackOverflowError异常。
如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
Java堆:
Java堆在虚拟机启动的时候被创建,Java堆主要用来为类实例对象和数组分配内存。Java虚拟机规范并没有规定对象在堆中的形式。
Java堆可能发生如下异常情况:
如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError异常。
方法区:
方法区在虚拟机启动的时候被创建,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。
方法区可能发生如下异常情况:
如果方法区的内存空间不能满足内存分配请求,那Java虚拟机将抛出一个OutOfMemoryError异常
运行时常量池:
运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池在方法区中。
在创建类和接口的运行时常量池时,可能会发生如下异常情况:
当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最大值,那Java虚拟机将会抛出一个OutOfMemoryError异常。
本地方法栈:
本地方法栈用于支持native方法的运行。
字节码指令集
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成。
对于大部分为与数据类型相关的字节码指令,他们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。
加载和存储指令:
将一个局部变量加载到操作栈的指令包括有: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
运算指令:
加法指令: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
类型转换指令:
Java虚拟机对于宽化类型转换直接支持,并不需要指令执行,包括:
int类型到long、float或者double类型
long类型到float、double类型
float类型到double类型
窄化类型转换指令包括有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。但是窄化类型转换很可能会造成精度丢失。
对象创建与操作指令:
创建类实例的指令: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
操作数栈管理指令:
Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2和swap;
控制转移指令:
条件分支: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
方法调用和返回指令:
invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(§2.9)、私有方法和父类方法。
invokestatic指令用于调用类方法(static方法)。而方法返回指令则是根据返回值的类型区分的,包括有ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用
抛出异常指令:
athrow
类库
值得一提的是Java类库中一些类的实现如果没有Java虚拟机的支持是无法实现的,包括:
反射,譬如在java.lang.reflect包中的各个类和java.lang.Class类
类和接口的加载和创建,最显而易见的例子就是java.lang.ClassLoader类
类和接口的链接和初始化,上一点的例子也适用于这点
安全,譬如在java.security包中的各个类和java.lang.SecurityManager等其他类
多线程,譬如java.lang.Thread类
弱引用,譬如在java.lang.ref包中的各个类
也就是说针对不同的Java虚拟机的实现,以上的类库很可能因为Java虚拟机支持的不同而带来差异。
第一章的内容大概就到这了,有一些很晦涩的知识点没有提,可能主观意识上还是觉得没太大作用吧,感兴趣的可以自己看书。
如有异议或者错误的地方请补充和指正!其实这些东西自己也看了2遍了,但是这样一整理写出来感觉的确是印象深刻了不少,博客生活刚开始啊!