java虚拟机学习笔记——java虚拟机内部体系概述(第五章)
注:文中的类型指的是一个类或一个接口。5.1、什么是Java虚拟机 当你谈到Java虚拟机时,你可能是指: 1、抽象的Java虚拟机规范 2、一个具体的Java虚拟机实现 3、一个运行的Java虚拟机实例5.2、Java虚拟机的生命
注:文中的类型指的是一个类或一个接口。
5.1、什么是Java虚拟机
当你谈到Java虚拟机时,你可能是指:
1、抽象的Java虚拟机规范
2、一个具体的Java虚拟机实现
3、一个运行的Java虚拟机实例
5.2、Java虚拟机的生命周期
一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。你在同一台机器上运行三个程序,就会有三个运行中的Java虚拟机。
Java虚拟机总是开始于一个main()方法,这个方法必须是公有、返回void、直接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包换main()方法的类名。
Main()方法是程序的起点,他被执行的线程初始化为程序的初始线程。程序中其他的线程都由他来启动。Java中的线程分为两种:守护线程(daemon)和普通线程(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集的线程就是一个守护线程。当然,你也可以把自己的程序设置为守护线程。包含Main()方法的初始线程不是守护线程。
只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。如果有足够的权限,你可以调用exit()方法终止程序。
5.3、Java虚拟机的体系结构
在Java虚拟机的规范中定义了一系列的子系统、内存区域、数据类型和使用指南。这些组件构成了Java虚拟机的内部结构,他们不仅仅为Java虚拟机的实现提供了清晰的内部结构,更是严格规定了Java虚拟机实现的外部行为。
每一个Java虚拟机都由一个类加载器子系统(class loader subsystem),负责加载程序中的类型(类和接口),并赋予唯一的名字。每一个Java虚拟机都有一个执行引擎(execution engine)负责执行被加载类中包含的指令。
程序的执行需要一定的内存空间,如字节码、被加载类的其他额外信息、程序中的对象、方法的参数、返回值、本地变量、处理的中间变量等等。 Java虚拟机将 这些信息统统保存在运行时数据区中。
运行时数据区中的一部分是由程序中所有线程共享的,还有一些则有只能有一个线程拥有。每一个Java虚拟机都包含方法区(method area)和堆(heap),他们都被整所有线程所共享。Java虚拟机加载并解析一个类以后,将从类文件中解析出来的信息保存于方法区中。程序执行时创建的对象都保存在堆中。
当一个线程被创建时,会被分配只属于他自己的PC寄存器“pc register”(程序计数器)和Java栈(Java stack)。当线程不掉用本地方法时,PC寄存器中保存线程执行的下一条指令。Java堆栈保存了一个线程调用方法时的状态,包括本地变量、调用方法的参数、返回值、处理的中间变量。调用本地方法时的状态保存在本地方法堆栈中(native method stacks),可能在寄存器或者其他非平台独立的内存中。
Java栈有许多栈帧(stack frames (or frames))组成,一个栈帧包含Java方法调用的状态。当一个线程调用一个方法时,Java虚拟机会将一个新的栈帧压入到Java栈中,当这个方法返回时,Java虚拟机会将对应的栈帧弹出并抛弃。
Java虚拟机不使用寄存器保存计算的中间结果,而是用Java栈在存放中间结果。这是的Java虚拟机的指令更紧凑,也更容易在一个没有寄存器的设备上实现Java虚拟机。
图中的Java堆栈中向下增长的,PC寄存器中线程三为灰色,是因为它正在执行本地方法,他的下一条执行指令不保存在PC寄存器中。
5.3.1、数据类型(Data Types)
所有Java虚拟机中使用的数据都有确定的数据类型,数据类型和操作都在Java虚拟机规范中严格定义。Java中的数据类型分为原始数据类型(primitive types)和引用数据类型(reference type)。引用类型依赖于实际的对象,但不是对象本身。原始数据类型不依赖于任何东西,他们就是本身表示的数据。
所有Java程序语言中的原始数据类型,都是Java虚拟机的原始数据类型,除了布尔型(boolean)。当编译器将Java源代码编译为自己码时,使用整型(int)或者字节型(byte)去表示布尔型。 注意:在Java虚拟机中,布尔型false是由整数0表示的,所有非零整数表示布尔型的true,布尔数组被表示为字节数组,虽然他们可能会以字节数组或者字节块(bit fields)保存在堆中。
除了布尔型,其他Java语言中的原始类型都是Java虚拟机中的数据类型。在Java中数据类型被分为:整形的byte,short,int,long;char和浮点型的float,double.Java语言中的数据类型在任何主机上都有同样的范围。
在Java虚拟机中还存在一个Java语言中不能使用的原始数据类型返回值类型(returnValue)。这种类型被用来实现Java程序中的finally 子句。
引用类型可能被创建为:类类型(class type),接口类型(interface type),数组类型(array type)。他们都引用被动态创建的对象。当引用类型引用null时,说明没有引用任何对象。
Java虚拟机规范只定义了每一种数据类型表示的范围,没有定义在存储时每种类型占用的空间。他们如何存储由Java虚拟机的实现者自己决定。
数据类型取值范围:
byte 8-bit (-27 to 27 - 1, 包括两端)
short 16-bit (-215 to 215 - 1,包括两端)
int 32-bit (-231 to 231 - 1,包括两端)
long 64-bit(-263 to 263 - 1,包括两端)
char 16-bit (0 to 216 - 1, 包括两端)
float 32-bit (IEEE 754 单精度浮点数)
double 64-bit( IEEE 754 双精度浮点数)
returnValueaddress 同一方法中某操作码的地址
reference 对堆中某对象的引用
5.3.2、字节长度
Java虚拟机中最小的数据单元式字(word),其大小由Java虚拟机的实现者定义。但是一个字的大小必须足够容纳 byte,short,int, char,float,returnValue,reference;两个字必须足够容纳long,double.所以虚拟机的实现者至少提供的字不能小于31bits的字,但是最好选择特定平台上最有效率的字长。
在运行时,Java程序不能决定所运行机器的字长。字长也不会影响程序的行为,他只是在Java虚拟机中的一种表现方式。
5.3.3、类加载器子系统
Java虚拟机中的类加载器分为两种:启动类加载器和用户自定义类加载器。启动类加载器是Java虚拟机实现的一部分,用户自定义类装载器是运行中的程序的一部分。
1、加载、连接、初始化(Loading, Linking and Initialization)
类加载子系统不仅仅负责定位并加载类文件,他按照以下严格的步骤作了很多其他的事情:(具体的信息参见第七章的“类的生命周期”)
1)、加载:寻找并导入指定类型(类和接口)的二进制信息2)、连接:进行验证、准备和解析
①验证:确保导入类型的正确性
②准备:为类型分配内存并初始化为默认值
③解析:将字符引用解析为直接饮用
3)、初始化:调用Java代码,初始化类变量为合适的值
5.3.4、方法区(The Method Area)
在Java虚拟机中,被加载类型的信息都保存在方法区中。这些信息在内存中的组织形式由虚拟机的实现者定义,程序中的所有线程共享一个方法区,所以访问方法区信息的方法必须是线程安全的。方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整,同样虚拟机也可以卸载某个“不再引用”的类,使方法区占据的内存保持最小。虚拟机也可以允许用户指定方法区的初始大小及最小和最大尺寸等。
虚拟机要为每个被装载的类型在方法区存储以下的信息:1)、类型的全名
2)、类型的父类型的全名(除非没有父类型,或者是java.lang.Object)
3)、该类型是一个类还是接口
4)、类型的修饰符(public,private,protected,static,final,volatile,transient等)
5)、所有父接口全名的列表
6)、类型的常量池
7)、类型字段的信息 包括:字段名、字段的类型、字段的修饰符(public,private,protected,static,final,volatile,transient等)
8)、类型方法的信息 包括:方法名、方法的返回类型、方法参数的类型和类型、方法的修饰符(public,private,static,final,synchronized,native,abstract)、方法的字节码、操作数栈和栈帧中局部变量区的大小、异常表。
9)、所有的静态类变量(非常量)信息10)、一个指向类加载器的引用
11)、一个指向Class类的引用
1)、类型的常量池(The constant pool for the type)
虚拟机必须为每个被装载的类型维护一个常量池,常量池就是该类型所用常量的一个有序集合,包含直接常量(literals)如字符串、整数、浮点数的常量,和对其它类型、字段、方法的符号引用。池中的数据就像数组一样是通过索引访问的,因为常量池存储了相应类型所用到的所有类型、字段和方法的符号引用,所以它是java程序动态连接中起到核心的作用。
2)、类(静态)变量(Class Variables)
类变量被所有类的实例共享,即使不通过类的实例也可以访问。这些变量绑定在类上(而不是类的实例上),所以他们是类的逻辑数据的一部分。在Java虚拟机使用这个类之前就需要为类变量(non-final)分配内存
常量(final)的处理方式于这种类变量(non-final)不一样。每一个类型在用到一个常量的时候,都会复制一份到自己的常量池中。常量也像类变量一样保存在方法区中,同时他也保存在常量池中。(可能是,类变量被所有实例共享,而常量池是每个实例独有的)。Non-final类变量保存为定义他的类型数据(data for the type that declares them)的一部分,而final常量保存为使用他的类型数据(data for any type that uses them)的一部分。
3)、方法表(Method Tables)
除了上述的原始类型信息,实现中还包括了其它的数据结构以加快访问原始数据的速度,比如方法表,把它作为类信息的一部分保存在方法区。方法表是一个数组,它的元素是所有它的实例可能被调用的实例方法的直接引用,包括那些从超类继承过来的实例方法。
5.3.5、堆
当Java程序创建一个类的实例或者数组时,都在堆中为新的对象分配内存。虚拟机中只有一个堆,所有的线程都共享他。
1、垃圾收集(Garbage Collection)
垃圾收集是释放没有被引用的对象的主要方法。它也可能会为了减少堆的碎片,而移动对象。
2、对象存储结构(Object Representation)
Java虚拟机的规范中没有定义对象怎样在堆中存储。每一个对象主要存储的数据包括他所属的类和父类中定义的实例变量。只要有一个对象引用,虚拟机就必须能够快速地定位对象实例的数据,另外,也必须通过该对象引用访问相应的类数据(方法区中的数据)。
一个可能的堆的设计是将堆分为两个部分:引用池和对象池。一个对象的引用就是指向引用池的本地指针。每一个引用池中的条目都包含两个部分:指向对象池中对象数据的指针和方法区中对象类数据的指针。这种设计能够方便Java虚拟机堆碎片的整理。当虚拟机在对象池中移动一个对象的时候,只需要修改对应引用池中的指针地址。缺点是每次访问对象的数据都需要处理两次指针。下图演示了这种堆的设计。
另一种堆的设计是:使对象的引用直接指向一组数据,而该数据包括对象实例数据以及指向方法区中类数据的指针。这种设计方便了对象的访问,可是对象的移动要变的异常复杂。下图演示了这种设计
注:虚拟机必须能通过对象引用得到类型数据的原因:1)当程序试图将一个对象转换为另一种类型时,虚拟机需要判断这种转换是否是这个对象的类型,或者是他的父类型。2)当程序适用instanceof语句的时候也 会做类似的事情。 3)当程序调用一个对象的实例方法时,虚拟机需要进行动态绑定,它不能按照引用的类型来决定将要调用的方法,而必须根据对象的实际类。
无论虚拟机实现者使用哪一种设计,他都可能为每一个对象保存一个类似方法表的信息。因为他可以提升对象方法调用的速度,对提升虚拟机的性能非常重要,但 是虚拟机的规范中比没有要求必须实现类似的数据结构。下图描述了这种结构:
图中展示了一种把方法表和对象引用联系起来的实现方式,每个对象的数据都包含一个指针特殊数据结构的指针,这个数据结构位于方法区,包括两部分:一个指向方法区对应类型数据的指针,另一部分是对象的方法表。方法列表是一个指向所有可能被调用对象方法的指针数组。方法数组包括三个部分:操作数栈的大小和方法栈的本地变量区的大小;方法的字节码;异常表。
另外:堆上的对象数据中还有一个逻辑部分,那就是对象锁,在java虚拟机中每个对象都有一个对象锁,但是只有当第一次需要加锁的时候才分配对应的锁数据,但这时虚拟机需要用某种间接的方法来联系对象数据和对应的锁数据。除了锁数据,每个java对象逻辑上还与实现等待集合的数据相关联,锁是用来实现多线程对共享数据的互斥访问的,而等待集合是用来让多线程完成共同目标而协调工作的。最后一种与java对象关联的数据是与垃圾收集器有关的数据。
3、数组的内部表示(Array Representation)
在Java 中,数组是一种完全意义上的对象,他和对象一样保存在堆中、有一个指向Class类实例的引用。所有同一维度和类型的数组拥有同样的Class,数组的长度不做考虑。对应Class的名字表示为维度和类型。比如一个整型数据的Class为“[I”,字节型三维数组Class名为“[[[B”,两维对象数据 Class名为“[[Ljava.lang.Object”。
多维数组被表示为数组的数组,如下图:
数组必须在堆中保存数组的长度,数组的数据和对数组类型数据的引用。通过一个数组引用的,虚拟机应该能够取得一个数组的长度,通过索引能够访问特定的数据,能够调用Object定义的方法。Object是所有数据类的直接父类。
5.3.6、PC寄存器(程序计数器)(The Program Counter)
每一个线程开始执行时都会被创建一个程序计数器。程序计数器只有一个字长(word),所以它能够保存一个本地指针和returnValue. 当线程执行时,程序计数器中存放了正在执行指令的地址,这个地址可以使一个本地指针,也可以使一个从方法字节码开始的偏移指针。如果执行本地方法,程序计数器的值没有被定义。
5.3.7、Java栈(The Java Stack)
当一个线程启动时,Java虚拟机会为他创建一个Java栈。Java栈以帧为单位保存线程的运行状态,虚拟机只会直接对Java栈执行两种操作:压入和弹出帧。
线程中正在执行的方法被称为当前方法(current method),当前方法所对应的frame被称为当前帧(current frame)。定义当前方法的类被称为当前类(current class),当前类的常量池被称为当前常量池(current constant pool.)。当线程执行时,Java虚拟机会跟踪当前类和当前常量池。
5.3.8、栈帧(The Stack Frame)
栈帧包含三部分:局部变量区、操作数栈和帧数据。局部变量区和操作数堆栈的大小都是以字(word)为单位的,他们在编译就已经确定并放在class文件中。帧数据的大小取决于不同的实现。当程序调用一个方法时,虚拟机从类数据中取得局部变量区和操作数栈的大小,创建一个合适大小的栈帧,然后压入Java栈中。
1、局部变量区(Local Variables)
Java栈帧的局部变量区被组织为一个以字长为单位、从0开始计数的数组,指令通过提供他们的索引从局部变量区中取得相应的值。 Int,float,reference, returnValue占一个字,byte,short,char被转换成int然后存储,long和doubel占两个字。
指令通过提供两个字索引中的前一个来取得long,doubel的值。比如一个long的值存储在索引3,4上,指令就可以通过3来取得这个long类型的值。
本地变量区中包含了方法的参数和局部变量。编译器将方法的参数以他们申明的顺序放在数组的前面。但是编译器却可以将本地变量任意排列在局部变量区数组中。注:1)对于任何一个实例方法都隐含的传递了参数this,因此局部变量区中第一个参数是一个reference(引用)。2)因为栈帧是以字长为单位的,所以byte、short、char在栈帧中被转成int存储,当它被存回堆或方法区时,才会转换回原来的类型。3)在java中,所有的对象都按引用传递,并且都存储在堆中,永远都不会在局部变量区或操作数栈中发现对象的拷贝,只会有对象引用。
和局部变量区一样,操作数栈也被组织为一个以字长为单位的数组。但是不像局部变量区那样通过索引访问,而是通过push和pop来实现访问的。虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的,byte、short、char类型的值在压入到操作数栈之前也会被转换成int。
不同于程序计数器,java虚拟机没有寄存器,程序计数器由于是寄存器,不能被java程序指令访问。java虚拟机的指令是从操作数栈中而不是从寄存器中取得操作数的,因此它的运行方式是基于栈的。当然,指令也可以从其他地方去的操作数,比如指令后面的操作码,或者常量池。但是Java虚拟机指令主要是从操作数栈中取得他们需要的操作数。
Java虚拟机将操作数栈视为工作区,大多数指令从这里弹出数据,执行运算,然后把结果夺回操作数栈。
3、帧数据区(Frame Data)
除了局部变量区和操作数栈以外,java栈帧还包括了为了支持常量池解析,方法返回值和异常分发需要的数据,他们被保存在帧数据中。
每当虚拟机要执行某个需要用到常量池数据的指令时,它都会通过帧数据区中指向常量池的指针来访问所需要的信息。前面提到过,常量池中对类型、字段和方法的引用在开始时都是符号,当虚拟机在常量池中搜索的时候,如果遇到指向类、接口、字段或方法的入口,假若它们仍然是符号,虚拟机才会进行解析。
另外,当一个方法正常返回时,虚拟机需要重建那个调用这个方法的方法的堆栈帧。如果执行完的方法有返回值,虚拟机就需要将这个值push进调用方法的哪个操作数堆栈中。
帧数据中也包含虚拟机用来处理异常的异常表的引用。异常表定义了一个被catch语句保护的一段字节码。异常表中的每一项都有一个被catch子句保护的代码的起始和结束位置等信息,当一个方法抛出一个异常时,Java虚拟机就是用异常表去判断如何处理这个异常。如果虚拟机找到了一个匹配的catch,他就会将控制权交给catch语句。如果没有找到匹配的catch,方法就会异常返回,然后在调用的方法中继续这个过程。
除了以上的三个用途外,帧数据还可能包含一些依赖于实现的数据,比如调试的信息。
5.3.9、本地方法堆栈
本地方法区依赖于虚拟机的不同实现。虚拟机的实现者可以自己决定使用哪一种机制去执行本地方法。
任何本地方法接口(Native Method Interface)都使用某种形式的本地方法堆栈。
5.3.10、执行引擎
一个java虚拟机实现的核心就是执行引擎。在Java虚拟机规范中,执行引擎被描述为一系列的指令。对于每一个指令,规范都描述了他们应该做什么,但是没有说要如何去做。 运行中java程序的每一个线程都是一个独立的虚拟机执行引擎的实例。从线程生命周期的开始到结束,它要么在执行字节码,要么在执行本地方法。一个线程可能通过解释或者使用芯片级指令直接执行字节码,或者间接通过即时编译器执行编译过的本地代码。java虚拟机的实现可能用一些对用户程序不可见的线程,比如垃圾收集器。这样的线程不需要是实现的执行引擎的实例。所有属于用户运行程序的线程,都是在实际工作的引擎。
1、指令集
在Java虚拟机中,一个方法的字节码流就是一个指令的序列。每一个指令由一个单字节的操作码(Opcode)和可能存在的操作数(Operands)。操作码指示去做什么,操作数提供一些执行这个操作码可能需要的额外的信息。一个抽象的执行引擎每次执行一个指令。这个过程发生在每一个执行的线程中。根据操作码的需要,虚拟机可能除了跟随操作码的操作数之外,还需要从另一些存储区域得到操作数。当虚拟机执行一条指令的时候,可能使用当前常量池中的项、当前帧的局部变量中的值、或者位于当前帧操作数栈顶的端的值。
有时,执行引擎可能会遇到一个需要调用本地方法的指令,在这种情况下,执行引擎会去试图调用本地方法,但本地方法返回时,执行引擎会继续执行字节码流中的下一个指令。本地方法也可以看成对Java虚拟机中的指令集的一种扩充。
决定下一步执行那一条指令也是执行引擎工作的一部分。执行引擎有三种方法去取得下一条指令。多数指令会执行跟在他会面的指令;一些像goto, return的指令,会在他们执行的时候决定他们的下一条指令;当一个指令抛出异常时,执行引擎通过匹配catch语句来决定下一条应该执行的指令。
java虚拟机指令集关注的中心是操作数栈,一般是把将要使用的值会压入栈中,虽然java虚拟机没有保存任意值的寄存器,但每个方法都有一个局部变量集合,指令集实际的工作方式就是把局部变量当做寄存器,用索引来访问。要使用保存在局部变量中的值之前,必须先将它压入操作数栈。
平台独立性、网络移动性、安全性左右了Java虚拟机指令集的设计。平台独立性是指令集设计的主要影响因素之一。基于栈的结构使得Java虚拟机可以在 更多的平台上实现。更小的操作码,紧凑的结构使得字节码可以更有效的利用网络带宽。一次性的字节码验证,使得字节码更安全,而不影响太多的性能。
2、执行技术
许多种执行技术可以用在Java虚拟机的实现中:解释执行,即时编译(just-in-time compiling),自适应优化、芯片级直接执行。最有意义也是最迅速的执行技术之一是自适应优化。在最初的虚拟机每次解析一条字节码;第二代虚拟机加入了即时编译器,在第一次执行方法的时候先编译成本地代码,然后执行这段本地代码。 自适应优化的虚拟机开始的时候对所有的代码都是解释运行,但是它会监视代码的执行情况,大多数程序花费80%-90%的时间来执行10%-20%的代码,当自适应优化的虚拟机判断出这种情况后,会启动一个后台线程,把字节码编译成本地代码,非常仔细的优化这些本地代码。自适应优化技术使程序最终能把原来80%-90%运行时间的代码变为极度优化的、静态连接的c++本地代码。
3、线程
Java虚拟机规范定义了一种为了在更多平台上实现的线程模型。Java线程模型的一个目标时可以利用本地线程。利用本地线程可以让Java程序中的线程能过在多处理器机器上真正的同时执行。
Java线程模型的一个代价就是线程优先级,一个Java线程可以在1-10的优先级上运行。1最低,10最高。如果设计者使用了本地线程,他们可能将这 10个优先级映射到本地优先级上。Java虚拟机规范只定义了,高一点优先级的线程可以却一些cpu时间,低优先级的线程在所有高优先级线程都堵塞时,也可以获取一些cpu时间,但是这没有保证:低优先级的线程在高优先级线程没有堵塞时不可以获得一定的cpu时间。因此,如果需要在不同的线程间协作,你必须使用的“同步(synchronizatoin)”。
同步意味着两个部分:对象锁(object locking)和线程等待、激活(thread wait and notify)。对象锁帮助线程可以不受其他线程的干扰。线程等待、激活可以让不同的线程进行协作。
在Java虚拟机的规范中,Java线程被描述为变量、主内存、工作内存。每一个Java虚拟机的实例都有一个主内存,它包含了所有程序的变量:对象的实例变量、数组的元素和类变量。每一个线程都有自己的工作内存,它保存了哪些它可能用到的变量的拷贝。由于局部变量和参数是每个线程私有的,可以从逻辑上看成是工作内存的一部分。管理低层线程行为的规则:
1)、从主内存拷贝变量的值到工作内存中
2)、将工作内存中的值写会主内存中
如果一个变量没有被同步化,线程可能以任何顺序更新主内存中的变量。为了保证多线程程序的正确的执行,必须使用同步机制。
5.3.11、本地方法接口(Native Method Interface)
Java虚拟机的实现并不是必须实现本地方法接口。一些实现可能根本不支持本地方法接口。Sun的本地方法接口是JNI(Java Native Interface)。
更多推荐
所有评论(0)