JVM 内存区域划分

Author: Corissa

Date: Dec 30 2023

1 运行时数据区域


JVM 在执行 Java 程序时会将管理的内存划分成多个不同的数据区域,其区域划分在 JDK1.7和1.8 有所不同(图源 JavaGuide):

Java 运行时数据区域(JDK1.7) Java 运行时数据区域(JDK1.8 )

可以看到在 JDK 1.8 中,运行时常量池被放置到了本地内存中。

这几个区域里,线程私有的有:虚拟机栈、本地方法栈和程序计数器;线程共有的有堆,方法区和直接内存区。

1.1 程序计数器


唯一一个不会出现 OutOfMemoryError 的区域

程序计数器可以看作是程序行号指示器,字节码解释器工作时依靠改变程序计数器来读取下一条需要执行的指令,实现代码流程的控制。

程序计数器是线程私有的,因为在线程切换后,程序需要回到正确的位置执行,为了避免线程之间的相互影响,每个线程有独立的程序计数器,独立存储。

1.2 Java 虚拟机栈


注意对比 StackOverFlowError & OutOfMemoryError

除了一些 Native 方法是通过本地方法栈实现的,其他所有方法都是通过栈来实现的。

Java 虚拟机栈也是线程私有,生命周期随着线程的创建而创建,线程的结束而死亡。每个线程在创建的时候都会创建一个虚拟机栈,虚拟机栈内部保存着一个个栈帧 (Stack Frame),一个栈帧就对应一次方法调用。每一次方法调用,就有一个栈帧被压入栈,每一次方法调用结束,又会有一个栈帧被弹出。其具体组成如下:

Java 虚拟机栈

一个栈帧里包括局部变量表,操作数栈,动态链接和方法返回地址。在编译器,栈帧中局部变量表的大小,操作数栈的深度等都可以完全确定,并写入方法表的 code 属性中

局部变量表 存放编译期可知的各种数据类型 (8个基本数据类型)、对象引用

操作数栈 是作为方法调用的中转站,存放方法执行过程中产生的中间结果,包括计算中的临时变量

动态链接 (运行时常量池方法引用):

​ 当 Java 源文件被编译成字节码文件时,所有方法和变量都作为符号引用被存入 class 文件的常量池里。

​ 动态链接的作用就是在一个方法调用另一个方法时,将符号引用转换为调用方法的内存地址进行直接引用。

StackOverFlowError & OutOfMemoryError

  • StackOverFlowError
    • 发生在当当前线程请求的栈深度超过 JVM 虚拟机栈最大深度时,如代码中出现死循环,栈中压入太多栈帧,发生该错误。
    • 无论方法调用正常还是异常,方法结束,栈帧就被弹出。
  • OutOfMemoryError (对于虚拟机栈可能发生的情况)
    • 对于允许栈内存动态扩展的虚拟机来说,当虚拟机在动态扩展时无法申请到足够的内存,发生该错误。
    • 对于HotSpot虚拟机来说,栈不能动态扩展,那么可能发生的情况是线程申请栈时没有足够的内存,也会出现 OOM 异常。

1.3 Java 本地方法栈


和虚拟机栈功能类似但是实现的是 Native 方法的调用。在 HotSpot 虚拟机中本地方法和虚拟机栈合二为一

本地方法栈栈帧结构、生命周期也与虚拟机栈一致。也会出现 StackOverFlowErrorOutOfMemoryError

1.4 堆


堆是线程共享区域,也是 JVM 内存划分最大的一块区域。

这个区域的作用是存放对象的实例,存放几乎所有对象实例和数组。

随着 JIT 编译技术的成熟、栈上分配、标量替换优化等技术,实例存放位置也发生了一定的改变。

JDK 1.7 后,对于没有逃逸出去的对象(没有结果返回引用或者没有被外界使用),直接在栈中分配内存。

堆是垃圾收集器管理的区域,因此也叫 GC堆。由于现在垃圾收集器往往采用分代算法,堆也可以细分为不同代:

  • 新生代内存 (Young Generation) 后又可进一步划分为 Eden区,S0区, S1区(S for Survivor)
  • 老生代内存(Old Generation)
  • 永久代内存(Permanent Generation)

JDK 8之后 PermGen, 被取代为MetaSpace,并存放在本地内存中。

堆内存结构

大部分情况下,对象先被分配到 Eden 区,在一次新生代垃圾回收后,如果对象仍然存活,则进入S0或S1区域,且对象年龄 + 1,年龄增加到年龄阈值,就晋升到老年代。

堆中会出现许多不同形式的 OutOfMemoryError

  1. **java.lang.OutOfMemoryError: GC Overhead Limit Exceeded**:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
  2. java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置,若没有特别配置,将会使用默认值,详见:Default Java 8 max heap sizeopen in new window)
  3. ……

1.5 方法区


是 JVM 运行时区域的一个逻辑划分,由线程共享,不同虚拟机对其实现不同。

虚拟机使用一个类,需要读取并解析其Class文件,并且将信息存入方法区,包括,类信息,字段信息,方法信息,静态变量、即使编译后到代码缓存等等。

永久代和元空间是方法区的具体实现。为什么在 JDK 8 后会用元空间代替永久代呢?

  • 元空间使用的是本地内存,受可用内存限制,OOM 出现几率小,且可用空间多,能加载的类也多。永久代由 JVM 设置上限无法调整,不灵活
  • JDK 8 合并了 HotSpot 和 JRockit, JRockit 中没有PerGen的概念

1.6 运行时常量池


Class 文件里除了之前提到的存入方法区的信息,还包括常量池表。

常量池表里包括编译期生成的字面量,符号引用。类加载后,常量池表被放入运行时常量池中

当受到方法区内存限制,无法再申请到内存时,也有 OOM 错误

1.7 字符串常量池


避免字符串的重复创建,JVM 为字符串开辟了字符串常量池。JDK8 将字符串常量池移到了堆中。

1.7 直接内存


通过 JNI 的方式在本地内存上分配,受本机内存大小、寻址空间限制。虽然直接内存不是虚拟机运行时区域的一部分,但是这部分内存也被频繁地使用,也可能导致 OutOfMemoryError 错误出现。

与 IO/NIO 结合

Tbc… 🐕

2 HotSpot 虚拟机对象


2.1 对象内存布局

2.2 对象创建过程

2.3 对象访问方法

Reference:

[1] Java内存区域详解(重点) | JavaGuide(Java面试 + 学习指南)