Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机
启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程对应的数据区
域会随着线程开始和结束而创建和销毁。
寄存器
Java 虚拟机可以支持多条线程同时执行(可参考《 Java 语言规范》第 17 章),每一条 Java
虚拟机
线程都有自己
的 PC( Program Counter)寄存器。在任意时刻,一条 Java 虚拟机线程
只会执行一个方法的代码,这个正在被线程执行的方法称为该线程的当前方法( Current
Method, §2.6)。如果这个方法不是 native 的,那 PC 寄存器就保存 Java 虚拟机正在执行的
字节码指令的地址,如果该方法是 native 的,那 PC 寄存器的值是 undefined。 PC 寄存器的容
量至少应当能保存一个 returnAddress 类型的数据或者一个与平台相关的本地指针的值。
Java 虚拟机栈
每一条 Java 虚拟机 线程 都有自己 私有 的 Java 虚拟机栈( Java Virtual Machine Stack)①,这个栈与线程同时创建,用于存储栈帧(Frames)。 Java 虚拟机栈的作用与传统语言(例如 C 语言)中的栈非常类似,就是用于存储 局部变量 与一些 过程结果 的地方。另外,它在方法调用和返回中也扮演了很重要的角色。因为除了栈帧的出栈和入栈之外, Java 虚拟机栈不会再受其他因素的影响,所以栈帧可以在堆中分配②, Java 虚拟机栈所使用的内存不需要保证是连续的。
Java 虚拟机规范允许 Java 虚拟机栈被实现成固定大小的或者是根据计算动态扩展和收缩的。如果采用固定大小的 Java 虚拟机栈设计,那每一条线程的 Java 虚拟机栈容量应当在线程创建的时候独立地选定。 Java 虚拟机实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段,对于可以动态扩展和收缩 Java 虚拟机栈来说,则应当提供调节其最大、最小容量的手段。
Java 虚拟机栈可能发生如下异常情况:
*
如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量时, Java 虚拟机将会抛出一
个 StackOverflowError 异常。
* 如果 Java 虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
① 在 Java 虚拟机规范第一版之中, Java 虚拟机栈也被称为“ Java 栈”。
② 译者注:请读者注意避免混淆 Stack、 Heap 和 Java ( VM) Stack、 Java Heap 的概念, Java 虚拟机
的实现本身是由其他语言编写的应用程序,在 Java 语言程序的角度上看分配在 Java Stack 中的数据,而在实现虚拟机的程序角度上看则可以是分配在 Heap 之中。
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
)、字段和方法数据、构造函数和普通方法的字节码内容、还包
括一些在类、实例、接口初始化时用到的特殊方法(
§
2.9
)。
方法区在虚拟机启动的时候被创建,虽然方法区是堆的逻辑组成部分,但是简单的虚拟机实现
可以选择在这个区域不实现垃圾收集。这个版本的
Java
虚拟机规范也不限定实现方法区的内存位
置和编译代码的管理策略。方法区的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,
并在不需要过多空间时自动收缩。 方法区在实际内存空间中可以是不连续的。
Java
虚拟机实现应当提供给程序员或者最终用户调节方法区初始容量的手段,对于可以动态
扩展和收缩方法区来说,则应当提供调节其最大、最小容量的手段。
方法区可能发生如下异常情况:
如果方法区的内存空间不能满足内存分配请求,那
Java
虚拟机将抛出一个
OutOfMemoryError
异常。
运行时常量池
运行时常量池(
Runtime Constant Pool
)是每一个类或接口的常量池(
Constant_Pool
)的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行
期解析后才能获得的方法或字段引用。运行时常量池扮演了类似传统语言中符号表(
Symbol
Table
)的角色,不过它存储数据范围比通常意义上的符号表要更为广泛。
每一个运行时常量池都分配在
Java
虚拟机的
方法区
之中
,在类和接口被加载到
虚拟机后,对应的运行时常量池就被创建出来。
在创建类和接口的运行时常量池时,可能会发生如下异常情况:
* 当创建类或接口的时候,如果构造运行时常量池所需要的内存空间超过了方法区所能提供的最
大值,那
Java
虚拟机将会抛出一个
OutOfMemoryError
异常。
本地方法栈
Java
虚拟机实现可能会使用到传统的栈(通常称之为“
C Stacks
”)来支持
native
方法
( 指使用
Java
以外的其他语言编写的方法)的执行,这个栈就是本地方法栈(
Native Method
Stack
)。当
Java
虚拟机使用其他语言(例如
C
语言)来实现指令集解释器时,也会使用到本地
方法栈。如果
Java
虚拟机不支持
natvie
方法,并且自己也不依赖传统栈的话,可以无需支持本
地方法栈,如果支持本地方法栈,那这个栈一般会在线程创建的时候按线程分配。
Java
虚拟机规范允许本地方法栈被实现成固定大小的或者是根据计算动态扩展和收缩的。如
果采用固定大小的本地方法栈,那每一条线程的本地方法栈容量应当在栈创建的时候独立地选定。
一般情况下,
Java
虚拟机实现应当提供给程序员或者最终用户调节虚拟机栈初始容量的手段,对
于长度可动态变化的本地方法栈来说,则应当提供调节其最大、最小容量的手段。
本地方法栈可能发生如下异常情况:
如果线程请求分配的栈容量超过本地方法栈允许的最大容量时,
Java
虚拟机将会抛出一个
StackOverflowError
异常。
如果本地方法栈可以动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存
去完成扩展,或者在建立新的线程时没有足够的内存去创建对应的本地方法栈,那
Java
虚拟
机将会抛出一个
OutOfMemoryError
异常。
栈帧
栈帧( Frame )是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接
(
Dynamic Linking
)、方法返回值和异常分派(
Dispatch Exception
)。
栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出
了在方法内未被捕获的异常)都算作方法结束。栈帧的存储空间分配在
Java
虚拟机栈
之中,每一个栈帧都有自己的
局部变量表
(
Local Variables
)、
操作数栈
(
Operand
Stack
)和指向当前方法所属的类的
运行时常量池
的引用。
局部变量表和操作数栈的容量是在编译期确定,并通过方法的
Code
属性(
§
4.7.3
)保存及
提供给栈帧使用。因此,栈帧容量的大小仅仅取决于
Java
虚拟机的实现和方法调用时可被分配的
内存。
在一条线程之中,只有目前正在执行的那个方法的栈帧是活动的。这个栈帧就被称为是当前栈
帧(
Current Frame
),这个栈帧对应的方法就被称为是当前方法(
Current Method
),定义
这个方法的类就称作当前类(
Current Class
)。对局部变量表和操作数栈的各种操作,通常都
指的是对当前栈帧的对局部变量表和操作数栈进行的操作。
如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧
了。当一个新的方法被调用,一个新的栈帧也会随之而创建,并且随着程序控制权移交到新的方法
而成为新的当前栈帧。当方法返回的之际,当前栈帧会传回此方法的执行结果给前一个栈帧,在方
法返回之后,当前栈帧就随之被丢弃,前一个栈帧就重新成为当前栈帧了。
请读者特别注意,栈帧是线程本地私有的数据,不可能在一个栈帧之中引用另外一条线程的栈
帧。
局部变量表
操作数栈
动态链接