第3章:深入堆空间

深入堆空间

在第2章中,我们讨论了引用和对象在内存中的差异。引用及其所指对象密切相关。我们发现,Java的按值调用机制可能会引发一个安全问题——“引用逃逸”,此外还有可变对象的问题。通过示例代码和示意图,我们考察了这些问题以及如何使用防御性拷贝来解决它们。

我们知道,基本类型和引用可以存在于栈和堆上,而对象只存在于堆上。现在,我们准备更仔细地审视堆,为下一章关于垃圾回收(GC)的内容做准备。本章将涵盖以下主题:

  • 探索堆上的不同代际
  • 学习如何使用这些空间

堆空间由两个不同的内存区域组成:

虽然本章不会深入讲解GC过程,但我们需要解释什么是存活对象。存活对象是指从GC根可达的对象。

探索堆上的不同代际

垃圾回收根

GC根是一种特殊类型的存活对象,因此不可被GC回收。所有从GC根可达的对象也都是存活的,因此也不可被GC回收。GC根在GC中充当起点——从这些根开始,将所有可达对象标记为存活。最常见的GC根如下:

  • 栈上的局部变量
  • 所有活跃的Java线程
  • 静态变量(因为可以通过其类来引用)
  • Java本地接口(JNI)引用——由本地代码在JNI调用过程中创建的对象。这是一种非常特殊的GC根情况,因为JVM不知道这些对象是否被本地代码引用。

让我们看看这些空间在内存中是如何呈现的,如图3.1所示:

图3.1 – 堆代际

(此处为示意图,展示年轻代(Eden、S0、S1)和老年代(Tenured)的布局。年轻代包含Eden和Survivor空间(S0和S1),老年代位于右侧。)

在此,我们需要一些简要的定义,以便讨论这些空间的使用方式:

  • 年轻代空间 – 年轻代空间,有时被称为新生区新空间,包含两个独立区域:Eden空间Survivor空间。两者功能不同,总体目标是提高内存效率。我们将依次讨论:
    • Eden空间:新对象分配在Eden空间。当Eden空间已满,无法分配新对象时,会触发年轻代(Minor)垃圾回收。
    • Survivor空间:存在两个大小相等的Survivor空间,即S0和S1。Minor垃圾回收器交替使用这些区域。我们将在后面更详细地探讨。
  • 老年代空间 – 也称为终身代空间。这是生命周期较长的对象所在的位置。换句话说,垃圾回收器将在一定数量的GC中存活下来的对象移到这里。当终身代空间变满时,会触发一次Major GC。

学习如何使用这些空间

为了理解这些不同空间的使用方式,我们将分两个阶段进行解释。首先,我们考察Minor GC算法中这些空间的使用方式。随后,通过一个示例展示该算法的实际运行。

理解Minor垃圾回收算法

我们先从Minor GC算法开始。图3.2是Minor GC过程的高级伪代码:

图3.2 – Minor垃圾回收算法的伪代码

当Minor GC触发时:
  复制Eden中所有存活对象到目标Survivor空间(S0或S1),设置年龄为1
  检查源Survivor空间:
    将年龄达到晋升阈值的存活对象复制到老年代(晋升)
    将剩余(未晋升)的存活对象复制到目标Survivor空间,年龄加1
  清空Eden和源Survivor空间

让我们用一个“Given-When-Then”场景来分析上图所示的过程。

  • Given(给定):初始时,S0作为目标Survivor空间,S1作为源Survivor空间。
  • When(当):Minor垃圾回收器运行。换句话说,Eden空间没有足够空间容纳JVM希望分配的对象。
  • Then(那么)
    1. 将所有来自Eden空间的存活对象复制到S0 Survivor空间。这些对象的年龄被设置为1,因为它们刚刚通过了第一次GC周期。
    2. 检查S1,任何存活对象中,年龄达到给定阈值(晋升阈值)的对象被复制到老年代空间,即被晋升。换句话说,这是一个长生命周期对象,因此将其复制到老年代区域(长生命周期对象所在的位置)。这使得未来的Minor GC更高效,因为确保这些相同的对象不会被重新检查。
    3. 剩余的S1存活对象(未晋升的那些)被复制到S0,其年龄增加1,因为它们刚刚又经历了一个GC周期。

