概述

  JVM在执行Java程序的过程中会把它所管理的内存区域分为多个数据区域,各个区域都其特定的用途,有的区域是线程共享的,有的区域是线程独有的。本文我们就根据JVM对内存的划分区域,对每一块区域的作用和概念做一个大致的了解。为了保证概念的准确性和描述的准确性,本文中的部分概念摘自《深入理解JVM(第二版)》,再加上一些自己的理解。根据Java虚拟机规范的规定,Java虚拟机所管理的内存包括了下图中的几个运行时数据区。(注:图中区域显示的大小,不代表实际的JVM中区域的大小比例)

程序计数器(Program Counter Register)

  程序计数器是一块较小的内存空间,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。我认为它和计算机组成原理中的程序计数器作用完全是类似的,比如我们的程序中,需要执行循环,或者分支跳转,那么操作系统如何来判断应该如何循环,如何跳转,其实就是依赖于程序计数器的。JVM中的这个程序计数器,原理也是一样的。
  Java虚拟机的多线程是通过线程轮流切换去获得处理器并执行的方式来实现的,在任何一个确定的时刻,一个处理器(对多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能回到正确的位置,每条线程都需要一个独立的计数器,线程之间互不影响,独立存储。所以程序计数器是一个“线程私有”的内存区域。
  如果一个线程正在执行的是一个普通的Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果是在执行一个Native方法,则这个计数器值为空(Undefined)。此内存区域是JVM唯一没有规定任何OutOfMemoryError的区域。

Java虚拟机栈(VM Stack)

  Java虚拟机栈(Java Virtual Machine Stacks)也是一个线程私有的区域,生命周期与线程相同。它描述的是Java方法执行的内存模型:每个方法在执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个Java方法调用直到完成,就对应这栈帧在虚拟机中入栈到出栈的过程。这里的VM Stacks就是我们平时通常所说的栈,或者说是栈中的局部变量表部分。
  局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、long、double、int、float)、对象引用(refence类型)。其中64位长度的long和double占2个局部变量空间(slot),其余数据类型只占1个,局部变量表所需的内存空间在编译期间完成,当一个方法进入时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小。这个区域定义了StackOverflowError和OutOfMemoryError异常。

本地方法栈(Native Method Area)

  本地方法栈与虚拟机栈的作用是基本一样的,它们之间的区别不过时虚拟机栈为虚拟机执行Java方法服务,而本地方法栈是为虚拟机使用到的Native方法服务。看Java的源码能看到一些native关键字,native方法主要用于加载文件和动态链接库,被native修饰的方法可以被其他语言(比如C语言)。Java程序中声明native修饰的方法,类似于abstract修饰的方法,只有方法名,没有方法实现。这个区域定也有StackOverflowError和OutOfMemoryError异常。

堆(Heap)

  Java堆是虚拟机所管理的内存中最大的一块,它是被所有线程共享的区域,在虚拟机启动时创建。堆区域的唯一目的就是存放对象实例和数组,在Java虚拟机规范中描述的是:The heap is the runtime data area from which memory for all class instances and arrays is allocated.
  Java堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以Java堆还可以被细分为:新生代,老年代,永久代(Java8移除了永久代)。其中新生代还可以分为:伊甸区(Eden),幸存区(From Survivor,To Survivor)。无论怎么划分,与存放的内容都没有关系,存储的依然是对象实例和数组,细致的划分只是为了更好的进行回收内存。后面的章节会对内存的分配和回收作详细说明,这里只是先描述一下对内存的大致划分。
  Java堆可以处理物理上不连续的空间,只要逻辑上连续即可,在实际的应用中,我们还可以通过-Xmx-Xms来控制堆的大小,eclipse的.ini配置文件中可以进行配置。此区域会抛出OutOfMemoryError异常。

方法区(Method Area)

  方法区也是线程共享的区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、以及编译后的代码等数据。此区域会抛出OutOfMemoryError异常。

运行时常量池(Runtime Constance Pool)

  运行时常量池是方法区的一部分(图中未画出),Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号引用,这个部分内容将在类加载后进入方法区的运行时常量池中存放。作为方法区的一部分,这个区域也会抛出OutOfMemoryError异常。

直接内存(Direct Memory)

  直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域。但是这部分内存由于使用频繁且会抛出OutOfMemoryError异常,就一块说了。在JDK1.4中加入了一种基于通道(Channel)与缓冲区(buffe)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作用于这块内存的引用进行操作。这样能在一些场景中提升性能,因为避免了再Java堆和Native堆中来回复制数据。

总结

  本文主要说明了虚拟机运行时数据区的各个区域及其作用,从概念上说明了虚拟机运行时的内存划分。下一篇打算学习一下对象的创建与访问定位。

参考

1.《深入理解JVM(二)》