如下是Java虚拟机运行时数据区的结构图:
Java虚拟机在执行Java程序的时候会将内存划分为方法区、虚拟机栈、本地方法栈、堆、程序计数器。接下来挨个介绍其概念。
-
程序计数器
程序计数器(Program Counter Register,简称PC)是一块较小的内存区域,可以看作当前线程执行的信号指示器。在虚拟机的概念模型中,字节码解释器工作时通过改变PC的值(通常是PC=PC+1)来选取下一条需要执行的字节码指令。
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器或一个内核,只会执行一条线程中的指令,那么为了线程切换后可以恢复到原来的执行位置,每条线程都需要一个独立的PC。这类内存被成为“线程私有”的内存。
如果线程正在执行的是Java方法,PC记录的是正在执行的虚拟字节码指令的地址。若执行的是Native方法,则PC的值为空。该内存是唯一 一个没有规定OutOfMemoryError情况的区域。 -
Java虚拟机栈
这是线程私有的。栈的生命周期和线程的生命周期相同,因为栈记录的是当前线程中每个方法对应的栈帧。一个线程对应一个独有的栈。栈帧随着方法的调用和执行完成,对应着栈帧在栈中的入栈和出栈的操作。每个栈帧记录着对应方法的局部变量表,操作数栈,动态链接,方法出口等信息。
局部变量表存放了编译期可知的各种基本数据类型、对象引用类型(reference类型,指向对象起始地址的引用指针)和returnAddress类型(指向一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配。
在该区域,有两种异常:- 若线程请求的栈的深度大于虚拟机所允许的深度,抛出StackOverflowError
- 若虚拟机栈可以动态扩展,若扩展时无法申请到足够的内存,抛出OutOfMemoryError异常。
-
本地方法栈
和虚拟机栈的作用类似,区别为虚拟机栈执行的是Java方法,而本地方法栈执行的是Native方法。有的虚拟机(譬如Sun 的 HotSpot)将虚拟机栈和本地方法栈合二为一,其抛出的错误也是StackOverflowError和OutOfMemoryError
-
Java堆
Java堆(Java Heap)是被所有线程共享的内存区域。在虚拟机启动时创建,其唯一目的是存放对象实例。
Java堆是垃圾收集器管理的主要区域,被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度看,Java堆细分为新生代和老年代。从内存分配的角度看,Java堆划分出多个线程私有的分配缓存区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都是存放对象实例,划分是为了更方便的回收对象实例或分配内存。
Java堆可以建立在物理上不连续的内存块上,只要是逻辑上连续即可。当前主流的虚拟机是可扩展的,若在堆中没有内存完成实例分配,且堆无法再扩展,抛出OutOfMemoryError。 -
方法区
和堆一样,是被所有线程共享的内存区域。用于存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区的别名叫做“Non-Heap(非堆)”。在HotSpot虚拟机中,方法区也叫“永久代(Permanent Generation)”,因为把GC分代收集扩展到方法区,使得方法区可以被垃圾收集器回收,该区域内存回收的目标是常量池的回收和对类型的卸载。
目前看来,使用永久代来实现方法区,容易造成内存溢出问题,而且有极少数方法(例如String.intern())会因这个原因导致不同虚拟机有不同的表现。在目前JDK1.7的HotSpot中,已把放在永久代中的字符串常量池移除。
当方法去无法满足内存分配的需求时,将抛出OutOfMemoryError。 -
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述外,还包括常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,常量池将在类加载后进入方法区的运行时常量池中存放。除了保存Class文件中描述的符号引用外,还保存着翻译出来的直接引用。
运行时常量池区别于Class文件的常量池的一个特征是动态性。常量不一定在编译期才能产生,在运行期间也可产生,动态产生的常量也可放入运行时常量池。这种特性被利用得较多的是String类的intern()方法。
运行时常量池是方法区的一部分,也会收到方法区内存的限制,当常量池无法在申请到内存时会抛出OutOfMemoryError。 -
直接内存
直接内存(Direct Memory)不是以上虚拟机运行时数据区的一部分,只是这部分内频繁使用,同样可能导致OutOfMemoryError。
在JDK1.4新加入了NIO(New Input/Output)类,引入了基于通道(Channel)和缓冲区(Buffer)的I/O方式。它通过Native函数库直接分配堆外内存,再通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,避免了在Java堆和Native堆中来回复制数据,提高性能。
虽然本机直接内存不会受到Java堆大小的限制,但是会受到本机总内存的限制。所以需要注意的是,在配置虚拟机参数时,不要让设置的各个内存区域综合大于物理内存限制,否则将抛出OutOfMemoryError。
内存管理的各个内存区域就已经介绍完毕了,接下来介绍的是HotSpot虚拟机在Java堆中对象分配、布局和访问的过程。