作为一个 Java 程序员,不了解 Java 内存模型就不能写出能够充分利用内存的代码。本文通过对 Java 内存模型的介绍,让读者能够了解 Java 的内存的分配情况,适合 Java 初学者或者对 JMM 不熟悉的同学。后面的博客会针对每个部分做更加深入的解释。

Java 内存模型

首先通过下图对于 Java 内存模型有一个整体的认识,然后针对不同的区域的作用和存储的内容做进一步的解释。

Java内存模型

PC(程序计数器)

这里的 PC 不是 Personal Computer,而是 Program Counter Register,从名字就可以看出来,这是一个寄存器,用来存储需要执行的指令地址。

程序计数器(Program Counter (PC))是在电脑处理器中的一个寄存器,用来指示电脑下一步要运行的指令序列。—WikiPedia

PC 和其他 JVM 内存区域最大的区别是:

“此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
摘录来自: 周志明. “深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)”。 iBooks.

像上面的图片一样,PC 是每个线程私有的,对于 Java 方法而言,PC 中存储的是正在执行的虚拟机字节码的内存地址;对于 Native 方法来说,PC 中的值为空(Undefined)。

Java 虚拟机栈和本地方法栈

虚拟机栈

无论是在大学的 Java 编程课堂上,还是我们在学习过程编码过程,经常会出现 StackOverFlow,甚至目前最大的技术问答社区的名字也是 StackOverFlow。Java 语言中会产生栈溢出的就是这块内存区域,当你的程序中设置了超过 JVM 规定的递归深度的时候就会触发这个异常。类似 JMM 的其他内存区域,如果虚拟机栈在动态扩展的时候无法申请到足够的内存也会报OOM异常。

Java 语言中每一个方法的执行都对应着一个栈帧(Stack Frame)的创建,栈帧中存储的是局部变量、方法出口等信息,因此对于一个方法的执行而言,所能够使用到的内存是在编译期间就能够完全确定的,在运行期间不会发生变化。在栈帧中,局部变量空间成为 Slot,除了 double 和 long 占有 2 个 slot 外,其他基本数据类型和对象引用都占用 1 个 slot 空间

本地方法栈

本地方法栈和虚拟机栈最大的区别就是虚拟机栈是为执行 Java 字节码服务的,而本地方法栈是为了虚拟机使用到的 Native 方法服务的。除此之外,Java 虚拟机规范并没有针对本地方法栈的实现做具体规定。在 HotSpot 虚拟机中,本地方法栈和虚拟机栈是共用同一块内存的,不做具体区分。同样,本地方法栈也会产生 OOM 异常和 StackOverFlow 异常。

Java 堆

“The heap is the runtime data area from which memory for all class instances and arrays is allocated。” —Java虚拟机规范

Java 虚拟机规范规定所有的实例对象和数组都应该分配到 Java 堆中。
说的通俗一点就是所有 new 出来的对象和数组都会放到该区域,由于现在的收集器都采用分代收集算法,所以在 Java 堆中又分了新生代和老年代,新生代有做了详细的区分。该区域的大小可以通过 JVM 参数 -Xmx-Xms 来设置。

直接内存

在 JDK1.4 中引入了 NIO,可以通过 Native 方法直接在堆外分配内存,然后通过在堆中存储的引用来对这块内存区域做操作。注意 这块区域并不会在 -Xmx-Xms 设置的大小之内,因此在设置 JVM 参数的时候要注意考虑这块内存区域,避免设置的内存区域总额大于物理内存

方法区

Method Area 又叫 NonHeap,也是线程共有的内存区域,用来存:

  • 类信息
  • 常量
  • 静态变量
  • 字符串常量池

在 JDK1.7 中已经将字符串常量池移出永久代,在 Java8 中更是之内取消了永久代,而是使用了元空间(MetaSpace)来存储这些信息,从而永久代的大小不需要再指定,只要不超出物理内存的限制就不会产生 OOM 异常

运行时常量池

运行时常量池主要用来存储类的版本、字段、方法、接口等描述信息。常量池(Constant Pool Table)用来存储各种字面量和符号引用。String 的 intern() 方法就是在运行期间将对象放到常量池中的。此部分也会出现 OOM 异常。

JDK8 以及之后的模型

pic