注意,晋升阈值可以使用JVM参数-XX:MaxTenuringThreshold进行配置。实际上,该标志允许你自定义一个对象在Survivor空间中停留多少个GC周期,然后才最终晋升到老年代空间。但使用该参数时需谨慎,因为大于15的值会指定对象永远不晋升,从而用旧对象无限填满Survivor空间。

图3.3展示了刚刚讨论的过程:

图3.3 – 以S0为目标空间的Minor垃圾回收

图中展示了:Eden中存活对象复制到S0(年龄设为1);S1中年龄达到阈值的对象复制到老年代;S1中年轻存活对象复制到S0(年龄递增);然后Eden和S1被回收。

以下是总结:

  • 复制Eden存活对象到S0(年龄设为1)
  • 复制旧S1存活对象到老年代空间
  • 复制年轻S1存活对象到S0(年龄递增)

既然来自Eden和S1的存活对象已被复制(保存),Eden和S1现在都可以被回收。

下一次Minor收集器运行时,鉴于上次S0是目标Survivor空间,这次S1将成为目标Survivor空间。因此,所有来自Eden的存活对象被复制到S1,每个对象年龄设为1。由于S1现在是目标空间,S0成为源空间。垃圾回收器检查S0,将长生命周期对象复制到终身代空间,将短生命周期对象复制到S1。图3.4展示了这个过程:

图3.4 – 以S1为目标空间的Minor垃圾回收

图中展示了:Eden中存活对象复制到S1(年龄设为1);S0中年龄达到阈值的对象复制到老年代;S0中年轻存活对象复制到S1(年龄递增);然后Eden和S0被回收。

以下是总结:

  • 所有Eden存活对象复制到S1(年龄设为1)
  • 复制旧S0存活对象到老年代空间
  • 复制年轻S0存活对象到S1(年龄递增)

既然来自Eden和S0的存活对象已被复制,Eden和S0都可以被回收。

现在我们已经讨论了空间的使用方式,接下来通过一个示例来增强我们的解释。

演示Minor垃圾回收算法的实际运行

图3.5显示了在第一次Minor垃圾回收运行之前,内存中的初始状态:

图3.5 – 第一次Minor垃圾回收前的初始堆状态

图中展示:Eden空间中有对象A、B、C、D、E、F、G,其中红色对象(A、D、G)不可达(可回收),绿色对象(B、C、E、F)存活;Survivor空间S0和S1均为空;老年代已有部分对象;JVM试图在Eden中分配对象H,但空间不足。

在上图中,对象H代表JVM试图在Eden空间中为其分配内存的对象。Eden空间包含以下内容:

  • 红色对象:没有从GC根指向它们的引用。它们符合GC条件。
  • 绿色对象:存活对象,意味着它们是GC根或者可通过GC根到达。这些对象不符合GC条件。
  • 白色空隙:Eden空间中的间隙。如果有足够连续空间来分配对象,则对象存储在Eden中,并返回其引用。然而,如果由于内存碎片化,没有足够连续空间来分配对象,则会触发一次Minor(年轻代)GC。

Survivor空间包含以下内容:

  • S0 – 初始为空;我们假设JVM使用它作为目标Survivor空间
  • S1 – 初始也为空;由于S0是目标空间,S1成为源空间(由于初始S1中无内容,第一次运行时没有影响)

老年代(终身代)空间包含长生命周期对象。长生命周期对象是指那些已经存活了预定义次数的Minor GC的对象。这是一个可通过-XX:MaxTenuringThreshold JVM参数自定义的阈值。

如图3.5所示,JVM需要分配对象H,但由于Eden中没有足够空间,这触发了Minor(年轻代)GC。对象A、D、G可以从Eden中移除,对象B、C、E、F可以移动到S0。Eden空间被回收,对象H被分配。

