JVM 定义了各种运行期的数据区域,可以在执行程序时使用。有些数据区域在虚拟机启动时创建,当虚拟机中止时才被销毁。另外一些数据区域是根据线程创建的,数据区域在线程被创建时创建,线程中止时销毁。且每个线程间数据是单独存储的。
Runtime Data Areas运行时数据区可以划分为6个区域:
PC 寄存器(Program Counter Register)
JVM 栈(JVM Stacks)
本地方法栈(Native Method Stacks)
方法区(Method Area)
运行时常量池(Runtime Constant Pool)
堆(Heap)
在这6个区域中,PC 寄存器(Program Counter Register),JVM 栈(JVM Stacks)以及本地方法栈(Native Method Stacks)是根据不同线程启动而被创建的。
堆(Heap)、方法区(Method Area)以及方法区(Method Area)其中的运行时常量池(Runtime Constant Pool)是随JVM 启动而被创建,其中数据都是被所有线程公用的。如图所示:
1.JVM 共享区
(1) 堆(Heap)
JVM 有一个所有JVM 线程间共享的堆(Heap)。堆是分配所有类实例和数组内存的运行期数据区域。
堆在虚拟机启动时被创建。堆中对象的存储由自动存储管理系统(常被称为垃圾回收器或GC)回收,对象从来不会被显示的回收。JVM 承担着非特殊类型的自动存储管理系统,当然存储管理技术也可以根据实现者的系统要求来选择。堆可以是固定大小或是根据需求计算进行扩展,或者也可以是当一个大的堆不必要时进行收缩。堆的内存不需要是连续的。
一个JVM 实现可以让开发者或者用户控制堆初始的大小,同样的,如果堆能够动态扩展或者收缩,可以控制其最大值和最小值。
以下异常情况常与堆有关:
如果计算需求所须更多的堆无法由自动存储管理系统提供时,JVM 将会抛出OutOfMemoryError.
(2) 方法区(Method Area)
JVM 有一个所有JVM 线程共享的方法区。方法区类似传统语言编译后代码的存储区,或者像是UNIX 进程中的正文(test)段。它保存每个类结构诸如运行时的常量池、域、方法数据、方法和构造器的代码(包括在类、实例初始化、接口初始化时使用的特殊方法)。
方法区在虚拟机启动时被创建。虽然方法区逻辑上是堆的一部分,但是简单的实现可以选择既不垃圾回收也不压缩它。该版本的JVM 规范不要求指定方法区的位置或者用于管理编译后代码的策略。方法区可以是固定大小,也可以根据需求计算扩展,并且当大的方法区不再需要时进行收缩。方法区的内存不需要是连续的。
一个JVM 实现可以让开发者或用户控制方法区初始的大小,同样的,在可变大小方法区时,控制方法区的最大值和最小值。
以下异常情况常与方法区有关:
如果方法区的内存不能满足分配请求,JVM 会抛出OutOfMemoryError。
(3) 运行常数池(Runtime Constant Pool)
运行常数池是每个类或者接口运行时constant_pool表在类文件(class file)中的表现。它包含几种常数类型,范围从编译期已知的数字文字到必须在运行期被解析的方法和域引用。常数池的作用类似于传统编程语言中的符号表,尽管它比典型符号表有更宽的数据范围。
每个常量池由JVM 方法区分配。每个类或接口的常量池在类和接口被JVM 创建时建立。
以下异常情况常与类或接口的常量池有关:
当创建类或接口时,如果常量池的建立需要的内存不能被JVM 的方法区分配,JVM 会抛出OutOfMenoryError.
2.线程共享区
(1) PC 寄存器(Program Counter Register)
JVM 能够支持多个线程同时执行。每个Java 虚拟机线程都有独自的PC 寄存器(程序计数器)。在任何时候,每个JVM 线程执行单个方法的代码时,这个方法就是那个线程的当前方法。如果那个方法不是本地的(Native),PC 寄存器则包含当前正在被执行的指令地址。如果线程正在执行的方法是本地的(Native),JVM 的PC 寄存器是未定义的。JVM 的PC 寄存器有足够的宽度来持有一个返回地址(ReturnAddress)或在特定的平台上的本地指针(Native Point)。
(2) Java虚拟机栈(JVM Stacks)
每个JVM 线程都有一个私有的JVM 栈(Stacks),它将和线程同时创建。JVM 栈用来存储帧(后面会讲解)。JVM 栈类似于传统语言例如C 的栈,它持有局部变量和部分结果并且参与方法的调用和返回。 由于JVM 栈除了压入弹出帧外不会被直接操作,所以帧可以由堆(Heap)来分配。对于JVM 栈的内存不必是连续的。
JVM 规范允许JVM 栈的大小是固定的,也可以是根据需求计算来扩展和收缩。如果JVM 栈是固定大小,则每个JVM 栈大小可以在栈创建时独立地选择。一个JVM 实现可以让程序员或用户控制JVM 初始栈的大小,以及在动态扩展或收缩JVM 栈时,控制其最大值和最小值。
以下异常情况常与JVM 栈有关:
如果线程中的计算需要一个比允许的JVM 栈更大时,JVM 将会抛出StackOverflowError.
如果JVM 栈可动态扩展,当没有足够的内存分配给所尝试的扩展,或者没有足够的内存来为一个新线程创建初始化JVM 栈,JVM 将会抛出OutOfMemoryError.
(3) 本地方法栈(Native Method Stacks)
一个JVM 的实现可以使用传统栈,俗称“C栈(C stacks)”,来支持本地方法,那些非Java 编程语言中的的方法。本地方法栈可以被JVM 指令集的解释器实现(比如C)所使用。不装载本地方法和不自身依赖传统栈的JVM 实现者,不需要提供本地方法栈。如果提供,本地方法栈通常由每个线程在创建时被分配。
JVM 规范允许本地方法栈是固定大小或者根据需求动态扩展或收缩。如果本地方法是固定大小,每个本地方法栈大小在栈被创建时独自选择。在任何情况下,一个JVM 实现可以让开发者或用户控制本地方法栈的初始大小。如果是可变大小本地方法栈,同样能够控制方法栈的最大值和最小值。
以下异常情况常与本地方法栈有关:
如果线程中计算所需的本地方法栈大于允许范围,JVM 会抛出StackOverflowError。
如果本地方法栈能动态扩展,当没有足够的内存分配给所尝试的扩展,或者没有足够的内存分配给新线程中创建的初始本地方法栈,JVM 就会抛出OutOfMemoryError。
3.帧(Frames)
帧用来存储数据和部分结果,同样也用来执行动态链接、返回方法值和投递异常。
每次调用方法时会创建一个新的帧。当方法调用完成时(无论正常或者异常中断抛出异常),帧才会销毁。帧由创建帧的线程的JVM 栈分配空间。每个帧拥有自己的本地变量数组,操作数栈(operand stack)和当前类中方法运行常数池的引用。
局部变量数组和操作数栈的大小在编译时决定,并且该大小由与方法代码相关联的帧提供。因此帧数据结构的大小依赖于JVM 的实现,帧内存在方法调用时被同时分配。
只有一个帧,即正在执行方法的帧,在给定线程控制的任何点是活跃的。这个帧被称为当前帧,这个方法即为当前方法。当前方法所在的类被定义为当前类。局部变量和操作数栈的操作通常引用当前帧。
只有当方法调用另一个方法或者方法结束时,帧不再为当前帧。当一个方法被调用,控制转移到新方法时,一个新帧被创建并成为当前帧。当方法返回时,如果方法引用有结果,则当前帧传递回该方法引用的结果给上一个帧。当前帧被抛弃,上一个帧成为当前帧。
注意一个线程中创建的帧是局部的,不能被其他线程所引用。
(1) 局部变量(Local Variables)
每个帧都有一个局部变量数组。帧中局部变量数组的大小在编译时决定,由二进制表示的类或接口与帧相关联方法的代码提供。
一个单字节的局部变量能保存boolean、byte、char、short、int、float、引用或返回地址的值。两个局部变量能保存long或者double。
局部变量通过索引寻址。第一个局部变量的索引为0。本地变量数组的索引基于0与数组大小之间的整数。
long与double类型占据两个连续的局部变量。该值通过第一个变量的索引寻址。例如,一个double类型的值存储在局部变量索引为n(其实占据了局部变量n和n+1的索引),但本地变量索引n+1不能被使用。它可以存储到,但是,这样会使本地变量n的内容无效。
JVM 不需要n为偶数。在直观上来讲,类型double和long在局部变量数组中不需要是64位对齐的,实现者可以自由决定用合适的方法诸如用两个局部变量来存储该值。
JVM 通过局部变量来传递方法引用中的参数。一个类方法引用中的任何参数通过连续的局部变量来传递(从局部变量0开始)。在一个实例方法引用中,局部变量0通常用来传递调用该方法对象实例的引用。其后任何参数通过从局部变量1开始的连续局部变量传递。
(2) 操作数栈(Operand Stacks)
每个帧都有一个后进先出(LIFO)栈,被称为操作数栈。一个帧操作数栈的最大深度在编译期决定,由与帧相关联方法的代码提供。
如果上下文明确,我们会把当前帧的操作数栈简称为操作数栈。
当帧创建时,其中的操作数栈是空的。通过JVM 提供的指令加载常数、局部变量的值或域到操作数栈。操作数栈同时也用来准备参数传递给方法,接收方法的返回值。
例如,iadd指令将两个整数值相加。它要求相加的两个整数值是操作数栈顶的两个值(由以前的指令压入在那里)。两个整数值都从操作数栈弹出,他们相加,相加的和压回操作数栈。子计算可以嵌套在操作数栈上,其结果值可以被相邻的计算使用。
每个操作数栈项可保存任何JVM 类型的值,包括long和double。
操作数栈的值必须用与他们的值类型相适当的方法来操作。例如,压入两个int值把它们当作long来处理或者压入两个float值随后通过iadd指令将他们相加,是不可能的。一小部分JVM 指令(dup和swap)在运行数据区域操作作为原始值而不需要关心他们的类型,这些指令被定义不能用来修改或打断单独的值。class文件校验器加强了这些操作数栈操作的限制。
在任何时间点操作数栈有一个关联的深度,long和double类型深度为两个单位,其他类型深度为一个单位。
(3) 动态链接(Dynamic Linking)
每个帧都有一个当前方法类型的运行常数池的引用,用来支持方法代码的动态链接。方法的class文件代码与被调用的方法相关联,通过符号引用来访问变量。动态链接转译这些符号方法引用到具体的方法引用,在需要时加载class来解析未定义的符号,并且把变量访问转译成这些变量地址相关联的存储结构中合适的位移。
这种方法和变量的晚绑定在使用方法时引起的其他类中的改变而破坏代码的可能性很小。
(4) 正常的方法调用结束
方法调用在没有引起异常抛出时正常的结束(异常直接由JVM ,或者执行显示throw语句抛出)。如果当前方法调用正常的结束,则一个值可以返回到正在调用该方法的方法。这在调用的方法执行return指令时发生,指令返回的选择必须与被返回值的类型(如果有)相合适。
如下情况帧用来恢复调用者的状态(包括局部变量和操作数栈),当调用者的程序计数器适当的增加跳过了方法调用指令。然后返回值(如果有)压入调用方法帧的操作数栈中,执行在这个帧中正常进行。
(5) 意外的方法调用结束
当方法中JVM 指令执行引起JVM 抛出异常,而且异常没有被方法处理,这个方法调用意外结束。执行throw语句显示的抛出异常,并且没有被当前方法捕获,会引起方法意外结束。意外结束的方法不会返回任何值给调用者。
这一节的内容有些抽象,只是一篇介绍性文章,后面会逐个概念去细节分析。