程序计数器:
程序计数器是当前线程正在执行的字节码行号指示器。Java 虚拟机在多线程环境下,会通过轮流切换并分配处理器执行时间来完成线程调度。对于多核处理器来说,每个核心在同一时刻通常只执行一个线程的指令。为了让线程切换后能够准确恢复到之前的执行位置,每个线程都需要拥有独立的程序计数器,因此它属于线程私有内存,线程之间互不影响、独立存储。程序计数器也是虚拟机规范中唯一没有规定 OOM 的内存区域。
虚拟机栈:
虚拟机栈是线程私有的内存区域,主要用于支持 Java 方法执行,其生命周期与线程保持一致。它描述的是 Java 方法运行过程中所依赖的内存结构:每当一个方法开始执行时,都会创建一个栈帧,用于保存局部变量表、操作数栈、动态链接、方法出口等信息。方法从调用到执行结束,对应的就是栈帧在虚拟机栈中完成入栈和出栈的过程。该区域可能出现两类异常:如果线程请求的栈深度超过虚拟机允许的最大深度,就会抛出 StackOverflowError;如果虚拟机支持动态扩展,而在扩展时无法申请到足够的内存,则会抛出 OOM。当前大多数 Java 虚拟机都支持动态扩展,但规范也允许固定长度的虚拟机栈。
本地方法栈:
本地方法栈与虚拟机栈的作用非常相似,主要为虚拟机执行 Native 方法提供支持。Java 虚拟机规范对本地方法栈使用的语言、实现方式和数据结构并没有强制要求。Sun HostSpot 虚拟机将虚拟机栈和本地方法栈合二为一,因此本地方法栈区域同样可能抛出 StackOverflowError 和 OOM 异常。
Ja va堆:
Java 堆是所有线程共享的内存区域,会在虚拟机启动时创建。它的主要作用就是存放对象实例,通常情况下,几乎所有对象实例和数组都会在堆上分配内存。对于绝大多数应用来说,Java 堆都是虚拟机中最大的一块受管内存,同时也是垃圾收集器重点处理的区域,因此也常被称为 GC 堆。
从内存回收角度看,现代垃圾收集器通常采用分代收集算法,所以堆还可以进一步划分为新生代和老年代;如果继续细分,还包括 Eden 空间、From Survivor 空间和 To Survivor 空间。
从内存分配角度看,虽然线程共享同一个 Java 堆,但其中也可能划分出多个线程私有的分配缓冲区。不过,无论如何划分,都不会改变它存放对象实例的本质。这样设计的目的,主要是为了提升内存回收效率,或者加快内存分配速度。
堆的容量可以是固定的,也可以按需扩展,通常可通过 Xmx 和 Xms 参数进行控制。如果堆中已经没有足够内存完成实例分配,并且也无法继续扩展,就会抛出 OOM 异常。
方法区:
方法区和 Java 堆一样,属于所有线程共享的内存区域。它用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在虚拟机规范中,方法区被描述为堆的一个逻辑部分,别名是“非堆”。对于习惯在 HotSpot 虚拟机上开发和部署的开发者来说,很多人会把方法区称为“永久代(Permanent Generation)”,但两者本质上并不等价。永久代存在 -XX:MaxPermSize 上限,容易导致内存溢出;而当方法区无法满足内存分配需求时,也会抛出 OOM 异常。
运行时常量池:
运行时常量池是方法区的一部分。class 文件中除了类的版本、字段、方法、接口等信息外,还有一项常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用。这部分内容在类加载后会进入方法区的运行时常量池中保存。运行时常量池同样受到方法区内存大小的限制,当常量池无法申请到足够内存时,就会抛出 OOM 异常。
直接内存:
直接内存不属于运行时数据区,也不受 Java 堆内存限制,但会受到本机总内存限制,同样可能抛出 OOM 异常。
