Skip to content

简单理解 Java 垃圾收集器

了解垃圾回收 (GC) 在 Java 中的工作原理有什么好处?满足软件工程师的求知欲是一个合理的原因,而且,了解 GC 的工作原理可以帮助你编写更好的 Java 应用程序。

这是我非常个人和主观的意见,但我相信精通 GC 的人往往是更好的 Java 开发人员。如果你对 GC 过程感兴趣,那意味着你有开发特定规模的应用程序的经验。如果你仔细考虑过选择正确的 GC 算法,则意味着你完全了解你开发的应用程序的功能。当然,对于优秀的开发人员来说,这可能不是通用的标准。然而,当我说理解 GC 是成为一名优秀的 Java 开发人员的必要条件时,很少有人会反对。

这是“成为 Java GC 专家”系列文章的第一篇。这次我会介绍GC介绍,下一篇我会分析GC状态和GC调优例子。

在学习 GC 之前,你应该了解一个术语。该术语是 Stop-the-world。无论你选择哪种 GC 算法,都会发生 Stop-the-worldStop-the-world意味着 JVM 正在停止应用程序运行以执行 GC。当 stop-the-world 发生时,除了 GC 所需的线程之外的每个线程都将停止它们的任务。被中断的任务只有在 GC 任务完成后才会恢复。

GC 调优通常意味着减少这种 Stop-the-world 的时间

分代垃圾收集

Java 没有在程序代码中明确指定内存并将其删除。有些人将相关对象设置为 null 或使用 System.gc() 方法显式删除内存。设置为null没什么大不了的,但是调用System.gc()方法会严重影响系统性能,千万不要执行。

在Java中,由于开发者在程序代码中并没有显式地移除内存,垃圾收集器会发现不需要的(垃圾)对象并移除它们。这个垃圾收集器是基于以下两个假设创建的。(将它们称为假设或前提,而不是假设更正确。)

  • 大多数对象很快就会变得无法访问。
  • 从旧对象到新对象的引用只存在少量。

这些假设被称为weak generational hypothesis。所以为了保持这个假设的优势,在 HotSpot VM 中,它在物理上被分为两个——年轻代老年代

年轻代:大部分新创建的对象都位于这里。由于大多数对象很快就无法访问,因此许多对象在年轻代中创建,然后消失。当对象从该区域消失时,我们说发生了“minor GC ”。

老年代:从年轻代中没有变得不可达并幸存下来的对象被复制到这里。它通常比年轻代大。由于内存较大,GC 发生的频率低于年轻代。当对象从老年代消失时,我们说发生了“ major GC ”(或“ full GC ”)。

让我们用图表来看看。

Figure 1: GC Area & Data Flow.

图 1:GC 区域和数据流。

图 1:GC 区域和数据流。

上图中的永久代也称为“方法区”,它存储类或内部字符串。所以,这个区域绝对不是让老年代存活下来的物体永久存在的。这个区域可能会发生GC。此处发生的 GC 仍被视为 major GC

可能有人会疑惑:

如果老年代的对象需要引用年轻代的对象怎么办?

为了处理这些情况,老年代有一个叫做 card table 的东西,它是一个512 字节的块。每当老年代的对象引用年轻代的对象时,都会记录在这张表中。对年轻代执行GC时,只查找这张卡片表来判断它是否是GC的主题,而不是检查老年代所有对象的引用。这个卡表是用写屏障*管理的。这个写屏障是一种可以为 minor GC 提供更快性能的设备。尽管因此会产生一些开销,但总体 GC 时间减少了。

图 2:卡片表结构。

图 2:卡片表结构。

年轻一代的组成

为了理解GC,让我们了解一下年轻代,第一次创建对象的地方。年轻代分为3个空间。

  • Eden 空间
  • 两个Survivor 空间

总共有3个空间,其中两个是Survivor空间。每个空间的执行过程顺序如下:

  1. 大多数新创建的对象都位于 Eden 空间中。
  2. 在 Eden 空间中进行一次 GC 后,将幸存的对象移动到 Survivor 空间之一。
  3. 在 Eden 空间进行一次 GC 后,对象被堆积到 Survivor 空间中,该空间已经存在其他幸存的对象。
  4. 一旦 Survivor 空间已满,幸存的对象就会移动到另一个 Survivor 空间。然后,已满的 Survivor 空间将变为完全没有数据的状态。
  5. 在重复多次的这些步骤中幸存下来的对象被移动到老年代。

通过检查这些步骤可以看到,幸存者空间之一必须保持为空。如果数据在两个 Survivor 空间中都存在,或者两个空间的使用量都为 0,则将其视为你的系统出现问题的标志。

数据通过minor GC堆积到老年代的过程如下图所示:

图 3:GC 之前和之后。

图 3:GC 之前和之后。

请注意,在 HotSpot VM 中,使用了两种技术来实现更快的内存分配。一种称为“bump-the-pointer”,另一种称为“ TLAB(线程本地分配缓冲区)”

