Java的动态内存分配和垃圾回收机制使java程序员不用像C++程序员那么头疼内存的分配与回收。相信熟悉COM机制的朋友对于引用计数管理内存的方式深有感触。

Java虚拟机的自动内存管理不仅降低了编码的难度而且不容易出现内存泄露和内存溢出的问题。但是这过于美好的愿景正是由于把内存的控制权交给了Java虚拟机,一旦出现内存泄露和溢出,我们就必须翻过Java虚拟机自动内存管理这堵高墙去排查错误。本文简要总结下JVM运行时数据区域的划分、作用以及可能出现的异常。

图1 Java虚拟机运行时数据区

如图1所示,根据《Java虚拟机规范(Java SE 7 Edition)》的规定,Java虚拟机在执行Java程序时,即运行时环境下会把其所管理的内存划分为几个不同的数据区域。有的区域伴随虚拟机进程的启动而创建,死亡而销毁;有些区域则是依赖用户线程的启动时创建,结束时销毁。所有线程共享方法区和堆,虚拟机栈、本地方法栈和程序计数器是线程隔离的数据区。

1.1程序计数器Program Counter Register

由于操作系统通过时间片轮流的多线程并发方式,任何时刻处理器只会处理当前线程的指令。线程间切换的并发要求每个线程都需要有一个私有的程序计数器,程序计数器间互不影响。

程序计数器存储当前线程下一条要执行的字节码的地址,占用内存空间较小。所有的控制执行流程,分支、循环、返回、异常等功能都在程序计数器的指示范围之内,字节码解释器通过改变程序计数器的值来获取下一条要执行的字节码的指令。

1.2 虚拟机栈 VM Stack

简单点说,虚拟机栈就是类中的方法的执行过程的内存模型。虚拟机栈也是线程私有的,并且同线程的生命周期相同。

对于方法的调用,有必要先介绍下栈帧(Stack Frame)。

虚拟机在执行每个方法的调用时会创建一个栈帧的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。每个方法的调用过程,就对应着一个栈帧在虚拟机里的入栈出栈的过程。栈帧包括了方法的局部变量表、操作数栈、动态链接和方法出口等一些额外的附加信息。对于活动线程中栈顶的帧栈,称为当前栈帧,这个栈帧所关联的方法称为当前方法,正在执行的字节码指令都只针对当前有效栈帧进行操作。

在栈帧的基础上,不难理解虚拟机栈的内存结构。Java虚拟机规范规定虚拟机栈的大小是可以固定的或者动态分配大小。Java虚拟机实现可以向程序员提供对Java栈的初始大小的控制,以及在动态扩展或者收缩Java栈的情况下,控制Java栈的最大值和最小值。

以下异常情况与Java栈相关:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,则Java虚拟机将抛出StackOverflowError异常。
  • 如果虚拟机栈可以动态扩展,但是无法申请到足够的内存来实现扩展,或者不能得到足够的内存为一个新线程创建初始Java栈,则Java虚拟机将抛出OutOfMemoryError异常。

1.3本地方法栈 Native Method Stack

本地方法栈内执行的是非Java语言编写的代码,比如C或C++,而虚拟机栈执行的是java方法字节码服务,这是两者最大的区别。本地方法栈的是虚拟机使用本地方法服务的,如果提供本地方法栈,则它们通常在每个线程被创建时分配在每个线程基础上的。

同虚拟机栈一样,本地方法栈也会出现与虚拟机栈类似的异常,也会抛出StackOverflowError和OutOfMemoryError异常。

1.4 Java堆 Java Heap

Java堆是类实例和数组的分配空间,是一块所有线程共享的内存区域。堆在虚拟机启动时创建,是Java虚拟机所管理的内存中最大一块。内存泄露和溢出的问题大都出现在堆区域,由此,Java堆是垃圾回收的主要重点管理区。

从内存回收的角度看,由于现在收集器基本上都是采用的分代收集算法,Java堆还可细分为新生代和老年代;从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓存区。这种进一步的内存划分方式目的是更好地回收内存,或者更快地分配内存。

Java虚拟机规范规定堆在内存单元中只要在逻辑上是连续的即可,Java堆可以是固定大小的,或者按照需求做动态扩展,并且可以在一个大的堆变的不必要时收缩。Java虚拟机的实现向程序员或者用户提供了对堆初始化大小的控制,以及对堆动态扩展和收缩的最大值和最小值的控制。

以下异常情况与Java堆相关:

  • 如果堆中没有可用内存完成类实例或者数组的分配,在对象数量达到最大堆的容量限制后将抛出OutOfMemoryError异常。

1.5 方法区 Mehod Area

方法区在虚拟机启动时创建,也是一块所有线程共享的内存区域。方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。用一句话说就是方法区类似于传统语言的编译后代码的存储区。

虽然Java虚拟机规范在逻辑上把方法区描述为堆的一个部分,但是在垃圾回收方面的限制却比较宽松,宽松到方法区可以不用实现垃圾回收。但是,垃圾回收在方法区还是必须有的,只是回收效果不是很明显。这个区域的回收目标主要针对的是常量池的回收和对类型的卸载。

方法去的大小也可控制,以下异常与方法区相关:

  • 如果方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常。

1.6运行时常量池 Runtime Constant Pool

常量池是每个类的Class文件中存储编译期生成的各种字面量和符号引用的运行期表示,其数据结构是一种由无符号数和表组长的类似于C语言结构体的伪结构,详细内容请参考《Java虚拟机规范第七版》第四章。

常量池也是方法区的一部分,类的常量池在该类的Java class文件被java虚拟机成功地装载时创建,这部分内容在类加载后存放到方法区的运行时常量池中。

运行时常量池属于方法区,自然也受到方法区内存大小的限制,以下异常与常量池有关:

  • 在装载class文件时,如果常量池的创建需要比Java虚拟机的方法区中需求更多的内存时,将会抛出OutOfMemoryError异常。

通过总结,对于虚拟机运行时数据区域的划分及每个区域作用,存储内容及可能出现的异常有了一个大致的了解。Java的自动内存分配和垃圾回收筑起的这道高墙,在出现内存泄漏或者溢出的情况下,这道高墙就必须翻越了。