Java 运行时数据区域

运行时数据区域

JVM 在执行 Java 程序时会将它所管理的内存划分为几个数据区域,《Java 虚拟机规范 (Java SE 7 版)》中规定了以下运行时的数据区域。

运行时数据区域

程序计数器

一块较小的内存空间,可以看作是当前线程执行字节码的行号指示器。在虚拟机的概念模型中,字节码解析器就是通过修改这个计数器的值来选取下一条需要执行的字节码指令。

程序计数器是线程私有的,每个线程都有一个独立的程序计数器,方便线程切换后能够恢复到当初的执行位置。

如果线程正在执行的是 Java 方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,则值为 Undefined。该区域是 Java 虚拟机规范中唯一没有规定 OutOfMemoryError 情况的区域。

虚拟机栈

虚拟机栈也是线程私有的,生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型,每个方法在执行时都会创建一个栈帧(Stack Frame)用来存储局部变量表、操作数栈、动态链接、方法返回地址等信息。每个方法从调用到结束的过程,对应着一个栈帧在虚拟机中从入栈到出栈的过程。

Java 虚拟机规范中对该区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机允许的深度,则抛出 StackOverFlowError 异常;如果虚拟机栈可以动态扩展而在扩展时无法申请到足够的内存,则会抛出 OutOfMemoryError 异常。

栈帧

栈帧是虚拟机进行方法调用和执行的数据结构,是虚拟机栈的栈元素。栈帧中需要多大的局部变量表,多深的操作数栈在编译时就已经确定,不会受程序运行时的影响,仅仅取决于具体的虚拟机实现。

一个线程的方法调用链可能很长,对于执行引擎来说,在活动线程中,位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),相关联的方法称为当前方法(Current Method)。执行引擎执行的字节码指令都只针对当前栈帧操作。

栈帧

局部变量表

局部变量表是一组变量值存储空间,存放方法参数和方法内定义的局部变量,大小在编译时就确定并存放在方法的 Code 属性的 max_locals 数据项中。

Java 虚拟机的数据类型有 boolean、byte、char、short、int、float、reference、returnAddress、long、double,它们与 Java 语言中的数据类型有本质区别。其中,long 和 double 需要占用 64 位,reference 类型可能是 32 位或 64 位,其余数据类型占用 32 位或小于 32 位。

局部变量表的基本单位是变量槽(Variable Slot),Java 虚拟机规范没有明确指定一个 Slot 占用的内存空间大小,但是一个 Slot 应该能够存放一个 32 位或小于 32 位的虚拟机数据类型。long 类型和 double 类型的数据虚拟机会通过高位对齐的方式为其分配两个连续的 Slot。因为没有明确指定,所以 Slot 可以随着处理器、OS 或虚拟机的不同而变化,如果使用 64 位虚拟机和 64 位物理内存空间,虚拟机可以通过对齐和补白的方式让 Slot 看起来仍和在 32 位虚拟机中一致。

虚拟机通过索引定位来使用局部变量表,从 0 开始到最大的 Slot,访问 32 位数据类型的变量,索引 n 就代表第 n 个 Slot;访问 64 位数据类型变量,会使用索引 n 和 n+1 两个连续的 Slot。

在执行方法时,虚拟机通过使用局部变量表来完成参数值到参数列表的传递。如果执行实例方法(非 static 的方法),局部变量表的索引 0 的 Slot 默认用于传递方法所属对象的引用,方法中可以使用 this 来访问这个隐含的参数,其余参数按照参数列表的顺序从索引 1 开始。当参数列表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的 Slot。

操作数栈

操作数栈是一个先进后出栈,最大深度也在编译时确定并保存到方法 Code 属性的 max_stacks 数据项中,每个元素可以是任意 Java 数据类型,32 位数据占用 1 个栈容量,64 位数据占用 2 个栈容量。

当方法开始执行时,这个方法的操作数栈是空的,在执行过程中,各种字节码指令向操作数栈写入(入栈)和提取(出栈)数据。如算术运算、调用其他方法时的参数传递等等都是通过操作数栈来进行的。

在概念模型中,虚拟机栈中的两个栈帧是完全独立的,但是在大多虚拟机实现中都会做一些优化处理,让上下两个栈帧共享一部分数据。

共享区域

动态链接

方法返回地址

附加信息

本地方法栈

本地方法栈与虚拟机栈作用相似,虚拟机栈为执行 Java 方法(也就是字节码)服务,而本地方法栈为虚拟机使用到的 Native 方法服务。虚拟机规范没有对本地方法栈中使用的语言、使用方式和数据结构做强制规定,具体的虚拟机可以自由实现。在 HotSpot 虚拟机中就直接把本地方法栈与虚拟机栈合二为一。本地方法栈也有 StackOverFlowErrorOutOfMemoryError 异常。

Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,几乎所有的对象实例在这里分配内存,根据 JSR133,即 Java 内存模型与线程规范的规定,所有的类实例数组都存储在堆中。

The Java® Virtual Machine Specification 原文

The Java Virtual Machine has a heap that is shared among all Java Virtual Machine threads. The heap is the run-time data area from which memory for all class instances and arrays is allocated.

大意是所有的对象实例以及数组都要在堆上分配内存。但是随着 JIT 编译器的发展和逃逸分析技术的成熟,栈上分配、标量替换优化技术会导致一些微妙的变化,所有对象都在堆上分配内存也不那么绝对了。

Java 堆是垃圾收集器管理的主要区域,堆可以处于物理上不连续的内存空间,只要逻辑上连续即可,当对象在堆中没有完成内存分配,并且堆无法再扩展时,会抛出 OutOfMemoryError 异常。

方法区

方法区也是各个线程共享的,它被用于存储已被虚拟机加载的类信息、常量、静态变量、方法数据、方法代码等数据,当方法区无法满足内存分配需求时,抛出 OutOfMemoryError 异常。

运行时常量池

运行时常量池是方法区的一部分。Class 文件中除了有类的版本信息、字段、方法、方法数据、构造函数、接口等信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,常量池的信息会在类加载后进入方法区的运行时常量池中存放(注意区分运行时常量池与 Class 文件的常量池信息)。

虚拟机对于 Class 文件的每一部分的格式都有严格的规定,每一个字节用于存储哪些数据都必须符合规范才能被虚拟机装载和执行,但是对于运行时常量池,Java 虚拟机规范没有做出细节要求。

运行时常量池相对于 Class 文件常量池的一个重要特征就是具有动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入。这一特性利用最多的便是 String 类的 intern() 方法。

直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区域的一部分,也不是 Java 虚拟机规范中定义的内存区域,但是这部分内存被频繁使用,并且也可能导致 OutOfMemoryError

在 JDK 1.4 加入的 NIO 中,引入了一种基于通道(Channel)与缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作,这样就避免了在 Java 堆和 Native 堆中来回复制数据。

本机直接内存的分配不受 Java 堆大小的限制,但是肯定还是会受到本机总内存(RAM、SWAP 区或者分页文件)大小以及处理器寻址空间的限制。