第12章 Java虚拟机.docx
《第12章 Java虚拟机.docx》由会员分享,可在线阅读,更多相关《第12章 Java虚拟机.docx(29页珍藏版)》请在冰豆网上搜索。
第12章Java虚拟机
第12章Java虚拟机
【本章专家知识导学】
Java虚拟机是Java语言平的基石。
所有的Java程序都执行于Java虚拟机之上。
Java虚拟机实质是一个虚构的计算机,它与普通计算机一样,拥有自己的虚拟处理器,堆、栈、寄存器等存储机制及相应的指令系统。
本章深入的介绍了Java虚拟机构的结构和对Java程序的执行方式。
学习完本章之后,读者应能够加深对Java语言及JavaSE体系结构的理解,提高编写高质量的Java程序能力。
12.1Java虚拟机概述
正如前所述,Java不仅仅是一个编程语言,它是一个软件开放运行平台,它主要由四个部分组成:
Java语言、开发工具和APIs、JRE、Java虚拟机(JavaVirtualMachine,JVM),如图1-4。
它们之间的关系如图12-1所示。
图12-1Java平台各部分之间关系
开发人员通过Java语言编写Java源程序,然后通过Java编译器将其编译为Java字节码文件。
在运行时Java虚拟机将Java字节码文件装载,并解释编译为本地机器所能识别的指令代码,在本地机器上执行。
从图12-1可知,Java虚拟机处在核心位置,是Java程序平台无关性的关键。
Java虚拟机实质上是一个虚构的计算机,通过在实际的计算机上仿真模拟各种计算机来实现。
它与实际的计算机一样,具有完整的硬件架构,拥有自己的虚拟处理器,堆、栈、寄存器等存储机制及相应的指令系统。
Java平台利用Java虚拟机解释执行Java程序,实现Java程序与操作系统的分离,从而实现Java的平台无关性,摆脱具体机器的束缚。
12.2Java虚拟机的生命周期
Java虚拟机负责装载、解析执行Java程序。
当Java虚拟机启动执行一个Java程序时,Java虚拟机同时创建一个Java虚拟机实例(java进程);当程序执行完毕退出时,Java虚拟机进程同时关闭, Java虚拟机实例结束生命周期。
Java虚拟机在启动时执行的Java程序,必须包含publicstaticvoidmain(Stringargs[])方法。
main方法是Java程序的入口方法,它将驱动程序相关的所有Java字节码文件。
同时main方法也是Java程序初时线程的起点,任何其它的线程都由这个初时线程启动。
Java虚拟机内部有两种线程:
守护线程和非守护线程。
守护线程通常由虚拟机自己使用,比如执行垃圾收集任务的线程。
非守护线程由Java程序创建,只要非守护线程在运行,Java虚拟机实例也将一直在运行,直到该程序中所有的非守护线程都终止时,Java虚拟机实例才自动退出。
12.3Java虚拟机数据类型
Java虚拟机是通过数据类型来执行计算的,数据类型及其运算都按Java虚拟机规范严格定义。
对应于Java语言,Java虚拟机数据类型可分为两种:
基本类型和引用类型(对应抽象数据类型),如图12-12。
图12-2Java虚拟机数据类型
基本数据类型包括数据类型、boolean类型和returnAddress类型。
Java语言中的所有基本类型同样也是Java虚拟机中的基本类型。
但boolean类型有点特别。
尽管Java语言定义了boolean类型,但是Java虚拟机没有用于boolean类型操作的指令。
虽有创建boolean类型数组的指令,但也不支持对boolean数组元素的访问。
在Java虚拟机中boolean型用int数据类型表示,其中false用整数零表示,true用非零整数表示。
相应地,对boolean型的操作转换为对int类型的操作,boolean数组也用int数组指令访问和修改。
retuanAddress类型在Java语言中没有对应的类型,供予Java虚拟机的jsr,ret和jsr_w指令使用。
returnAddress类型的值是Java虚拟机指令的操作码指针,与数值基本类型不同。
引用类型有三种:
类类型、接口类型和数组类型。
它们的值是对动态创建的类实例,数组或接口实现类实例的引用。
引用值也可以是特殊的null引用,它不引用对象。
null引用初始没有运行期类型,但是可以转换成任何类型。
Java虚拟机规范定义了每一种数据类型的取值范围,如表12-1。
但是规范没有定义它们的位宽,存储这些类型的值所需的占位宽度,由具体的虚拟机实现的设计者决定。
表12-1Java虚拟机数据类型的取值范围
类型
范围
Byte
8比特,带符号,二进制补码
Short
16比特,带符号,二进制补码
Int
32比特,带符号,二进制补码
Long
64比特,带符号,二进制补码
Char
16比特,不带符号,Unicode字符
Float
32比特,IEEE754标准单精度浮点数
Double
64比特,IEEE754标准单精度浮点数
ReturnAddress
同一方法中某操作码的地址
Reference
堆中对某对象的引用,或者是null
Java虚拟机中,最基本的数据单元就是字(word),它的大小是由每个虚拟机实现的设计者来决定的。
字长必须足够大,一个字长至少应足以存储byte、short、int、char、float、returnAddress或者reference类型的值,而两个字长应足以存储long或者double类型的值。
因此,虚拟机实现的设计者至少得选择32位作为字长,或者选择更为高效的字长大小。
通常字长根据底层主机平台的指针长度来选择。
Java虚拟机规范中,关于运行时数据区的大部分内容,都是基于“字”这个抽象概念的。
比如,关于栈帧的两个部分——局部变量和操作数栈——都是按照“字”来定义的。
这个内容区域能够容纳任何虚拟机数据类型的值,当把这些值放到局部变量或者操作数栈中时,它将占用一个或两个字单元。
12.4Java虚拟机指令系统
Java虚拟机与普通的计算机一样具有一套完整的指令系统,每条指令执行一种基本的CPU运算。
每一个指令一般由一个字节的操作码(Opcode)和零个或多个操作数(Operands)组成。
操作码指明做什么,操作数提供操作所需的参数或数据。
操作数的个数由操作码决定,当然也有些指令没有操作数,仅由一个单字节的操作码构成。
如果一个操作数的大小大于一个字节,则按照big-endian顺序存储——高位字节在前。
例如,一个16位的无符号操作数被作为2个无符号字节byte1和byte2存储,这样它的值是:
(byte1<<8)|byte2
每一条Java虚拟机指令均有一个助记符,Java虚拟机的指令助记符集合就犹如Java虚拟机的汇编语言。
Java类中的方法编译成字节码流后转换为一组Java虚拟机指令序列。
【例12.1】
1.packagechapter12;
2.publicclassOperandStack
3.{
4.publicstaticvoidmain(String[]args)
5.{
6.intrs=add(100,98);
7.}
8.publicstaticintadd(intop1,intop2)
9.{
10.intrs=op1+op2;
11.returnrs;
12.}
13.}
将【12.1】编译成字节码文件后,通过“javap-coperandstack”命令反汇编,可得到如下OperandStack类的Java虚拟机指令助记符程序:
1.Compiledfrom"OperandStack.java"
2.publicclasschapter12.OperandStackextendsjava.lang.Object
3.{
4.publicchapter12.OperandStack();
5.Code:
6.0:
aload_0
7.1:
invokespecial#8;//Methodjava/lang/Object."":
()V
8.4:
return
9.
10.publicstaticvoidmain(java.lang.String[]);
11.Code:
12.0:
bipush100
13.2:
bipush98
14.4:
invokestatic#16;//Methodadd:
(II)I
15.7:
istore_1
16.15:
return
17.
18.publicstaticintadd(int,int);
19.Code:
20.0:
iload_0
21.1:
iload_1
22.2:
iadd
23.3:
istore_2
24.4:
iload_2
25.5:
ireturn
26.}
其中4到8行是默认构造函数的助记符程序,10到16行是main方法的助记符程序,18到25行是add方法的助记符程序。
Java虚拟机指令集中的绝大多数指令根据操作数的类型进行编码,例如,iload指令把一个int型局部变量的内容装载到操作数栈,fload指令对一个float型局部变量做同样的事,这两个指令可以具有相同的实现,但具有不同的操作码。
对大多数有类型的指令,指令类型由操作码助记符前第一个字母显式地表示,如i指对int型操作,l指对long型操作,s指对short型操作,b指对byte型操作,c指对char型操作,f指对float型操作,d指对double型操作,a指对reference型操作。
当然有些指令不能操作在有类型地操作数上,例如goto指令,无条件控制转移指令等。
Java虚拟机指令可分为10类,装载和存储指令、运算指令、类型转换指令、对象创建和操纵指令、控制转移指令、方法调用返回指令、抛出和异常处理指令、finally实现指令、同步指令。
12.4.1装载和存储指令
装载和存储指令在Java虚拟机的局部变量和操作数栈之间传递值:
1)把一个局部变量装载到操作数栈的指令有:
iload,iload_,lload,lload_,fload,fload_,dload,dload_,aload,aload_。
2)把一个值从操作数栈存储到局部变量的指令有:
istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstore_,astore,astore_。
3)把一个常数装载到操作数栈的指令有:
bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_m1,iconst_,lconst_,fconst_,dconst_。
4)扩展本地变量引用范围的专门指令:
wide。
上述尾字母带尖括号中的指令助记符,如iload_,表示一个指令族。
如:
iload_指令的成员有:
iload_0,iload_1,iload_2和iload_3。
指令族是一般指令附加一个操作数的规范指令。
对规范的指令,操作数是隐式的,不需要存储或获得。
iload_0表示与iload操作数为0时一样,其它语义是相同的。
尖括号中的字母规范代表该指令族的隐式操作数的类型:
代表自然数,代表int,代表long,代表float,代表double。
在许多情况下,类型int的形式用于类型byte,char和short的值上的操作。
12.4.2运算指令
运算指令对操作数栈上的一个或两个值进行计算后把结果压回到操作数栈。
每一个运算指令均被规范为Java虚拟机数值类型。
在整型数(int)、长整型数(long)和浮点数(float、double)上的运算操作由Java虚拟机指令直接支持。
对于byte,short和char数据类型的运算,Java虚拟机没有直接支持的指令,对它们的操作需其转化为int型处理。
运算指令如下:
1)加:
iadd,ladd,fadd,dadd;
2)减:
isub,lsub,fsub,dsub;
3)乘:
imul,lmul,fmul,dmul;
4)除:
idiv,ldiv,fdiv,ddiv;
5)取模:
irem,lrem,frem,drem;
6)取负:
ineg,lneg,fneg,dneg;
7)移位:
ishl,ishr,iushr,lshl,lshr,lushr;
8)按位或:
ior,lor;
9)按位与:
iand,land;
10)按位异或:
ixor,lxor;
11)局部变量递增:
iinc;
12)比较:
dcmpg,dcmpl,fcmpg,fcmpl,lcmp。
整数和浮点数指令在上溢、下溢和被零除的行为上也不同。
Java虚拟机在作整数运算时不提示上溢或下溢,仅当除数为零时,才抛出ArithmeticException异常。
对浮点数运算,Java虚拟机按照IEEE754规范进行。
特别地,Java虚拟机要求完整地支持IEEE754非规格化(denormalized)浮点数和逐渐下溢(gradualanderflow)。
这使得Java虚拟机易于检验特定数值算法的合适属性。
Java虚拟机要求执行浮点运算的结果舍入到指定精度;不精确的(inexact)结果必须舍入到最接近无穷精确结果可表示的值;如果两个最接近的可表示值同样接近,则选择带有最低位零的结果。
这是IEEE754标准的缺省舍入模式,称为最接近舍入(round-to-nearest)。
Java虚拟机把浮点值转化为整数时,采用向零舍入(round-towards-zero)。
这将可能使操作结果的小数部分有效数字位被截断。
向零舍入选择最接近该类型且不大于无穷精确结果的数值作为它得结果。
Java虚拟机在作浮点运算时不产生异常,上溢时将产生一个有符号无穷数值,下溢时将产生一个有符号零,没有数学定义结果的操作产生NaN,所有以NaN作为操作数的运算,运算结果均为NaN。
12.4.3类型转换指令
Java虚拟机允许数值类型之间的转换,这种转换可以是在用户代码中显式的转换,也可由Java虚拟机自行隐式的转换。
Java虚拟机直接支持以下隐式转换:
1)int到long、float或double;
2)long到float、double
3)float到double。
这些隐式转换,对应的指令分别是:
i2l、i2f、i2d,l2f、l2d和f2d。
隐式转换也称为放宽型转换。
放宽型转换不会丢失数值的任何信息,数值将被精确的保留下来。
需显示转换的一般为缩窄型转换。
Java虚拟机直接支持以下显式转换:
1)int到byte,short或者char
2)long到int;
3)float到int或者long;
4)double到int、long或者float。
对应的缩窄数值转换指令分别是:
i2b,i2c,i2s,l2i,f2l,d2i,d2l,d2f。
缩窄数值转换可能使结果值的符号不同,或者数量的数量级不同,或者两者都不同,从而它们可能丢弃精度。
12.4.4对象创建和操纵
尽管类实例和数组都是对象,但是Java虚拟机用不同的指令集创建和操纵类实例与数组。
1、类实例创建与操纵
1)类实例创建:
new;
2)类变量(类static变量)访问:
getstatic,putstatic;
3)实例变量访问:
getfield,putfield;
4)检查类实例类型的操作:
instanceof。
2、数组创建与操纵
1)数组创建:
newarray,anewarray,mulitiancewarray;
2)把数组装载到操作数栈:
baload,caload,saload,iaload,laload,faload,daload,aaload;
3)把数值从操作栈存储在数组元素中:
bastore,castore,sastore,iastore,lastore,fastore,dastore,aastore;
4)获取数组长度:
arraylength;
5)检查数组类型:
checkcast。
12.4.5操作数栈管理指令
为了直接操纵操作数栈Java虚拟机提供以下指令:
pop,pop2,dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2,swap。
12.4.6控制转移指令
控制转移指令,有条件或无条件地使Java虚拟机转移到指定指令处继续执行。
控制转移指令主要有:
1)有条件转移指令:
ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull,if_icmpeq,if_icmpne,if_icmplt,if_icmpgt,if_icmple,if_icmpge,if_acmpeq,if_acmpne,lcmp,fcmpl,fcmpg,dcmpl,dcmpg;
2)复合条件转移指令:
tableswitch,lookupswitch;
3)无条件转移指令:
goto,got_w,jsr,jsr_w,ret。
12.4.7方法调用返回指令
1、Java虚拟机提供了4条方法调用指令:
1)调用对象的实例方法,调度对象的(虚)类型:
invokevirtual,这是Java中的正常方法调度;
2)调用接口的实现方法,在特定的运行期对象中搜索接口的实现方法,以寻找合适的方法:
invokeinterface;
3)调用需要特殊处理的实例方法,即实例初时化方法,private方法或者超类方法:
invokespecial;
4)调用类方法(static方法):
invokestatic。
2、方法返回指令,由方法返回类型区分:
1)ireturn:
用于返回类型为byte,char,short,int的方法;
2)lreturn:
用于返回类型为long的方法;
3)freturn:
用于返回类型为float的方法;
4)dreture:
用于返回类型为double的方法;
5)areturn:
用于返回类型为reference的方法。
6)return:
用于返回类型为void的方法。
12.4.8抛出和异常处理指令
异常用athrow指令有计划地抛出。
如果它们检测到一个不正常的条件,各种Java虚拟机指令也可能抛出异常。
12.4.9finally实现指令
finally关键字通过使用jsr,jsr_w和ret指令实现。
12.4.10同步指令
Java虚拟机以不同的方式用单个的机制(监视器)支持方法和块层次的同步。
同步方法被作为方法调用和返回的一部分处理。
但是代码块的同步在指令集中有显式地支持:
monitorenter,monitorexit。
12.5Java虚拟机体系结构
Java虚拟机体现结构主要由类装载子系统,运行时数据区,执行引擎,本地方法接口等部分组成,其中运行时数据区又分为:
方法区、堆、Java栈、PC寄存器、本地方法栈,如图12-3。
图12-3Java虚拟机体系结构
12.5.1类装载器子系统
Java虚拟机执行Java程序时,首先需要通过类装载器子系统装载Java字节码。
这一过程被分为三个阶段:
加载、连接、初时化,其中连接又分为三个步骤:
验证、准备和解析,如图12-4。
图12-4字节码装载过程
1、加载
加载的主要工作是寻找并导入指定的Java字节码文件。
加载过程是由类加载器完成的。
在加载过程中加载器会对所有加载的Java字节码文件进行校验检查。
当字节码文件有错,或类文件格式、版本不被支持,或类加载器找不到类或接口的定义时,加载器都会抛出异常。
类加载器主要有三种类型引导类加载器(Bootstrapclassloader)、扩展类加载器(Extensionclassloader)和应用类加载器(Systemclassloader)。
引导类加载器(bootstrapclassloader),也称为原始加载器,它负责加载Java核心类,如将java.lang.Object和其他一些运行时代码(运行时的类在Java_home\JRE\lib\rt.jar包文件中)先加载进内存中。
在Sun的JVM中,执行java命令时使用-Xbootclasspath选项或使用-D选项指定sun.boot.class.path系统属性值可以指定附加的类。
扩展类加载器,负责加载Java扩展类库。
Java扩展类库提供比Java运行时代码更多的特性。
程序员也可将扩展类库保存在由java.ext.dirs属性指定的路径中,扩展类加载器将自动加载该路径下的.jar文件。
java.ext.dirs属性值默认为Java_home\jre\lib\ext目录,java.ext.dirs属性是一个系统属性,所有的系统属性可通过System.getProperties()方法获得。
应用类加载器(Systemclassloader),也称为系统加载器,它负责加载-classpath参数或系统属性java.class.path指定路径下的.jar包和类文件。
出于安全考虑,类加载器的顺序是:
先是bootstrapclassloader,然后是extensionclassloader,最后才是systemclassloader。
类加载器加载类时用的是全盘负责机制、委托机制和cache机制。
所谓全盘负责,即是当一个类加载器加载一个Class的时候,这个Class所依赖的和引用的所有相关Class也由这个类加载器负责载入,除非是显式的使用另外一个类加载器载入。
所谓委托机制,则是先委托parent(父)类加载器寻找加载,只有在parent找不到的时候才从自己的类路径中去寻找。
此外类加载器还采用了cache机制,只有当cache中找不到这个class时,才从文件中加载该Class,并存入cache,这就是为什么修改Class文件后,必须重新启动Java虚拟机才能生效的原因。
类加载器加载class的流程图可表述如下,如图12-5。
图12-5类加载流程
1.首先检测此Class是否载入过(即在cache中是否有此Class),如果有则转到第8步,否则,到第2步;
2.检测parentclassloader存在否(没有parent,那parent一定是bootstrapclassloader了),如果存在则到第3步,否则到第4步;
3.请求parentclassloader载入,如果成功转到第8步,否则转到第5步;
4.请求jvm从bootstrapclassloader中载入,如果成功转到第8步,否则转到第5步;
5.寻找Class文件(从与此classloader相关的类路径中寻找),如果找到则转第6步,否则转到第7步.;
6.从文件中载入Class,并转到第8步;
7.抛出ClassNotFoundException;
8.返回Class。
2、连接
1)验证
连接过程的第一步就是验证需要连接的类文件。
由于Java虚拟机与Java编译器是完全分离的,Java虚拟机在解释执行时无法保证所装载的类文件的正确性,甚至无法保证文件是否由Java编译器所生成。
因此,Java虚拟机必须在类装载后,验证类文件是否满足Java语言规范。
验证过程可分为4个步骤:
第一步,由Java虚拟机