图3.6显示了第一次Minor GC完成后的堆状态:

图3.6 – 第一次Minor垃圾回收后的堆状态

图中展示:Eden中已分配对象H(绿色);S0中有对象B、C、E、F(年龄均为1);S1为空;老年代不变。

在上图中,对象H被分配在Eden中,对象B、C、E、F在S0中。注意S0中的对象每个年龄为1,因为这是它们存活的第一次Minor GC。

图3.7显示了第二次Minor GC运行前的堆状态:

图3.7 – 第二次Minor垃圾回收前的堆状态

图中展示:Eden中有对象H(存活)、I(存活)、J(存活)、K(存活)、L(可回收)、M(可回收);S0中有对象B(可回收)、C(存活)、E(存活)、F(存活);S1仍为空;JVM试图分配对象N但Eden空间不足。

在图3.7中,JVM试图分配对象N,但Eden中没有空间。这将触发Minor垃圾收集器运行(第二次)。在Eden空间中,对象H、L、M符合GC条件,对象I、J、K存活。在Survivor空间S0中,对象B现已符合GC条件,而对象C、E、F存活。

图3.8显示了第二次Minor GC运行后的堆状态:

图3.8 – 第二次Minor垃圾回收后的堆状态

图中展示:S1中现在有来自Eden的对象I、J、K(年龄1)和来自S0的对象C、E、F(年龄2);Eden已清空并分配了对象N(存活);S0被回收;老年代不变。

在图3.8中,S1现在是目标Survivor空间,因此S0是源空间。垃圾收集器将存活对象C、E、F从S0移动到S1,将其年龄值从1增加到2。然后垃圾收集器回收S0空间。

对象I、J、K从Eden移动到S1,年龄值为1,因为这是它们第一次存活过Minor GC。Eden空间被回收,对象N被分配。

最后要展示的是对象移动到终身代空间。这就是图3.9所演示的:

图3.9 – 对象移动到终身代空间

图中展示:经过15次Minor GC后,S0(此时为源空间)中有对象J(年龄14)、P(年龄8)、S(年龄3)和E、F(年龄15,已晋升);E和F被复制到老年代(年龄值15达到默认阈值15);S1是目标空间,接收来自Eden的对象X(新分配触发GC)以及来自S0的未晋升对象J、P、S;Eden被回收。

图3.9代表经过15次Minor GC后的堆。对象E和F移动到终身代空间,因为它们的年龄值15已达到阈值(默认阈值是15)。下次Minor垃圾收集器运行时,这两个对象都不会出现,从而使垃圾收集器运行更高效。

对象X是触发本次Minor垃圾收集器的对象,在此次迭代中,S1是源空间,S0是目标Survivor空间。对象J、P、S仍然存活,从S1移动到S0,年龄计数分别为14、8和3。

相关JVM标志

在结束本章之前,值得提及其他一些相关的JVM标志:

  • -Xms-Xmx:分别指定堆的最小和最大大小。
  • -XX:NewSize-XX:MaxNewSize:分别指定年轻代的最小和最大大小。
  • -XX:SurvivorRatio:指定两个Survivor空间相对于Eden空间的大小比例。例如,-XX:SurvivorRatio=6 将Eden与一个Survivor空间的比例设置为1:6。换句话说,每个Survivor空间的大小将是Eden的六分之一,因此是年轻代的八分之一(不是七分之一,因为有两个Survivor空间)。
  • -XX:NewRatio:表示新生代相对于老年代的大小比例。例如,-XX:NewRatio=3 将新生代与老年代的比例设置为1:3。这意味着新生代(Eden加上两个Survivor空间)占用堆的25%,老年代占用剩余的75%。
  • -XX:PretenureSizeThreshold:如果一个对象的大小大于此标志指定的大小,则该对象会立即晋升,即该对象直接分配到老年代空间。此标志通常与-XX:+UseTLAB配合使用,但需要注意,如果TLAB(线程本地分配缓冲区)已启用,则此阈值可能被忽略。

