JVM运行时内存区域

一、运行时数据区域

Java虚拟机在执行Java程序的过程中会将它所管理的内存划分为数个不同的数据区域。根据《Java虚拟机规范(Java SE 7版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:程序计数器(Program Counter Register)、Java虚拟机栈(Java Virtual Machine Stacks)、本地方法栈(Native Method Stacks)、Java堆(Java Heap)、方法区(Method Area)。如图所示:

jdk1.7jvm内存模型

ps.为了线程切换后能恢复到正确的位置,每条线程都需要有一个独立的程序计数器,
各条线程之间计数器互不影响,独立存储,程序计数器器内存区域为 线程私有 的。

1.程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。程序计数器是线程私有的。

其功能类似于汇编语言中的程序计数器,在虚拟机的概念模型中(仅是概念模型,不同的虚拟机可能会通过更高效的方式去实现),字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,如:分支、循环、跳转、异常处理、线程恢复等基础功能。

JVM规范中规定,如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器为空(Undefined)。因为程序计数器中存储数据所占的空间不会随着程序的执行而改变其大小,所以此内存区域是Java虚拟机规范中唯一一个没有规定OutOfMemoryError情况的区域。

(汇编语言中的程序计数器是CPU中的寄存器,计数器的值指向下一条将要执行的指令地址。)

2. Java虚拟机栈

将Java内存区域划分成堆内存(Heap)和栈内存(Stack)是一种较为流行的划分方式,原因在于这两块与对象内存分配关系最为密切,然而这种划分方式并不精确。其中所指的栈即为Java虚拟机栈 (Java Virtual Machine Stacks) 。

与程序计数器相同,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame),栈帧包含了:局部变量表操作数栈动态链接方法出口等信息。每一个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

1)局部变量表
局部变量表用于存放编译期可知的各种基本数据类型(boolean、byte、char、short、int、long、float、double)、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。局部变量表的大小在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是确定的,方法运行期间局部变量表的大小不会改变。

2)对象引用
对于引用类型 的变量,仍然存放与局部变量表中,然而存储的并非对象本身,可能是指向对象起始地址的指针,也可能是其他与此对象 相关的位置。

3)操作数栈
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。
简单的来说,程序中的所有计算过程都是在借助于操作数栈来完成的。

4)动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接 。

5)方法出口
当每一个方法执行完毕时,必须返回到调用它的位置,因此需要在栈帧中保存一个方法的出口地址。因为不同的方法可能通过不同的线程执行,所以每个线程都需要一个自己的Java栈,故此Java虚拟机栈是线程私有的 。

当一个线程开执行一个方法是,就会创建一个栈帧同时进行压栈,当方法执行完毕时弹栈,所以当前执行的方法的栈帧必然处于Java栈的栈顶。

在Java虚拟机规范中,对这个区域规定了两种异常:
·如果线程请求的栈深度超过虚拟机所需深度,将抛出StackOverFlowError异常 。
·如果虚拟机可以动态扩展,但扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常 。

3.本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈作用类似。它们的区别在于,虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈则为虚拟机用到的Native方法服务。(Sun HotSpot虚拟机将本地方法栈和虚拟机栈合二为一)

与虚拟机栈一样,本地方法栈也会抛出StackOverFlowError异常和OutOfMemoryError异常。

Java堆

上文提到简单的Java内存区域划分中的堆内存,指的就是这里要介绍的Java堆(Java Heap)。

对于大多数应用来说,Java堆是Java虚拟所管理的内存中最大的一部分。Java堆是被所有内存共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都要在这里分配内存。

The Heap is the runtime data area from which memory for all classs instances and arrays is allocted.
(所有的对象实例及数组都要在堆上分配)

《Java虚拟机规范》

Java堆是垃圾收集器管理的主要区域,因此又被称作“GC堆”(Garbage Collected Heap)。从内存回收的角度,由于现在收集器基本都采用分带收集算法,所以Java堆中还可以细分为:新生代、老年代、永久代(Hotspot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代实现方法区) 。从内存分配角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。

然而无论如何划分,无论哪个区域,存储的都是对象实例,进一步划分是为了更好的回收内存,或者更快的分配内存。

根据Java虚拟机规范规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,在实现时可以使固定的,也可以是可扩展的。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

5.方法区

方法区(Metohd Area)与Java堆一样是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类的信息、常量、静态变量、及时编译器编译后的代码等数据。虽然Java虚拟机规范将它描述为堆的一个逻辑部分,但是它却有一个别名Non-Heap(非堆)。

上文提及“Hotspot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代实现方法区”,但是永久代并不等于方法区(永久代只是一种具体的实现,对于其他虚拟机不存在永久代的概念),这种设计方法并不好,因为更容易遇到内存溢出问题,而且有极少数方法会因此导致不通虚拟机下有不同表现。

除了和Java堆一样不需要连续的内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾收集,但并非进入了方法区的数据就是“永久”存在的。这一区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

Java虚拟机规范规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

·在已发布的jdk1.7的HotSpot中,已经把原本永久代的字符串常量池移出。

·为了解决永久代造成的内存泄漏并且更好的融合HotSpot JVM和JRockit VM,在jdk1.8中元数据区取代了永久代。

6.运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性 ,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量池放入池中。

作为方法区的一部分,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

·jdk1.3~1.6常量池位于方法区中
·jdk1.7常量池位于堆内存中
·jdk1.8常量池位于元数据空间中

7.直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区域的一部分,也不是Java虚拟机规范中定义的内存区域。显然本机直接内存不会受到Java堆大小的限制,但是当服务器管理员配置虚拟机参数时,如果各内存区域总和大于物理内存限制,会导致动态扩展时出现OutOfMemoryError异常。