Bump-the-pointer 技术跟踪分配给 Eden 空间的最后一个对象。该物体将位于伊甸园空间的顶部。如果之后创建了一个对象,它只会检查对象的大小是否适合 Eden 空间。如果所述对象看起来正确,它将被放置在伊甸园空间中,而新对象将放在最上面。因此,当创建新对象时,只需要检查最后添加的对象,这样可以更快地分配内存。但是,如果我们考虑多线程环境,情况就不同了。为了将多个线程使用的对象保存在 Eden 空间中用于 Thread-Safe,不可避免地会发生锁,并且由于锁争用而导致性能下降。

TLABs 是 HotSpot VM 中此问题的解决方案。这允许每个线程拥有一小部分对应于自己共享的 Eden 空间。由于每个线程只能访问自己的 TLAB,因此即使是指针碰撞技术也允许在没有锁定的情况下进行内存分配。

这是对年轻代 GC 的快速概览。你不一定要记住我刚刚提到的两种技术。你不会因为不认识他们而入狱。但是请记住,在Eden空间首先创建对象之后,通过Survivor空间将长期存活的对象移动到老年代。

老年代GC

老年代基本上是在数据满的时候进行一次GC。执行过程因GC类型而异,所以如果你了解不同类型的GC会更容易理解。

根据 JDK 7,有 5 种 GC 类型。

  1. 串行气相色谱
  2. 并行GC
  3. Parallel Old GC (Parallel Compacting GC)
  4. 并发标记和清除 GC(或“CMS”)
  5. 垃圾优先 (G1) GC

其中,串行 GC 不得用于正在运行的服务器上。这种 GC 类型是在台式计算机上只有一个 CPU 核心时创建的。使用此串行 GC 将显着降低应用程序性能。

现在让我们了解每种 GC 类型。

Serial GC (-XX:+UseSerialGC)

年轻代中的 GC 使用我们在上一段中解释的类型。老年代的 GC 使用了一种叫做“ mark-sweep-compact ”的算法。

  1. 该算法的第一步是标记老年代幸存的对象。
  2. 然后,它从前面检查堆,只留下幸存的堆(扫描)。
  3. 在最后一步,它从前面用对象填充堆,使对象连续堆积,并将堆分成两部分:有对象的部分和没有对象的部分(紧凑)。

串行GC适用于小内存和少量CPU内核。

Parallel GC (-XX:+UseParallelGC)

图 4:串行 GC 和并行 GC 之间的区别。

图 4:串行 GC 和并行 GC 之间的区别。

从图中可以很容易看出串行GC和并行GC的区别。串行 GC 仅使用一个线程来处理 GC,而并行 GC 使用多个线程来处理 GC,因此速度更快。当有足够的内存和大量内核时,此 GC 很有用。它也被称为“吞吐量 GC”

Parallel Old GC(-XX:+UseParallelOldGC)

自 JDK 5 更新以来,支持 Parallel Old GC。与并行 GC 相比,唯一的区别是老年代的 GC 算法。它经历了三个步骤:标记 - 摘要 - 压缩。汇总步骤针对 GC 先前执行过的区域分别识别幸存对象,因此不同于标记-扫描-压缩算法的扫描步骤。它经历了更复杂的步骤。

CMS GC (-XX:+UseConcMarkSweepGC)

图 5:串行 GC 和 CMS GC。

图 5:串行 GC 和 CMS GC。

从图中可以看出,Concurrent Mark-Sweep GC 比我迄今为止解释的任何其他 GC 类型都要复杂得多。早期的初始标记步骤很简单。在最接近类加载器的对象中搜索幸存的对象。所以,暂停时间很短。在并发标记步骤中,对刚刚确认的幸存对象所引用的对象进行跟踪和检查。这一步的不同之处在于它在处理其他线程的同时进行。在remark步骤中,检查并发标记步骤中新添加或停止引用的对象。最后,在并发扫描中步骤,垃圾收集程序发生。在其他线程仍在处理中时执行垃圾收集。由于这种GC类型以这种方式执行,因此GC的暂停时间非常短。CMS GC 也称为低延迟 GC,在所有应用程序的响应时间至关重要时使用

虽然这种 GC 类型具有停止世界时间短的优点,但它也有以下缺点。

  • 它比其他 GC 类型使用更多的内存和 CPU。
  • 默认情况下不提供压缩步骤。

在使用这种类型之前,你需要仔细检查。此外,如果由于内存碎片很多而需要执行压缩任务,则 stop-the-world 时间可能比任何其他 GC 类型都长。你需要检查执行压缩任务的频率和时间。

G1 GC

最后,让我们了解一下垃圾优先(G1)GC。

图 6:G1 GC 的布局。

图 6:G1 GC 的布局。

如果你想了解 G1 GC,忘记你所知道的关于年轻代和年老代的一切。如图所示,每个网格分配一个对象,然后执行一次GC。然后,一旦一个区域已满,将对象分配到另一个区域,然后执行 GC。数据从年轻代三个空间移动到老年代的步骤在这种GC类型中是找不到的。创建这种类型是为了取代 CMS GC,从长远来看,它会引起很多问题和抱怨。G1 GC 的最大优势在于其性能。它比我们目前讨论的任何其他 GC 类型都快。

参考文档:

https://www.cubrid.org/blog/3826410