Java虚拟机的结构(二)
5.运行时数据域(Run-Time Data Areas)
JVM定义了不同的运行时数据域,这些数据域在程序被执行时被使用。一些数据域是在JVM启动的时候就创建,在JVM退出的时候被销毁。而另一些数据域是属于每一个线程的,每一个线程的数据域是随着线程创建而创建,随着线程退出而销毁的。
5.1 程序计数器寄存器(The pc Register)
JVM一次可以支持多个线程运行。每一个JVM线程都拥有属于自己的pc(program counter)寄存器.在任何时间点上,每一个JVM的线程都是在执行单个方法的代码,这个方法被命名为当前线程的当前方法(current method)。如果这个方法不是native的,那么pc寄存器就包含当前正在被执行的JVM指令的地址。如果当前的方法是native的,那么JVM的pc寄存器是未定义的。JVM的pc寄存器是足够宽的足以保存reurnAdress或者一个在特定平台上的native指针。
5.2 JVM栈(JVM Stacks)
每一个JVM线程都有一个私有的JVM 栈,这个JVM栈在线程被创建的时候被建立。一个JVM栈存储着栈帧。一个JVM的栈类似于传统编程语言例如c语言的栈,它保存局部变量,局部结果,在方法调用和返回中起作用。因为JVM栈不能够被直接操作除了压栈和出栈,因此栈帧也许是堆内存分配的。JVM栈的存储不需要是连续的。
这个版本的JVM规范允许JVM栈要么是一个固定大小的,要么可以依据计算需要而动态扩张和压缩的。如果JVM的栈的大小是固定的话,那么在JVM栈创建的时候,这个JVM栈的大小的选择就是独立的。
一个JVM实现应该提供给程序员或者用户对JVM栈的初始化大小的控制,以及在动态扩张和压缩的情形时,对最小最大值的控制。
这个下面是关于JVM栈异常的状态
.如果一个线程中的计算需要的栈的大小超出了JVM的限制,那么JVM就会抛出StackOverflowError
.如果一个JVM栈能够被动态扩张,当动态扩张发生时,没有足够的可用的内存来满足这个扩张强求,或者说是当一个新线程创建一个初始化的JVM栈的时候,没有足够的可用的内存来完成JVM栈的初始化时,JVM抛出OutOfMemeoryError
5.3 堆(Heap)
JVM有一个在JVM所有线程之间共享的堆。堆是这么一种运行时数据区域,所有的类实例和数组的内存都从堆这里来分配。
堆在JVM虚拟机开始的时候被创建。对象的堆存储被一个自动的存储管理系统(俗称GC);对象从来不会被显式的释放。JVM假设没有特定类型的自动内存管理系统,这个存储管理技术根据实现者的系统的需求来选择。这个堆也许是固定大小的或者根据计算需要来进行扩张和压缩。这个堆内存不要求是连续的。
和JVM stacks一样,JVM应该提供程序或者用户对JVM 堆内存固定大小或者动态扩张或者压缩的最大最小值的控制。
.下面是关于堆的异常的状态
如果一个计算过程需要的堆内存大于自动存储管理系统所能提供的可用的堆内存时,JVM抛出OutOfMemoryError
5.4 方法域(Method Area)
JVM有一个方法域,这个数据域被JVM中的所有的线程所共享。这个方法域类似于传统编程语言存储编译后代码的存储域或者类似于操作系统进程中的正文段(text segment).方法域存储每一个类结构例如:运行时常量池、域、方法数据、方法(包括被用来在初始化类和初始化类实例以及接口初始化的特殊的方法)和构造器的代码。
方法域在JVM开启的时候就被创建。尽管方法域逻辑上是堆内存的一部分,但是简单的JVM实现也许不会选择GC的方式来回收和压缩它的。这个版本的JVM规范并没有强制这个方法域的位置以及采用哪一种管理编译后代码的策略。这个方法域也许是固定大小的或者根据计算需要动态的扩张和压缩。方法域的内存不需要是连续的。
和前面介绍的JVM stacks 和 JVM heap一样,JVM的实现应该提供给程序员以及用户对方法域初始化大小和在不同的大小方法域的情形下的最小和最大值的控制。
下面是关于方法域的异常的状态
如果方法域中的内存不足以满足一个内存分配请求,JVM抛出OutOfMemeoryError
5.5 运行时常量池(Run-Time Constant Pool)
一个运行时常量池是class文件中contant_pool表在每一个类或者线程的运行时表示。它包含了几种常量,从编译时确定的数字字面量到必须在运行时解析的方法和域引用等。运行时常量池提供的功能类似于传统编程语言的符号表,但是常量池包含比典型符号表更多广的数据范围。
每一个运行时常量池的内存都是分配与JVM的方法域(method area),一个类或接口的常量池在类或接口被JVM创造的时候被构建出来。
下面是为了构建一个类或接口的常量池可能出现的异常情况:
当创造一个类或接口的时候,如果其运行时常量池的构建需要的内存大于JVM方法域里面可用的内存,JVM就会抛出OutOfMemoryError错误。
5.6 本地方法栈(Native Method Stacks)
一个JVM实现也许会使用传统的栈,通俗的讲就是常说的"C语言栈"来支持native方法(方法用另一种非java语言所写)。本地方法栈也许被JVM指令集的解释器使用在C语言中。如果JVM提供本地方法栈的实现,那么典型的就是。本地方法栈在每一个线程被创建的时候被分配给每一个线程。
同JVM栈一样,本地方法栈也可以使固定大小的,也可以是动态的根据计算需要来扩展或压缩大小的。
下面是关于本地方法栈的一些异常的情况
如果在一个线程中的计算需要的本地方法栈的大小超过了允许的大小,JVM抛出StackOverflowError
如果本地方法栈能被动态的扩展,当本地方法栈尝试扩展的时候没有足够可用的内存,或者说是没有足够的内存来为了一个新线程初始化一个本地方法栈的时候,JVM抛出OutOfMemoryError。
6 栈帧(Frames)
一个栈帧被用来存储数据、局部计算结果、执行动态链接,方法的返回值,以及分发的异常等内容。
一个栈帧在方法在一个方法被调用时候被创建,在方法调用结束的时候被销毁,无论调用是正常结束或者中途异常退出(抛出一个没有被捕获的异常)。栈帧从创造栈帧的线程的JVM栈中得到分配的内存,每一个栈帧有他自己的局部变量的数组,他自己的操作数栈和一个当前方法(Current Method)所在的类的常量池的引用。
一个栈帧也许被增加额外的信息,例如一些调试信息。
上面提到的局部变量数组的大小和操作数栈是在编译时间就被确定了,并且一直被这个栈帧相关联的方法里面的代码所使用。因此,栈帧数据结构的大小仅仅依赖于JVM的实现和这些数据结构在方法调用的同时能被分配到的内存。
在一个给定的控制的线程中,在给定的任何一个时刻,仅仅只有一个栈帧是活跃的,这个栈帧就是当前执行方法(excuting method)的栈帧,这个栈帧被称为当前栈帧(current frame),正如前面提到,它的方法被称为当前方法(current method),这个定义当前方法的类叫做当前类(current class)。在局部变量和操作数上的操作通常是通过使用当前栈帧的引用来完成的。
如果一个栈帧对应的方法调用了另一个方法或者它的方法完成了,那么这个栈帧就不再是当前栈帧了。当一个方法被调用时,一个新的栈帧被创建,当控制权转交到这个方法时,这个栈帧就变成当前栈帧了,这个方法也就变成了当前方法了。当方法返回的时候,这个当前栈帧传回这个方法调用的结果值给前面的栈帧(如果存在的话)。然后这个当前的栈帧就被丢弃(is discarded),而前面那个栈帧就变成了当前的了。
注意被一个线程创建的栈帧是这个线程本地的,不能被任何其他线程所引用。
6.1 局部变量(Local Variables)
每一个栈帧包含一个变量数组,被称为局部变量数组。这个数组的大小在编译时期就已经被确定了,被运用在一个类或者接口的二进制表示中,连同该栈帧相联系的方法的代码一起。
一个单个的局部变量能保留一个boolean、byte、char、short、int(4字节)、float(4字节)、reference、 returnAddress类型的值。一对局部变量能存储long、double类型的值(8字节)。(因此一个单个的局部变量的大小可能是4个字节大小)。
局部变量是通过下标来定位的。第一个变量的下标是0,。一个位于0到小于数组大小的整数可以考虑是一个局部变量数组的下标。
一个long 或者double类型的值消耗两个连续的局部变量。这样类型的局部变量的值使用小的那个的index来定位。例如,一个double类型的值被存储在局部变量数组的位置n,实际上消耗了两个下标值,n 和 n+1。然而,这个n+1的局部变量不能被加载,但是它可以存储值。可是如果在n+1中存储其他值的话,那么存储在n中的内容就没有意义了。
JVM不必须要n是一个偶数。在直观的方面,在局部变量数组中的long和double类型的值不需要是64位对齐的。实现者可以自由的决定合适的方式通过使用两个局部变量来表示这样的值。
JVM在方法调用时候使用局部变量来传递参数。在类方法调用的时候,任何参数的传递都是通过连续的局部变量来完成的,这些连续的局部变量从局部变量0开始。在实例方法的调用时候,局部变量0一直被用来传递当前被调用方法所属的对象的引用(Java语言中就是this)。任何随后的参数通过从局部变量1开始的连续的变量传递。
6.2 操作数栈(Operand Stacks)
每一个栈帧包含一个被称为“操作数栈”的后进先出的栈。这个栈帧的操作数栈的最大的深度在编译时间就已经确定了并一直伴随着这个栈帧相关联的方法的代码被使用。
在上下文清楚无异议的情况下,我们有时候把当前栈帧的操作数栈简单的成为操作数栈。
当包含操作数栈的栈帧被创建的时候,里面的操作数栈是空的。JVM运行一些指令来将常量、局部变量的值或者域加载到操作数栈中。其他的一些JVM指令就从操作数栈中取操作数,运算它们,然后将计算结果压回操作数栈中。这个操作数栈也被用来准备将要传递给方法的参数和接收方法的返回值。
例如:这个iadd指令加和两个int类型的值在一起。它需要这需要加和的两个int类型的值是在操作数栈最顶部的两个值,这两个值被先前的指令压在这个位置(最顶部两个)的。这两个值都从操作数栈中出栈。他们被加和,然后他们的计算结果被压回到操作数栈。
子计算也许会被嵌入在操作数栈中。
在操作数栈中,每一个条目都能持有一个任何JVM类型的值,包括long、double类型的值。
来源于操作数栈的值必须被已合适的方式被操作或计算。将两个int值压入操作数栈中,然后却把它们当做long类型来操作是不不可能的,或者亚茹两个float类型的值到操作数栈中,却使用iadd指令来加和它们也是不可能的。这些严格的定义被class文件检查的时候,被强制实施。
在任何时刻点,一个操作数栈都有一个对应的深度,在这个深度上,一个long 和 double的值占用两个单位深度,而其他类型占用一个单位深度。
6.3 动态链接(Dynamic Linking)
每一个栈帧都包含一个当前方法所在类的常量池的引用为了支持方法代码的动态链接(dynamic linking).这个class文里面的方法代码通过符号引用来关联将调用的方法和将要访问的变量。动态链接就是将这这些符号引用转化为具体的方法引用,和将变量访问转换为数据结构的合适的偏移量。
方法和变量的后期绑定的使用很有用,类中的方法使用这个技术破坏代码的可能性更小。
6.4 正常的方法调用完成(Normal Method Invocation Completion)
一个方法正常的结束如果这个调用过程中没有出现一个来自JVM抛出的异常或者是一个显示的通过throw语言抛出的自定义异常。
如果这个当前方法正常退出,那么接下来一个值也许会返回给调用这个当前方法的方法。这个在被调用的方法执行一个返回指令集中的某一个指令的时候发生,具体使用哪一个返回指令,这个是有严格要求的,这个执行的返回指令必须要是合适的对于将要被返回的值的类型。(如果有返回指的话)
当前栈帧在这种情况下被用来存储调用者的状态包括它的局部变量和操作数栈,伴随着这个调用者的程序计算器适当的增加来跳过这个方法调用指令。然后,在使用方法调用的方法的栈帧中,执行正常进行,伴随着返回值被压入到这个栈帧的操作数栈中。
6.5 异常的方法调用完成(Abrupt Method Invocation Completion)
一个方法调用异常退出如果在这个方法的JVM指令的执行引起了VM抛出了一个异常并且这个异常没有得到这个方法的处理。一个athrow指令的执行也会导致一个异常被显示的抛出,并且如果这个异常没有被当前方法所捕获,就会导致方法调用的异常退出。一个方法调用异常退出是不会不会有一个返回值给调用者的。
总结:
运行时数据域(Run-Time Data Area) | |||
英文名 | 中文名 | 特点 | |
PC | 程序计数器 | 每一个JVM线程都有属于自己的程序计数器寄存器,如果current method不是native的,那么pc寄存器包含当前正在被执行的JVM指令的地址,如果是native的,那么pc寄存器就是未定义的 | |
JVM Stacks | JVM栈 | 每一个JVM线程都有一个私有的JVM栈,JVM栈在线程创建的时候建立,线程结束的时候被销毁,JVM栈保存局部变量,局部结果等 | |
Heap | 堆 | JVM有一个在JVM所有线程之间共享的堆,所有的类实例和数组的内存都来自这里。堆在JVM虚拟机开启的时候被创建,JVM退出的时候被销毁。 | |
Method Area | 方法域 | 方法域被所有线程所共享,在JVM开启的时候被创建,在JVM退出的时候被销毁。方法域逻辑上是堆内存的一部分,方法域存放着运行时常量池、域、方法数据、以及方法和构造器的代码 | |
Run-Time Constant Pool | 运行时常量池 | 运行时常量池是每一个类或者线程的constant_pool表的运行时表示。运行时常量池属于每一个类或者接口,在类或者接口被JVM创建的时候被构建出来 | |
Native Method Stacks | 本地方法栈 | 本地方法栈是属于每一个线程私有的额,JVM在每一个线程被创建的时候分配给每一个线程一个本地方法栈 | |
Frames | 栈帧 | 栈帧属于每一个线程的JVM栈,一个栈帧在一个方法调用时候被创建,在方法调用结束的时候被销毁,无论调用是正常结束,或者中途异常退出,栈帧从创造栈帧的线程的JVM栈中分配内存每一个。每一个栈帧有他自己的局部变量数组和操作数栈,和一个当前方法的当前类的常量池的引用 | |
Local Variables | 局部变量 | 存放方法的局部变量,这个数组的大小在编译时期就被确定了,局部变量通过下标来定位,第一个变量下标是0.一个long或者double类型的值消耗两个连续的局部变量。JVM在方法调用的时候使用局部变量来传递参数,在类方法调用的时候,任何参数的传递都是通过里连续的局部变量来完成的,下标都是0开始的,在实例方法的调用时候,局部变量0一直被用来传递当前方法所在的当前对象的引用this | |
Operand Stacks | 操作数栈 | 每一个栈帧中包含一个先进后出的操作数栈,操作数栈的最大深度在编译时期就已经确定。包含操作数栈的当前栈帧被创建的时候,里面的操作数是空的。JVM运行一些指令将常量、局部变量的值或者域加载到操作数栈中开始运算它们 |