第五章 Java虚拟机
1、Java虚拟机是什么?
1)抽象规范
2)一个具体的实现
3)一个运行中的虚拟机实例
2、Java虚拟机的生命周期
一个运行时的Java虚拟机实例的天职就是:负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。每个Java程序都运行于它自己的Java虚拟机实例中。
在Java虚拟机内部有两种线程:守护线程和非守护线程。守护线程是由虚拟机自己使用的,比如执行垃圾收集任务的线程(但是,Java程序也可以把它创建的任何线程标记为守护线程)。只要还有任何非守护线程在运行,那么这个Java程序也在继续运行(虚拟机仍然存活)。当该程序中所有的非守护线程都终止时,虚拟机实例将自动退出。
3、Java虚拟机的体系结构
每个Java虚拟机都有一个类装载子系统,它根据给定的权限定名来装入类型。
每个Java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令。
当Java虚拟机运行一个程序时,它需要内存来存储许多东西,例如字节码,从已装载的class文件中得到其他信息,程序创建的对象,传递给方法的参数,返回值,局部变量,以及运算的中间结果等等,Java把这些东西都组织到几个“运行时数据区”中,以便于管理。
方法区:某些运行时数据区是由程序中所有线程共享的,还有一些则只能由一个线程拥有,每个Java虚拟机实例都有一个方法区以及一个堆,它们是由该虚拟机实例中所有的线程共享的。当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,然后它把这些类型放到方法区中,当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。
当每一个新的线程被创建时,它都将得到它自己的PC寄存器(程序计数器)以及一个Java栈,如果线程正在执行一个Java方法(非本地方法),那么PC寄存器的值将总是指示下一条被执行的指令,而它的Java栈则总是存储该线程中Java方法调用的状态,包括它的局部变量,被调用时传进来的参数,它的返回值,以及运算的中间结果等等。而本地方法的调用则是以某种依赖于具体实现的方式存储在本地方法栈中,也可能是在寄存器或者其他某些与特定实现相关的内存区中。
1)数据类型:Java虚拟机通过数据类型来执行计算,数据类型及其运算都是由Java虚拟机规范严格定义的,数据类型可以分为两种:基本类型和引用类型。基本类型的变量持有原始值,而引用类型的变量只有引用值。
Java语言中所有的基本类型同样也都是Java虚拟机中的基本类型,但是Boolean有点特别,虽然Java虚拟机也把boolean看做基本类型,但是指令集对boolean只有有限的支持,当编译器把Java编译为字节码时,它会用int或byte来表示boolean.在Java虚拟机中,false是由整数零来表示的,所有非零整数都表示true,涉及boolean值的操作会用int,另外,boolean数组是当做byte数组来访问的。Java虚拟机中还有一个只在内部使用的基本类型:returnAddress,但是程序员不能使用这个类型。
Java虚拟机的引用类型被称为“引用”(reference),有三种引用类型:类类型,接口类型以及数组类型,它们的值都是对动态创建对象的引用。类类型的值是对类实例的引用,数组类型的值是对数组对象的引用,在Java虚拟机中,数组是个真正的对象,而接口类型的值,则是对实现了该接口的某个类实例的引用。还有一类特殊的引用值是Null,表示该引用变量没有引用任何对象。
2)字长:Java虚拟机中,最基本的数据单元就是字(Word)。Java程序无法侦测到底层虚拟机的字长大小,同样,虚拟机的字长大小也不会影响程序的行为,它仅仅是虚拟机实现的内部属性。
3)类装载子系统:在Java虚拟机中,负责查找并装载类型的那部分被称为类装载器子系统。Java虚拟机有两种类装载器,启动类转载器和用户自定义类装载器,前者是Java虚拟机实现的一部分,后者则是Java程序的一部分。
装载、连接以及初始化类装载器子系统除了要定位和导入二进制class文件外,还必须负责验证被导入类的正确性,为类变量分配并初始化内存,以及帮助解析符号引用。以上动作严格按照以下顺序进行:
装载,查找并装载类型的二进制数据。
连接,执行验证,准备,以及解析。验证确保被导入类型的正确性;准备,为类变量分配内存,并将其初始化为默认值;把类型中的符号引用转换为直接引用。
初始化,把类变量初始化为正确初始值。
4) 堆
Java程序在运行时创建的所有类实例或者数组都放在同一个堆中,而一个Java虚拟机实例中只存放一个堆空间,因此所有线程都将共享这个堆。又由于一个Java程序独占一个Java虚拟机实例,因而每个Java程序都有自己的堆空间—它们不会彼此干扰。但是同一个Java程序的多个线程却共享着同一个堆空间,在这种情况下,就得考虑多线程访问对象(堆数据)的同步问题。
垃圾收集器的主要工作就是自动回收不再被运行的程序引用的对象所占用的内存,此外,它也可能去移动那些还在使用的对象,以此减少堆碎片。
Java虚拟机规范并没有规定具体的实现必须为Java程序准备多少内存,也没有说它必须怎么管理自己的堆空间,它仅仅告诉实现的设计者:Java程序需要从堆中为对象分配空间,并且程序本身不会主动释放它。
对象的内部表示:Java虚拟机规范并没有规定Java对象在堆中是如何表示的,对象的内部表示也影响着整个堆以及垃圾收集器的设计,它是由虚拟机的实现者决定。Java对象中包含的基本数据由它所属的类及其所有超类声明的实例变量组成,只要有一个对象引用,虚拟机就必须能够快速定位对象实例的数据,另外它也必须能够通过该对象引用访问相应的类数据(存储于方法区的类型信息),因此在对象中通常会有一个指向方法区的指针。
一种可能的堆空间设计是:把堆分为两部分,一个是句柄池,一个是对象池,一个对象引用就是一个指向句柄池的本地指针。句柄池的每个条目有两部分,一个指向对象实例变量的指针,一个指向方法区类型数据的指针。这种设计的好处是有利于堆碎片的整理,当移动对象池中的对象时,句柄部分只需要更改一下指针对象的新地址就可以了。缺点是每次访问对象的实例变量都要经过两次指针传递。
另一种设计方式是使对象指针直接指向一组数据,而该数据包括对象实例数据以及指向方法区中类数据的指针。这样设计的优缺点正好与前面的方法相反,它只需要一个指针就可以访问对象的实例数据,但是移动对象就变得更加复杂。
不管虚拟机的实现使用什么样的对象表示法,很可能每个对象都有一个方法表,因为方法表加快了调用实例方法时的效率,从而对Java虚拟机实现的整体性能起着非常重要的正面作用。
如下一种方案:每个对象的数据都包含一个指向特殊数据结构的指针,这个数据结构位于方法区,包括两部分:一个指向方法区对应数据的指针和次对象的方法表。
方法表是个指针数组,其中每一项都是一个指向“实例方法数据”的指针,实例方法可以被那类的对象调用,方法表指向的实例方法数据包括以下信息:此方法的操作数栈和局部变量区的大小,此方法的字节码,异常表。
堆上的对象数据中还有一个逻辑部分,那就是对象锁,这是一个互斥对象。虚拟机中的每个对象都有一个对象锁,它被用于协调多个线程访问同一个对象时的同步。在任何时刻,只能有一个线程拥有这个对象锁,因此只有这个线程才能访问该对象的数据,此时其他希望访问这个对象的线程只能等待,直到拥有对象锁的线程释放锁。
很多对象在其整个生命周期内部都没有被任何线程加锁,在线程实际请求某个对象锁之前,实现对象锁所需要的数据是不必要的。
除了实现锁所需要的数据外,每个Java对象逻辑上还与实现等待集合(wait set)的数据相关联。锁是用来实现多个线程对共享数据的互斥访问的,而等待集合是用来让多个线程为完成一个共同目标而协调工作的。等待集合由等待方法和通知方法联合使用。
最后一种数据类型,可以作为堆中某个对象映像的一部分,是与垃圾收集器有关的数据。垃圾收集器必须跟踪程序引用的每个对象,这个任务不可避免地要附加一些数据给这些对象,数据的类型要视垃圾收集使用的算法而定。除了标记对象的引用情况外,垃圾收集器还要区分对象是否调用了终结方法。
数组的内部表示,在Java中,数组时真正的对象,和其他对象一样,数组总是存储在堆中,同样和普通对象一样,实现的设计者将决定数组在堆中的表示形式。
数组和其他对象一样,数组也拥有一个与它们的类相关联的Class的实例,所有具有相同维度和类型的数组都是同一个类的实例,而不管数组的长度(多维数组中每一维的长度)是多少。
数组类的名称由两部分组成:每一维用一个方括号,“[”表示,用字符或字符串表示元素类型。
在堆中的每个数组对象还必须保存的数据是数组的长度、数组数据以及某些指向数组的类数据的引用。虚拟机必须能够通过一个数组对象的引用得到此数组的长度,通过索引访问其元素(其间检查数组边界是否越界),调用所有数组的直接超类Object声明的方法等等。
6)程序计数器
对于一个运行中的Java程序而言,其中的每一个线程都有它自己的PC(程序计数器)寄存器,它是在该线程启动时创建的。PC寄存器的大小事一个字长,因此它既能够持有一个本地指针,也能够持有一个returnAddress
7) Java栈
每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈,以帧为单位保存线程的运行状态。虚拟机只会对直接对Java栈执行两种操作:以帧为单位的压栈或出栈。
某个线程正在执行的方法被称为该线程的当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。在线程执行一个方法时,它会跟踪当前类和当前常量池,当虚拟机遇到栈内操作指令时,它对当前帧内数据执行操作。
Java方法可以以两种方式完成。一种通过return返回的,称为正常返回,一种是通过抛出异常而异常中止的。不管以哪种方法返回,虚拟机都会将当前帧弹出Java栈然后释放掉,这样上一个方法的帧就成为当前帧了。
Java栈上的所有数据都是此线程私有的,任何线程都不能访问另一个线程的栈数据,因此我们不需要考虑多线程情况下栈数据的访问同步问题。当一个线程调用一个方法时,方法的局部变量保存在调用线程Java栈的帧中,只有一个线程能总是访问那些局部变量,即调用方法的线程。
8) 栈帧
栈帧由三部分组成:局部变量区,操作数栈和帧数据区。局部变量区和操作数栈的大小要视对应的方法而定,它们是按字长计算的。当虚拟机调用一个Java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数栈的大小,并据此分配栈帧内存,然后压入Java栈中。
局部变量区,Java栈帧的局部变量区被组织为一个字长为单位,从0开始计数的数组。字节码指令通过从0开始的索引来使用其中的数据。类型为int、float、reference和returnAddress的值在数组中只占据一项,而类型byte、short和char的值在存入数组前都将被传为int值,因而同样占据一项,但是类型为long和double的值在项目中却占据连续的两项(在访问局部变量中的long和double的值的时候,指令只需要指出连续两项中第一项的索引值)。
局部变量区包含对应方法的参数和局部变量,编译器首先按声明的顺序把这些参数放入局部变量数组。
参数this对于任何一个实例方法都是隐含加入的,它用来表示调用该方法的对象本身。而静态类方法中没有this变量,因为它是一个类方法,类方法只与类相关,没有与具体的对象相关系。不能通过类方法访问类实例的变量,因为在方法调用时没有关联到一个具体实例。
在Java中,所以的对象都是按引用传递,并且存储在堆中,永远不会在局部变量区或操作数栈中发现对象的拷贝,只会有对象引用。
操作数栈:和局部变量区一样,操作数栈也是被组织成一个以字长为单位的数组,但是和前面不同的是,它不是通过索引来访问,而是通过标准的栈操作(压栈和出栈)来访问的。不同于程序计数器,Java虚拟机没有寄存器,程序计数器也无法被程序指令直接访问,Java虚拟机的指令时从操作数栈中而不是从寄存器中取得操作数的,因此它的运行方式是基于栈的而不是基于寄存器的。虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。
帧数据区:除了局部变量区和操作数栈外,Java栈帧还需要一些数据来支持常量池的解析,正常方法返回以及异常派发机制,这些信息都保存在Java栈帧的帧数据区中。
Java虚拟机中的大多数指令都涉及到常量池入口,有些指令仅仅是从常量池中取出数据然后压入Java栈,还有些指令使用常量池的数据来指示要实例化的类或者数组、要访问的字段或者要调用的方法,还有些指令需要常量池中的数据才能确定某个对象是否属于某个类或实现了某个接口。
每当虚拟机要执行某个需要用到常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问它。
帧数据区还帮助虚拟机处理Java方法的正常结束或者异常中止,为了处理Java方法执行期间的异常退出情况,帧数据区还必须保存一个对此方法异常表的引用。
Java栈的可能实现方式,实现的设计者可以任意按自己的想法设计Java栈,一个可能的方式就是从堆中分配每一个帧。
9) 本地方法栈
当线程调用一个本地方法时,就进入了一个全新并且不再受到虚拟机限制的世界。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区,但是不止于此,它还可以做任何它想做的事情。
任何本地方法接口都会使用某种本地方法栈,当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再线程的Java栈中压入新的栈,虚拟机只是简单地动态链接并直接调用指定的本地方法,可以把这看做是虚拟机利用本地方法来动态扩展自己。一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈,或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。
10) 执行引擎
任何Java虚拟机实现的核心都是它的执行引擎,在Java虚拟机规范中,执行引擎的行为使用指令集来定义。“执行引擎”这个术语也有三种理解:一个是抽象的规范,一个是具体的实现,另一个是正在运行的实例,抽象规范使用指令集规定了执行引擎的行为。运行中Java程序的每一个线程都是一个独立虚拟机执行引擎的实例,从线程生命周期的开始到结束,它要么在执行字节码,要么在执行本地方法。Java虚拟机的实现可能用一些对用户程序不可见的线程,比如垃圾收集器,这样的线程不需要实现的执行引擎实例,所有属于用户运行程序的线程,都是在实际工作的执行引擎。
指令集 方法的字节码流是由Java虚拟机的指令序列构成的,每一条指令包含一个单字节的操作码,后面跟随0个或多个操作数。操作码表明需要执行的操作;操作数向Java虚拟机提供执行操作码需要的额外信息。操作码本身就已经规定了它是否需要跟随操作数,以及如果有操作数的话,它是什么形式的。很多Java虚拟机的指令不包含操作数,仅仅是由一个操作码字节构成的。
Java虚拟机指令集关注的中心是操作数栈,一般是把将要使用的值会压入栈中,虽然Java虚拟机没有保存任意值的寄存器,但是每个方法都有一个局部变量集合,指令集实际的工作方式就是把局部变量当做寄存器,用索引来访问。