java虚拟机规范阅读简介
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java虚拟机规范阅读简介相关的知识,希望对你有一定的参考价值。
java虚拟机规范在日常工作中可以说根本用不到,但作为一个完美主义者,感觉如果进入java这个行业,对它的方方面面不去掌握的话,未免有些遗憾,我没有那些改写java语言大师们的天赋,我只能站在他们的肩膀,来掌握他们创造的技术。
闲话不多说,我会认真读java虚拟机并写下自己理解或者有用的东西,看到的、写下的、说出的才是学到的,读者可以去http://down.51cto.com/自行下载java虚拟机规范。
关于java虚拟机规范:
本规范描述的是一种抽象化的虚拟机的行为,而不是任何一种(译者注:包括Oracle公司自己的HotSpot和JRockit虚拟机)被广泛使用的虚拟机实现。 如果只是“正确的”实现一台java虚拟机,其实并不是所有人想的那么困难和高深-只需要读取Class文件每一条字节码指令,并能正确的执行这些指令的操作即可。所有在虚拟机规范之中没有明确描述的实现细节,都不应成为虚拟机设计者发挥创造性的牵绊,设计者可以完全自主决定所有规范中不曾描述的虚拟机内部细节,例如:运行时数据区的内存如何布局、选用哪种垃圾收集的算法、是否要对虚拟机字节码指令进行一些内部优化操作(如使用即时编译器把字节码编译为机器码) |
我们可以很简单的实现虚拟机,也可以设计出性能优秀的虚拟机,在遵循虚拟机规范的前提下,设计性能优秀的虚拟机往往会得到开发者青睐。
CLass文件格式:
编译后被Java虚拟机所执行的代码使用了一种平台中立(不依赖于特定硬件及操作系统的)的二进制格式来表示,并且经常(但并非绝对)以文件的形式存储,因此这种格式被称为Class文件格式。Class文件格式中精确地定义了类与接口的表示形式,包括在平台相关的目标文件格式中一些细节上的惯例,例如字节序(Byte Ordering)等。 |
JAVA实现平台无关性的基础是虚拟机和字节码存储格式,使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等其他语言的编译器一样可以把程序代码编译成Class文件,虚拟机并不关心Class的来源是什么语言,只要它符合Class文件应有的结构就可以在Java虚拟机中运行。
数据类型:
与Java程序语言中的数据类型相似,Java虚拟机可以操作的数据类型可分为两类:原始类型(Primitive Types,也经常翻译为原生类型或者基本类型)和引用类型(Reference Types)。与之对应,也存在有原始值(Primitive Values)和引用值(Reference Values)两种类型的数值可用于变量赋值、参数传递、方法返回和运算操作。 Java虚拟机希望更多的类型检查放在编译期就完成,换句话说,编译器应当在编译期间最大努力完成可能的类型检查,使得虚拟机在运行期不需要进行这些操作。其中原始类型不需要通过特殊标记或者额外的标记手段来在运行期确定他们的实际数据类型,也不用和引用类型区分开。虚拟机的字节码指令本身就可以确定他们的指令操作数的类型是什么,利用这种特性可以直接确定操作数的数据类型。举个列子:iadd、fadd、ladd和dadd这几条指令都表示两个数值相加,并返回相加的结果,单每条指令都有专属的数据类型,他们依次对应:int、float、long、double,相关指令后面我们介绍。 Java虚拟机是直接支持对象的,这里的对象可以指动态分配的某个类实例,也可以指某个数组的实例。虚拟机用Reference类型来表示某个对象的引用,Reference类型的值读者可以想象成一个类似于指向对象的指针,每一个对象都存在多个指向它的引用,对象的操作、传递和检查都通过引用它的Reference类型数据进行操作。 |
关于数据类型分类:
整形类型和整形取值:
byte类型:值为8位有符号二进制补码整数,默认值为0,取值范围是从-128至127(-27至27-1),包括-128和127。
short类型:值为16位有符号二进制补码整数,默认值为0,取值范围是从32768至32767(-215至215-1),包括32768和32767。
int类型:值为32位有符号二进制补码整数,默认值为0,取值范围是从2147483648至2147483647(-231至231-1),包括2147483648和2147483647。
long类型:值为64位有符号二进制补码整数,默认值为0,取值范围是从9223372036854775808至9223372036854775807(-263至263-1),包括9223372036854775808和9223372036854775807。
char类型:值为使用16位无符号整数表示,指向基本多文本平面的Unicode值,以UTF-16编码,默认值为Unicode的null值,取值范围是从0至65535,包括0和65535
浮点类型类型和取值:
float类型:值为单精度浮点数集合中的元素,或者是(如果虚拟机支持)单精度扩展指数集合中的元素,默认值为正整数0.
double类型:值为双精度浮点数集合中的元素,或者是(如果虚拟机支持)双精度扩展指数集合中的元素,默认值为正整数0.
java中float、double都遵循IEEE754标准,不清楚的可以看http://zangyanan.blog.51cto.com/11610700/1854836了解一下
布尔类型:
boolean类型:取值范围为true和false,Java虚拟机不提供操作boolean类型的字节码指令,程序在编译后boolean类型都转化成了int操作。但是Java虚拟机支持boolean类型的数组的访问和修改,共用byte类型数组的字节码指令。
returnAddress类型:
returnAddress类型:returnAddress类型会被Java虚拟机的jsr、ret和jsr_w指令所使用。returnAddress类型的值指向一条虚拟机指令的操作码。与前面介绍的那些数值类的原始类型不同,returnAddress类型在Java语言之中并不存在相应的类型,也无法在程序运行期间更改returnAddress类型的值。
运行时数据区:
Java虚拟机在执行Java的过程中会把管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,而有的区域则依赖线程的启动和结束而创建和销毁。如图:
程序计数器
程序计数器是一块较小的区域,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的模型里,字节码指示器就是通过改变程序计数器的值来指定下一条需要执行的指令。分支,循环等基础功能就是依赖程序计数器来完成的。 由于java虚拟机的多线程是通过轮流切换并分配处理器执行时间来完成,一个处理器同一时间只会执行一条线程中的指令。为了线程恢复后能够恢复正确的执行位置,每条线程都需要一个独立的程序计数器,以确保线程之间互不影响。所以程序计数器是“线程私有”的内存。 如果虚拟机正在执行的是一个Java方法,则计数器指定的是字节码指令对应的地址,如果正在执行的是一个本地方法,则计数器指定问空undefined。程序计数器区域是Java虚拟机中唯一没有定义OutOfMemory异常的区域。 |
Java虚拟机栈
每一条Java虚拟机线程都有自己私有的Java虚拟机栈,这个栈与线程同时创建,用于存储栈帧。Java虚拟机栈的作用与传统语言(例如C语言)中的栈非常类似,就是用于存储局部变量与一些过程结果的地方。另外,它在方法调用和返回中也扮演了很重要的角色。因为除了栈帧的出栈和入栈之外,Java虚拟机栈不会再受其他因素的影响,所以栈帧可以在堆中分配,Java虚拟机栈所使用的内存不需要保证是连续的。 Java虚拟机规范允许Java虚拟机栈被实现成固定大小的或者是根据计算动态扩展和收缩的。如果采用固定大小的Java虚拟机栈设计,那每一条线程的Java虚拟机栈容量应当在线程创建的时候独立地选定。Java虚拟机实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段,对于可以动态扩展和收缩Java虚拟机栈来说,则应当提供调节其最大、最小容量的手段。 Java虚拟机栈可能出现两种类型的异常: 1. 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。 2.虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。 栈帧: 栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。 栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。栈帧的存储空间分配在Java虚拟机栈之中,每一个栈帧都有自己的局部变量表(Local Variables)、操作数栈(Operand Stack)和指向当前方法所属的类的运行时常量池的引用。 局部变量表和操作数栈的容量是在编译期确定,并通过方法的Code属性保存及提供给栈帧使用。因此,栈帧容量的大小仅仅取决于Java虚拟机的实现和方法调用时可被分配的内存。 在一条线程之中,只有目前正在执行的那个方法的栈帧是活动的。这个栈帧就被称为是当前栈帧(Current Frame),这个栈帧对应的方法就被称为是当前方法(Current Method),定义这个方法的类就称作当前类(Current Class)。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的对局部变量表和操作数栈进行的操作。 如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。当一个新的方法被调用,一个新的栈帧也会随之而创建,并且随着程序控制权移交到新的方法而成为新的当前栈帧。当方法返回的之际,当前栈帧会传回此方法的执行结果给前一个栈帧,在方法返回之后,当前栈帧就随之被丢弃,前一个栈帧就重新成为当前栈帧了。 请读者特别注意,栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一条线程的栈帧。 局部变量表: 每个栈帧内部都包含一组称为局部变量表(Local Variables)的变量列表。栈帧中局部变量表的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用。 一个局部变量可以保存一个类型为boolean、byte、char、short、float、reference和returnAddress的数据,两个局部变量可以保存一个类型为long和double的数据。 局部变量使用索引来进行定位访问,第一个局部变量的索引值为零,局部变量的索引值是从零至小于局部变量表最大容量的所有整数。 long和double类型的数据占用两个连续的局部变量,这两种类型的数据值采用两个局部变量之中较小的索引值来定位。例如我们讲一个double类型的值存储在索引值为n的局部变量中,实际上的意思是索引值为n和n+1的两个局部变量都用来存储这个值。索引值为n+1的局部变量是无法直接读取的,但是可能会被写入,不过如果进行了这种操作,就将会导致局部变量n的内容失效掉。 上文中提及的局部变量n的n值并不要求一定是偶数,Java虚拟机也不要求double和long类型数据采用64位对其的方式存放在连续的局部变量中。虚拟机实现者可以自由地选择适当的方式,通过两个局部变量来存储一个double或long类型的值。 Java虚拟机使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将会传递至从0开始的连续的局部变量表位置上。特别地,当一个实例方法被调用的时候,第0个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即Java语言中的“this”关键字)。后续的其他参数将会传递至从1开始的连续的局部变量表位置上。 操作数栈: 每一个栈帧内部都包含一个称为操作数栈(Operand Stack)的后进先出(Last-In-First-Out,LIFO)栈。栈帧中操作数栈的长度由编译期决定,并且存储于类和接口的二进制表示之中,既通过方法的Code属性保存及提供给栈帧使用。 在上下文明确,不会产生误解的前提下,我们经常把“当前栈帧的操作数栈”直接简称为“操作数栈”。 操作数栈所属的栈帧在刚刚被创建的时候,操作数栈是空的。Java虚拟机提供一些字节码指令来从局部变量表或者对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用的时候,操作数栈也用来准备调用方法的参数以及接收方法返回结果。 举个例子,iadd字节码指令的作用是将两个int类型的数值相加,它要求在执行的之前操作数栈的栈顶已经存在两个由前面其他指令放入的int型数值。在iadd指令执行时,2个int值从操作栈中出栈,相加求和,然后将求和结果重新入栈。在操作数栈中,一项运算常由多个子运算(Subcomputations)嵌套进行,一个子运算过程的结果可以被其他外围运算所使用。 每一个操作数栈的成员(Entry)可以保存一个Java虚拟机中定义的任意数据类型的值,包括long和double类型。 在操作数栈中的数据必须被正确地操作,这里正确操作是指对操作数栈的操作必须与操作数栈栈顶的数据类型相匹配,例如不可以入栈两个int类型的数据,然后当作long类型去操作他们,或者入栈两个float类型的数据,然后使用iadd指令去对它们进行求和。有一小部分Java虚拟机指令(例如dup和swap指令)可以不关注操作数的具体数据类型,把所有在运行时数据区中的数据当作裸类型(Raw Type)数据来操作,这些指令不可以用来修改数据,也不可以拆散那些原本不可拆分的数据,这些操作的正确性将会通过Class文件的校验过程来强制保障。 在任意时刻,操作数栈都会有一个确定的栈深度,一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度。 动态链接: 每一个栈帧内部都包含一个指向运行时常量池的引用来支持当前方法的代码实现动态链接(Dynamic Linking)。在Class文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用(Symbolic Reference)来表示的,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用。类加载的过程中将要解析掉尚未被解析的符号引用,并且将变量访问转化为访问这些变量的存储结构所在的运行时内存位置的正确偏移量。 由于动态链接的存在,通过晚期绑定(Late Binding)使用的其他类的方法和变量在发生变化时,将不会对调用它们的方法构成影响。 方法调用结果: ⑴方法正常调用完成 方法正常调用完成是指在方法的执行过程中,没有任何异常被抛出——包括直接从Java虚拟机之中抛出的异常以及在执行时通过throw语句显式抛出的异常。如果当前方法调用正常完成的话,它很可能会返回一个值给调用它的方法,方法正常完成发生在一个方法执行过程中遇到了方法返回的字节码指令的时候,使用哪种返回指令取决于方法返回值的数据类型(如果有返回值的话)。 在这种场景下,当前栈帧承担着回复调用者状态的责任,其状态包括调用者的局部变量表、操作数栈和被正确增加过来表示执行了该方法调用指令的程序计数器等。使得调用者的代码能在被调用的方法返回并且返回值被推入调用者栈帧的操作数栈后继续正常地执行。 ⑵方法异常调用完成 方法异常调用完成是指在方法的执行过程中,某些指令导致了Java虚拟机抛出异常,并且虚拟机抛出的异常在该方法中没有办法处理,或者在执行过程中遇到了athrow字节码指令显式地抛出异常,并且在该方法内部没有把异常捕获住。如果方法异常调用完成,那一定不会有方法返回值返回给它的调用者。 |
Java堆
在Java虚拟机中,堆(Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。 Java堆在虚拟机启动的时候就被创建,它存储了被自动内存管理系统(Automatic Storage Management System,也即是常说的“Garbage Collector(垃圾收集器)”)所管理的各种对象,这些受管理的对象无需,也无法显式地被销毁。本规范中所描述的Java虚拟机并未假设采用什么具体的技术去实现自动内存管理系统。虚拟机实现者可以根据系统的实际需要来选择自动内存管理技术。Java堆的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。Java堆所使用的内存不需要保证是连续的。 Java虚拟机实现应当提供给程序员或者最终用户调节Java堆初始容量的手段,对于可以动态扩展和收缩Java堆来说,则应当提供调节其最大、最小容量的手段。Java堆可能发生如下异常情况:如果实际所需的堆超过了自动内存管理系统能提供的最大容量,那Java虚拟机将会抛出一个OutOfMemoryError异常。 |
拓展-----堆与栈的关系
堆和栈是程序运行的关键,很有必要把他们的关系说清楚。
方法区:
在Java虚拟机中,方法区(Method Area)是可供各条线程共享的运行时内存区域。方法区与传统语言中的编译代码储存区(Storage Area Of Compiled Code)或者操作系统进程的正文段(Text Segment)的作用非常类似,它存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。 方法区在虚拟机启动的时候被创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现可以选择在这个区域不实现垃圾收集。这个版本的Java虚拟机规范也不限定实现方法区的内存位 置和编译代码的管理策略。方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。方法区在实际内存空间中可以是不连续的。 Java虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段。 方法区可能发生如下异常情况:
|
运行时常量池:
运行时常量池(Runtime Constant Pool)是每一个类或接口的常量池(Constant_Pool)的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池扮演了类似传统语言中符号表(Symbol Table)的角色,不过它存储数据范围比通常意义上的符号表要更为广泛。 每一个运行时常量池都分配在Java虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。 在创建类和接口的运行时常量池时,可能会发生如下异常情况:
|
本地方法栈:
Java虚拟机实现可能会使用到传统的栈(通常称之为“C Stacks”)来支持native方法(指使用Java以外的其他语言编写的方法)的执行,这个栈就是本地方法栈(Native Method Stack)。当Java虚拟机使用其他语言(例如C语言)来实现指令集解释器时,也会使用到本地方法栈。如果Java虚拟机不支持natvie方法,并且自己也不依赖传统栈的话,可以无需支持本地方法栈,如果支持本地方法栈,那这个栈一般会在线程创建的时候按线程分配。
Java虚拟机规范允许本地方法栈被实现成固定大小的或者是根据计算动态扩展和收缩的。如果采用固定大小的本地方法栈,那每一条线程的本地方法栈容量应当在栈创建的时候独立地选定。一般情况下,Java虚拟机实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段,对于长度可动态变化的本地方法栈来说,则应当提供调节其最大、最小容量的手段。 本地方法栈可能发生如下异常情况:
|
本文出自 “进击的程序猿” 博客,请务必保留此出处http://zangyanan.blog.51cto.com/11610700/1855494
以上是关于java虚拟机规范阅读简介的主要内容,如果未能解决你的问题,请参考以下文章
Java虚拟机规范阅读IEEE754简介以及Java虚拟机中的浮点算法