一般建议

通常,将年轻代空间保持在整个堆大小的25%到33%之间。这确保了老年代空间总是更大。这是可取的,因为Full GC比Minor GC代价更高。

总结

本章我们深入审视了堆空间。我们首先考察了堆上的不同代际——即年轻代空间和老年代(终身代)空间。

年轻代空间分为两个区域:Eden空间和Survivor空间。Eden空间是新对象分配的地方。Survivor空间(S0和S1)用于在Minor GC期间保存从Eden晋升的存活对象。Minor GC算法以交替方式使用这些Survivor空间,通过复制存活对象并递增其年龄来优化内存使用。年龄达到配置阈值的对象被晋升到老年代。我们还通过逐步示例演示了该算法,并介绍了相关的JVM调优标志(如-Xms-Xmx-XX:NewRatio-XX:SurvivorRatio-XX:MaxTenuringThreshold-XX:PretenureSizeThreshold),这些标志可用于控制堆代际的大小和行为。

3.0 第3章:深入堆空间

幸存者空间由两个大小相等的空间组成,即 S0 和 S1。年轻代垃圾收集器(次要 GC)在回收内存时会使用这些幸存者空间。当 Eden 空间中没有足够的连续空间来分配对象时,便会触发 Minor GC。通过伪代码和图示,我们考察了次要垃圾收集器如何利用各个代际和空间。接着我们使用了一个包含多个用例场景的例子来强化这些概念。

终身代空间是存放生命周期更长的对象的地方。我们看到,如果一个对象存活过了数个 GC 周期,该对象就会被移至终身代空间,以使后续的 Minor GC 周期更加高效。最后,我们考察了相关的 JVM 标志。

现在我们已经理解了堆,并对次要垃圾收集器有了高层次的了解,准备深入探讨 GC——这正是下一章的主题。

默认值行为

默认值为 0,这意味着不会有任何对象被直接分配至堆的老年代。

图像上下文:

  • [第71页上的图564]
  • [第73页上的图567]
  • [第75页上的图571]
  • [第76页上的图573]
  • [第77页上的图575]
  • [第79页上的图578]
  • [第80页上的图580]
  • [第81页上的图582]
  • [第82页上的图584]

3.0 第3章:深入堆空间


图片内容分析(由视觉模型提取)

Page 71, Image 1: 图片未能加载,无法分析其具体内容。

Page 73, Image 1: 无法识别图片内容,请提供图像。

Page 75, Image 1: 图片展示了Java堆空间的示意图,可能包含年轻代(Young Generation)、老年代(Old Generation)以及元空间(Metaspace)等区域,并标注了对象分配和垃圾回收的流程。该图用于直观说明堆内存的分代管理和对象生命周期。

Page 76, Image 1: 图片内容无法获取(显示为[Unsupported Image]),根据上下文推测,该图可能展示了Java堆空间的结构,包括对象和引用在堆与栈中的分布关系,以及逃逸引用和防御性拷贝的示意图。

Page 77, Image 1: 图片展示了Java堆空间的内部结构,包括年轻代(Eden、Survivor0、Survivor1)和老年代(Tenured)的划分,以及对象分配与晋升的流程。可能还标注了元空间(Metaspace)区域。整体用于说明对象在堆中的生命周期和垃圾回收机制。

Page 79, Image 1: 图片无法获取,无法分析。根据上下文,推测该图片展示了堆空间内部结构,包括对象、引用及其与栈的关系,可能涉及逃逸引用和防御性复制等概念。

Page 80, Image 1: 图片展示了 Java 堆空间的内存结构示意图,其中包含若干对象和引用,用于解释对象在堆中的分配、引用指向以及逃逸引用的问题。图中使用了箭头表示引用关系,并可能标有防御性复制的相关示意。

Page 81, Image 1: 图片内容不可见,无法分析。

Page 82, Image 1: 由于图片无法加载,无法分析其具体内容。