Java虚拟机内存模型

89 阅读7分钟

一、背景

关于Java虚拟机内存模型,网上有很多的描述,但是有很多旧的或者不准确的解释,于是便查找了oracle官方的虚拟机规范
链接如下:
docs.oracle.com/javase/spec…
此文档更新于2023.3
以下内容均翻译自其中的<运行时数据区>的内容

二、概述

Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在Java虚拟机启动时创建的,并且仅当Java虚拟机终止时才被销毁。其他数据区域是每个线程专有的。每个线程专有的数据区域是在线程创建时创建并在线程终止时销毁。

2.1 程序计数器

Java虚拟机可以同时支持多个执行线程。每个Java虚拟机线程都有自己的pc(程序计数器)寄存器。在任何时刻,每个Java虚拟机线程都在执行单个方法的代码,即该线程的当前方法。如果该方法不是本地方法,则pc寄存器包含当前正在执行的Java虚拟机指令的地址。如果由线程执行的方法是本地方法,则Java虚拟机的pc寄存器值未定义。 Java虚拟机的pc寄存器宽度足以容纳特定平台上的返回地址或本地指针。

2.2 Java 虚拟机栈

每个Java虚拟机线程都有一个私有的Java虚拟机栈,与线程同时创建。每个栈Java虚拟机栈都存储着若干栈帧。Java虚拟机栈类似于传统语言(如C语言)的堆栈:它保存局部变量和部分结果,并在方法调用和返回中发挥作用。由于除了入栈和出栈操作之外,从不直接操作Java虚拟机堆栈,因此可以将栈帧分配到堆上。Java虚拟机堆栈的内存不需要是连续的。

在《Java虚拟机规范》第一版中,Java 虚拟机堆栈被称为 Java Stack。

本规范允许 Java 虚拟机堆栈具有固定大小或按需动态扩展和收缩。如果 Java 虚拟机栈具有固定大小,那么每个Java虚拟机堆栈的大小可以在创建堆栈时独立选择。

Java虚拟机实现可以为程序员或用户提供对Java虚拟机堆栈初始大小的控制,以及在动态扩展或收缩Java虚拟机堆栈的情况下,对最大和最小大小的控制。

以下是与Java虚拟机堆栈相关的异常情况:

  • 如果线程中的计算需要比允许的Java虚拟机堆栈更大,则Java虚拟机会抛出StackOverflowError。
  • 如果可以动态扩展Java虚拟机堆栈,并且尝试进行扩展但无法提供足够的内存来实现扩展,或者无法提供足够的内存来为新线程创建初始Java虚拟机堆栈,则Java虚拟机会抛出OutOfMemoryError。

2.3 堆区

Java虚拟机具有一个堆,该堆在所有Java虚拟机线程之间共享。 堆是运行时数据区域,用于分配所有类实例和数组的内存。

堆在虚拟机启动时创建。对象的堆存储由自动存储管理系统(称为垃圾收集器)回收; 对象从未明确地释放。 Java虚拟机不假定任何特定类型的自动存储管理系统,并且可以根据实现者的系统要求选择存储管理技术。 堆可以是固定大小或根据计算需要扩展,并且如果不需要更大的堆,则可以缩小。 堆的内存不需要连续。

Java虚拟机实现可能会提供程序员或用户对堆初始大小以及是否可以动态扩展或缩小以及最大和最小堆大小进行控制。

以下异常条件与堆相关:

  • 如果计算所需的比自动存储管理系统可用空间更多,则Java虚拟机会抛出OutOfMemoryError。

2.4 方法区

Java虚拟机具有一个方法区,该区域在所有Java虚拟机线程之间共享。方法区类似于传统语言的编译代码存储区或操作系统进程中的“text”段(即代码段)。它存储每个类的结构,例如运行时常量池、字段和方法数据以及用于类和接口初始化以及实例初始化的特殊方法。

方法区在虚拟机启动时创建。尽管方法区在逻辑上是堆的一部分,但简单实现可能选择不对其进行垃圾回收或压缩。此规范不强制执行方法区位置或管理编译代码所使用的策略。如果计算需要,则可以根据需要扩展固定大小或可变大小,并且如果不再需要更大的方法区,则可以将其收缩。 方法区内存无需连续。

Java虚拟机实现可以为程序员或用户提供控制初始大小以及在变化大小情况下控制最大和最小值。

以下异常条件与方法区相关:

  • 如果无法提供内存来满足分配请求,则Java虚拟机会抛出OutOfMemoryError异常

2.5 运行时常量池

运行时常量池是类文件中 constant_pool 表的每个类或接口的运行时表示。它包含多种类型的常量,从编译时已知的数字字面值到必须在运行时解析的方法和字段引用。运行时常量池类似于传统编程语言中符号表的功能,尽管它包含比典型符号表更广泛范围的数据。

每个运行时常量池都分配自 Java 虚拟机方法区 。当 Java 虚拟机创建一个类或接口时,就会构造该类或接口对应的运行时常量池。

以下异常情况与为某个类或接口构造其对应的运行时常量池相关:

  • 如果创建一个类或接口需要构建其对应的运行时间常数池所需内存超过了 Java 虚拟机方法区可提供内存,则 Java 虚拟机将抛出 OutOfMemoryError 异常。

2.6 本地方法栈

Java虚拟机的实现需要使用传统堆栈(俗称“C堆栈”)来支持本地方法(用其他语言编写的方法,而非Java编程语言)。本地方法栈也可被用于在诸如C之类的语言中实现Java虚拟机指令集解释器。不能加载本地方法且不依赖于传统堆栈的Java虚拟机实现无需提供本地方法栈。如果提供了,则通常在创建每个线程时为其分配一个本地方法栈。

该规范允许将本地方法堆栈设置为固定大小或按需要动态扩展和收缩。如果本地方法堆叠是固定大小的,则可以在创建该堆叠时独立选择每个原生堆叠的大小。 Java虚拟机实现可以使程序员或用户控制原生方式堆叠的初始大小以及对于变化尺寸原生方式堆叠最大和最小尺寸进行控制。

以下异常条件与原生方式垛有关:

  • 如果线程中计算所需求比允许更大的原始方式垛,则Java虚拟机会抛出StackOverflowError异常。
  • 如果可以动态扩展并尝试扩展但无法获得足够内存,或者无法获得足够内存以为新线程创建初始本地方法堆栈,则Java虚拟机会抛出OutOfMemoryError异常。

总结

以上是官网文档中关于虚拟机内存模型的描述,总结一下,按照线程共享与否来划分,可以为:

  • 线程间共享的区域:

    • 堆区
    • 方法区
    • 运行时常量区
  • 线程独有的区域:

    • 程序计数器
    • 虚拟机栈
    • 本地方法栈

关于异常处理:

  • 虚拟机栈和本地方法栈都可能抛出 栈溢出 和 OOM 异常
  • 堆区、方法区、运行时常量区 可抛出 OOM 异常
  • 程序计数器不抛出任何异常