运行时数据区域
Java 虚拟机运行时区域主要由:程序计数器、虚拟机栈、本地方法栈、堆和方法区组成,其中堆和方法区是所有线程共享的数据区域,程序计数器、虚拟机栈和本地方法栈是线程隔离的。
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器。通过改变程序计数器的值,来确定下一条需要执行的字节码指令。程序中的分支、循环、跳转、异常处理、线程恢复等都是通过修改程序计数器的值来完成的。
如果线程执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的本地方法(Native Method),这个计数器的值则为空(Undefined)。此内存区域是唯一一个在 Java 虚拟机规范没有规定任何 OutOfMemoryError 情况的区域。
虚拟机栈
Java 虚拟机栈(Java Virtual Machine Stacks)描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈,动态链接、方法出口信息等。每个方法从调用到执行完成的过程,对应一个栈帧在虚拟机栈从入栈到出栈的过程。
虚拟机栈中规定两种异常:如果线程请求的栈深度大于虚拟机运行所允许的深度,会抛出 StackOverflowError 异常;如果虚拟机栈在动态扩展(现在大部分虚拟机都可以动态扩展,也允许固定长度)中无法申请到足够的内存,会抛出 OutOfMemoryError 异常。
本地方法栈
本地方法栈(Native Method Stack)和虚拟机栈的作用相似,本地方法栈是为 Native 方法服务的。此区域也会抛出 StackOverflowError 和 OutOfMemoryError 两种异常
堆
Java 堆(Java Heap)是 Java 虚拟机管理的内存中最大的一块,堆被所有的线程共享,在虚拟机启动的时候创建。几乎所有的对象实例和数组都要在堆上分配。堆是 GC 管理的主要区域。
从内存回收的角度,由于现在收集器基本都是采用分代收集的算法,所以 Java 堆还可以细分为:新生代(Young Generation)和老年代(Old Generation)。再细分一点的可以分为:Eden 空间、From Survivor(S0) 空间和 To Survivor(S1) 空间等。
从内存的分配角度,线程共享的 Java 堆可能分配出多个线程私有的本地线程分配缓冲区(Thread Local Allocation Buffer, TLAB)。
如果在堆中没有内存完成实例分配,并且堆也无法扩展时,会抛出 OutOfMemoryError。
在默认情况下,Eden 空间和每个 Survivor 空间的大小比例为 8/1,可以通过 JVM 参数
-XX:SurvivorRatio
来调节 Eden 和 Survivor 的空间分配。
在新生代发生的 GC 称为 Minor GC,在老年代发生的 GC 称为 Major GC。
方法区
方法区(Method Area)和堆一样,也是线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等。虽然 Java 虚拟机规范中把方法区描述为堆的一个逻辑部分,但是它有一个别名叫 Non-Heap(非堆),和 Java 堆区分开来。
在 HotSpot 虚拟机中,方法区又常被称为永久代(Permanent Generation)。
方法区的内存回收目标主要针对常量池的回收和对类型的卸载。
运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有还有一项是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区运行时常量池存放。
当常量池无法再申请到内存的时候,就会抛出 OutOfMemoryError 异常。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区域的一部分,但是这部分经常会导致 OutOfMemoryError 异常出现。
在 JDK 1.4 中心加入 NIO(New Input/Output)类,引入了基于通道(Channel)和缓冲区(Buffer)的 I/O 方式,可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样在一些场景中性能显著提高,因为避免了在 Java 堆和 Native 堆中来回复制数据。
要注意的是,在 JVM 设定 -Xmx 等参数信息的时候,是不包含直接内存的。而如果按照物理机内存设定堆内存,而不考虑直接内存时,导致 JVM 设定的总内存大于物理机内存,从而在动态扩展时会抛出 OutOfMemoryError 异常。