[Java基础] Java 内存是如何进行分区的?


#1

Java 中内存都有哪些区域?

我们以 Java 8 为例说明,下图先介绍一下 Hotspot 虚拟机的的架构图:

图中可以见到,在 JVM 中运行时的数据区域包括很多种。这些数据区域有的是在 JVM 启动时就创建好,并且一直持续到 JVM 退出;另外一些是每个线程私有的,这部分数据区域随着线程的创建而创建,随着线程的退出而销毁。

1. 程序计数器 (Program Counter Registers)

JVM 支持多个线程同时运行,当每一个新线程被创建时,它都将得到它自己的程序计数器。在任意时刻,每个 JVM 线程都只在执行一个方法,称为线程的当前方法。如果线程正在执行的是一个Java方法(非native),那么程序计数器的值总是指向当前被执行的指令(即内容为指令的地址),如果方法是 native的,程序计数器寄存器的值为空。 JVM的程序计数器寄存器的宽度足够保证可以持有一个返回地址或者native的指针。

在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

程序计数器是线程私有的,也就是说每个线程创建后都将拥有一个程序计数器。此内存区域是唯一一个在java虚拟机规范中没有规定任何 OutOfMemoryError 的区域。

2. 虚拟机栈 (Java Virtual Machine Stacks)

当每个 JVM 线程创建时都会同时被分配一个私有的 JVM 栈。JVM 栈中存储的内容为栈帧,包括局部变量表、方法参数和中间结果,在方法调用和返回中起着关键的作用。每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

虚拟机栈所占用的内存不要求是连续的。JVM 规范允许虚拟机实现固定大小或是可根据计算动态调整大小的虚拟机栈。如果虚拟机栈是固定大小的,每个线程所对应的栈可以独立选择初始大小。

虚拟机栈相关的异常条件包括以下两种:

  • 当线程的运行需要比允许的虚拟机栈更大的空间时,JVM 会抛出 StackOverflowError
  • 如果虚拟机栈可以动态扩展空间,当某一次尝试扩展因为没有足够内存而失败时,或者当创建新线程初始化虚拟机栈时没有足够的内存,JVM 会抛出 OutOfMemoryError

3. JVM 堆 (Heap)

Java 虚拟机的堆是指所有 JVM 线程都会共享的运行时数据区域。所有的类实例和数组都会分配在这个区域。

堆由 JVM 在启动时创建。堆中存储的对象由垃圾回收器 GC 进行自动管理和回收;对象所占用的存储空间不能进行显式释放。JVM 虚拟机本身并不强制使用某种 GC 类型,不同的 JVM 实现可以根据系统需要自由选择不同的 GC;同一个 JVM 实现也可以根据不同的系统需求指定不同的 GC。堆的大小可以是固定的,也可以是根据运行时需要自动计算并进行扩展或收缩。堆内存也不需要是物理连续的。

现代的垃圾收集器基本都采用了分代收集算法,所以堆也可以进一步划分为新生代和老年代等,本文就不展开 GC 相关的进一步堆空间的划分细节了,有时间再撰文详述。

还有一点需要注意的是,堆中存放的对象的引用是在虚拟机栈中,而不是存在堆中。

当堆中没有可用内存完成实例的分配,并且堆也无法再扩展时,JVM 将会抛出 OutOfMemoryError

4. 方法区 (Method Area)

方法区是 JVM 中所有线程共享的一个区域,用于存放已被虚拟机加载的类信息,例如运行时常量池、类成员和方法,以及方法和构造器的代码等。

方法区随着虚拟机的启动而创建。虽然方法区在逻辑上属于堆的一部分,但简单的实现可以选择不对它进行垃圾回收或压缩。JVM 规范不要求方法区的位置和用来管理编译代码的策略。同样地,方法区可以是固定大小或按需扩大或收缩的动态大小区域。方法区所占用空间不需要是连续的。

当方法区不能再满足新的分配请求时,JVM 抛出 OutOfMemoryError

5. 运行时常量池 (Runtime Constant Pool)

运行时常量池属于方法区的一部分,这里单拿出来描述一下。它是类文件中的常量表在运行时的表示,包括各种常量,如编译时数字常量和必须在运行时解析的方法和成员引用。每个类或接口都有一个专属的运行时常量池,它们在相应的类或接口创建的时候由虚拟机在方法区分配空间。

当创建类或接口时,如果在构造运行时常量池时虚拟机无法再在方法区获得可用内存,则会抛出 OutOfMemoryError

6. 本地方法栈 (Native Method Stacks)

本地方法栈和 JVM 栈类似,虚拟机实现可以选择使用本地方法栈来实现 native 方法。本地方法栈通常在每个线程创建时分配。

同虚拟机栈类似地,本地方法栈相关的异常条件包括以下两种:

  • 当某个线程的运行需要比允许的空间更大的本地方法栈时,JVM 将抛出 StackOverflowError;
  • 当本地方法栈可以动态扩展空间,且某一次尝试扩展因为没有足够内存而失败时,或者当创建新线程初始化本地方法栈时没有足够的内存,JVM 会抛出 OutOfMemoryError

参考文献