JVM 内存划分

Java 虚拟机内存规范所管理的内存包括如下运行时数据区域:程序计数器、虚拟机栈、本地方法栈、Java 堆、方法区等。

程序计数器(Program Counter Register)

可以看做是当前线程所执行的字节码行号指示器,每条线程都有一个独立的程序计数器(线程私有),此内存区是 Java 虚拟机规范中唯一没有任何 OutOfMemoryError 情况的区域。

Java 虚拟机栈(Java Virtual Machine Stack)

线程私有,描述 Java 方法执行的内存模型:每个方法执行时,都会创建一个栈帧用于存放局部变量表、操作数栈、动态链接、方法出口等信息。既每一个方法的执行到完成的过程,对应着栈帧的入栈和出栈的过程。

局部变量表

局部变量表存放了编译器可知的各种基本数据类型、对象引用和 returnAdress 类型

  • 其中 double 和 long 类型的数据会占用 2 个局部变量空间(slot),其余的类型均占用 1 个。
  • 局部变量表的空间在编译器即完成分配,大小完全确定,方法运行期间不会改变局部变量表的大小。

两种异常情况

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverFlowError 异常。

如果虚拟机栈可以动态扩展(大部分 Java 虚拟机都可以),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

本地方法栈(Native Method Stack)

与虚拟机栈的发挥作用类似,区别如下:

  • 虚拟机栈为虚拟机执行 Java 方法(字节码)服务。
  • 本地方发栈为虚拟机使用到的 Native 方法服务。

本地方法栈在不同的虚拟机有不同的实现方式,甚至 Sun HotSpot 虚拟机将本地方法栈和虚拟机栈合二为一。

本地方法栈同样会出现虚拟机栈存在的两种异常情况。

Java 堆(Java Heap)

线程共享,在虚拟机启动时创建,唯一目的是存放对象实例。几乎所有的对象实例都在堆上分配。Java 堆是 GC 的主要区域,因此也常称为 “GC 堆”。

  • 从垃圾回收的角度看,由于现在的 GC 基本都采用分代的收集算法,因此还可以将 Java 堆细分为:新生代老年代。再细致划分的话,有:Eden 空间、From Survivor 空间、To Suvivor 空间。
  • 从内存分配的角度看,线程共享的 Java 堆还可能划分成多个线程私有的分配缓冲区(TLAB)。

但无论如何划分,无论哪个区域,Java 堆中存储的都是对象实例,进一步划分是为了更好的回收和分配内存。

Java 堆要求逻辑上连续,但物理上可以不连续。主流虚拟机在 Java 堆的实现上大多采用可以扩展(通过 - Xms 和 - Xmx 控制)的实现。

如果 Java 堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

方法区(Method Area)

线程共享,用于存储已被 Java 虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java 虚拟机规范将其描述为堆的一个逻辑部分,但它却有个别名叫做 Non-Heap(非堆),目的将其与 Java 堆分开。

  • 对于 HotSpot 虚拟机来说,通常将方法区成为 “永久代”(Permanent Generation),但这种描述并不准确,只是因为 HotSpot 的设计团队将 GC 分代算法扩展到了方法区,目的是省去专门为方法区编写内存管理代码的工作。对于其它虚拟机来说是不存在永久代的概念的。通常在方法区发生的 GC 是比较少的,这区域的主要目标是针对常量池的回收和对类型的卸载。
  • HotSpot 团队官方发布的路线图信息,现在也有放弃永久代并逐步改为 Native Memory 来实现方法区的规划。在 JDK1.7 的 HotSpot 中,已经把原本放在永久代中的字符串常量池移出。

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

运行时常量池(Runtime Constant Pool)

运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息以外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。

  • 对于运行时常量池,Java 虚拟机规范中没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。不过,一般来说,除了存储 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
  • 运行时常量池相对于 Class 文件常量池的另外一个重要特性是具备动态性。Java 语言不要求敞亮一定要在编译期产生,也就是说并非一定要预置入 Class 文件中常量池的内容才能进入运行时常量池,运行期间也可能将新的敞亮放入池中,这种特性被开发人员利用的比较多的便是 String 类的 intern () 方法。

当运行时常量池无法满足内存的分配需求,将会抛出 OutOfMemoryError 异常。

直接内存(Direct Memory)

直接内存不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中的内存区域,但却被频繁使用,也有可能导致 OutOfMemoryError 异常。

在 NIO 中,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,可以利用 Native 函数库直接分配堆外内存,通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,避免了在 Java 堆和 Native 堆中来回复制数据。

直接内存不受到 Java 堆大小的限制,但是仍然受到本机 RAM 和 SWAP 区(或分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,常常忽略直接内存而设置 - Xmx 等信息,终而导致各个内存区的总大小大于物理内存限制,从而导致动态扩展时出现的 OutOfMemoryError 异常。