程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
在虚拟机的概念模型中(不同的虚拟机实现可能会不同),字节码解释器工作时就是通改变程序计数器的值来选取下一条要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器来完成。
由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。因此程序计数器是线程私有的。
线程私有,生命周期与线程相同。虚拟机栈描述的是Java方法的内存模型:在每个方法执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
在Java虚拟机规范中,对这个区域规定了两种异常状况,如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError异常,如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够内存,就会抛出OutOfMemoryError异常。
我们常用来表示java内存划分简化的“堆”、“栈”中所说的“栈”既是虚拟机栈。
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。
在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。(最大Slot数量)
一个局部变量可以保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。reference类型表示对一个对象实例的引用。returnAddress类型是为jsr、jsr_w和ret指令服务的,目前已经很少使用了。
虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储。
操作数栈(Operand Stack)也常称为操作栈,它是一个后入先出栈(LIFO)。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。
操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。
当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。
在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)。
这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接。
当一个方法开始执行时,可能有两种方式退出该方法:
无论是Java虚拟机抛出的异常还是代码中使用athrow指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会导致方法退出。
无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。
方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。
一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。
本地方法栈( Native Method Stack )与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。Java堆是垃圾回收器管理的主要区域,因此也被称为“GC堆”(Garbage Collected Heap)。Java堆可以使物理上不连续的内存空间,只要逻辑上连续即可。
此内存的唯一目的就是存放对象实例,几乎所有的对象示例都是在这里分配内存。在Java虚拟机规范中:所有的对象示例及数组都要在堆上分配,但随着JIT(Just-In-Time)编译器(即时编译器)的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有对象都分配在堆内存上也逐渐变得不那么“绝对”。
从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java对还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。
从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。不过不论如何划分,都与存放内容无关,无论哪个区域,存储的依然是对象实例,进一步划分的目的是为了更好的回收内存,或者更快分配内存。
根据JVM规范规定,Java堆可以实现成固定大小,也可以是可拓展的,当前主流的虚拟机都是可拓展的(通过-Xmx和-Xms控制)。如果在堆中内有内存完成实例分配,并且对也无法再拓展,将会抛出OOMError异常。
方法区域Java堆一样,是线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它的别名叫做Non-Heap,目的应该是与Java堆区分。
在HotSopt虚拟机上,方法区又被称为“永久代”(Permanent Generation),本质上两者不等价,仅仅是因为HotSpot虚拟机的设计团队使用永久代来实现方法区。这样HotSpot的垃圾收集器可以像管理Java对一样管理这部分内存,能够省去专门为方法区编辑内存管理代码的工作。对于其他虚拟机来说是不存在永久代概念的。
原则上,如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但使用永久代来实现方法区,现在看来并不是一个好主意,因为这样更容易遇到内存溢出问题(永久代有 -XX : MaxPermSize 的上限, J9 和 JRockit 只要没有触碰到进程可用内存的上限,例如 32 位系统中的 4GB ,就不会出现问题),而且有极少数方法(例如 String.intern ())会因这个原因导致不同虚拟机下有不同的表现。因此,对于 HotSpot 虚拟机,根据官方发布的路线图信息,现在也有放弃永久代并逐步改为采用 Native Memory 来实现方法区的规划了 [1] ,在目前已经发布的 JDK 1.7 的 HotSpot 中,已经把原本放在永久代的字符串常量池移出。
Java 虚拟机规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是必要的。在 Sun 公司的 BUG 列表中,曾出现过的若干个严重的 BUG 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。根据 Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池( Constant Pool Table )
运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
直接内存不是迅疾运行时数据区的一部分,也不是Java虚拟机规范重定义的内存区域,但是这部分内存也被频繁使用,也可能导致OOMError。
在 JDK 1.4 中新加入了 NIO ( New Input/Output )类,引入了一种基于通道( Channel )与缓冲区( Buffer )的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。