Blog chevron_right 未分类

作为 Java 开发人员,我应该了解垃圾收集的哪些知识?

垃圾收集器是 Java 虚拟机 (JVM) 的重要组成部分,它会影响应用程序的性能和可靠性。但它到底是什么,为什么了解它的工作原理很重要?如果您是 Java 开发人员,您可能会问,“关于垃圾收集,我应该了解什么?” 让我们与 Azul 的一些专家交谈,以更深入地了解 Java 生态系统的这一部分。

本概述是利用 Azul 的专家深入了解 Java 系统的系列博文中的第一篇

什么是垃圾收集器?

在许多其他编程语言中,程序员管理对象的创建和删除以调节内存使用。在 Java 中,程序员专注于应用程序功能,而垃圾收集器 (GC) 控制内存的使用方式。GC 会自动释放不再使用的对象占用的内存空间。当存储在内存中的对象对程序来说变得“不可访问”时,GC 会检测到它们并清除它们。

得益于 Java 虚拟机中的 GC,Java 应用程序通常可以避免影响 C/C++ 的意外内存泄漏和碎片问题。Java 应用程序具有数月的可靠正常运行时间,无需定期重启。

因此很明显 GC 是 Java 应用程序内存管理的中心,但其他应用程序采用不同的方法:

  • 在模块启动或启动时分配的所有内存:例如,这用于 Fortran(pre-Fortran90)和 Cobol
  • 堆中内存的显式分配和释放通常使用链表进行管理:C、C++、……
  • 托管内存以多种方式存在:
    • 压缩 GC:这是 JAVA GC
    • 适用范围:Rust、Erlang
    • 引用计数:每个对象的引用数定义了一个对象是否仍在使用中
    • 范围和计数的混合以及一些 GC

垃圾收集器如何工作?

像 Java 这样的托管语言为您隐藏了复杂性

John CuthbertsonC4 GC 团队首席工程师

从 1.1 版开始,我就一直在研究 Java 中的垃圾收集器,最初是在 G1 GC 上,后来是在其他垃圾收集器上。我想与开发人员分享的最重要的信息是,托管语言(如 Java)的整体理念是,像 GC 这样基本和广泛的实现的复杂性实际上对您来说是“隐藏的”。这意味着你不需要关心!由于许多开发人员都在运行时和语言本身上努力工作,因此广大的开发​​人员用户群可以从“幕后”进行的所有工作中受益。原始 JVM 中的初始 GC 非常有限并且无法无缝工作,但由于所有的演变,GC 的当前开销已经不那么明显了。

请继续阅读,但请记住,您可以专注于您的业务逻辑,而 Java 运行时将负责内存管理——即使您没有意识到 GC 为您工作了多少!另一方面,如果您想更好地了解 GC 如何影响您的程序,这里有很多东西需要学习。

垃圾收集的不同阶段

GC 过程可以遵循不同的方法,并且在所有情况下都包含以下一个或多个步骤。

  • 标记(= Trace):从应用程序的根开始,所有可以到达的链接内存块都是“Painted”。想象这是一棵有树枝的树,所有的叶子都是彩色的。当到达分支的所有端点时,已绘制的块可被视为“Live”,而其余未绘制的内存块可被视为“Non-Live”。
  • 清除:所有“Non-Live”对象都从堆内存中清除。 
  • 压缩:“实时”内存对象靠得更近(碎片整理、重定位)以确保大的空闲内存块可用于新对象。一些收集器将有第二个“通道”来更新应用程序中对内存对象的引用,以确保它们指向内存中的正确位置。
  • 复制:这是另一种改进内存使用方式的方法。在这个过程中,所有“Live”对象都被移动到“To”空间,而“From”空间中的剩余对象可以被视为“Non-Live”。

有多种类型的 GC,具体取决于它们使用的是以下哪种方法:

  • 标记/扫描/压缩
  • 复制
  • 标记/压缩

当您想更好地理解 GC 过程时,与 GC 的实现方式相关的一些其他术语是必不可少的。

  • 单次与多次通过:
    • 单程:在单次运行中处理多个步骤。
    • 多遍:在多遍中,步骤在不同的遍中处理,一个接一个。
  • 串行与并行:
    • 串行:一个 GC 线程
    • 并行:多个 GC 线程
  • 停止世界与并发:
    • Stop-The-World:应用程序在 GC 循环运行时停止。
    • 并发:GC 在应用程序“旁边”运行,对应用程序执行没有影响。

Live Set 和分配率的重要性

如不同阶段所述,包含所有仍在使用的对象的活动集是 GC 行为的重要因素。如果 Java 应用程序具有恒定的负载和行为,并且从活动集中稳定地添加和删除对象,则它的大小将保持稳定。不断增长的活动集可能是由内存泄漏引起的。

-Xmx标志定义了 Java 应用程序的最大堆大小。如果活动集的大小接近该-Xmx大小,则 JVM 缺少可用内存来存储新对象和执行 GC。这会降低性能。为了保持服务器的大小适合运行您的应用程序,您需要平衡安装的内存量和值与活动集的实际大小。使您的服务器过大只是浪费金钱。但要正确定义这个维度,必须考虑分配率。-Xmx

此分配率是基于每个时间单位分配的内存量的值,例如 MB/sec。高值表示正在创建大量对象,从而导致需要进行大量清理。这将影响 GC 暂停的频率和/或持续时间。

堆大小 ( -Xmx) 的一个很好的准则是平均活动集大小的 2.5 到 5 倍。分配率越高,为实现最佳 GC,堆就必须越大。

OpenJDK 的 Azul Zulu Prime 构建包含一种称为分配步调的机制,当堆使用接近本博-Xmx文中所述时,该机制通过限制应用程序的分配率来帮助减少峰值分配延迟。

分代回收

GC 中使用的另一种技术是“分代堆”,在堆的不同区域保留“年轻”对象和“旧”对象。 

  • 大多数物体在年轻时就死去
  • 很少有从旧对象到新对象的引用存在。

使用这个假设,Java 堆被分成两个物理区域:

  • 年轻一代:这是分配新对象的地方,也是存储不够老的对象的地方。这通常是一个较小的集合,其中包含大量垃圾对象,GC 可以快速处理这些对象。通常年轻一代的 Stop-The-World GC 是单次传递。年轻一代进一步分为称为“伊甸园”和“幸存者空间”的部分,以便在使用时间较长时移动年轻对象。
  • 老年代:寿命更长的对象最终被提升到老年代。GC 处理此集合的频率较低,但需要较长时间。

在许多情况下,老年代比年轻代大,但并非总是如此。这取决于应用程序的静态工作活动集以及年轻代和老年代之间的边界有多灵活。在基于区域的分代收集器(C4 和 G1)中,分代的大小是流动的和有弹性的。大多数区域可能是年轻一代,或者大多数可能是老年代。在 CMS、Parallel 和 Serial 等收集器中,两代之间的边界是固定的,新老代大小的比例可能需要调整。

下图说明了典型的年轻代 GC 如何在 Eden 空间被填满时清理和移动对象。新对象被分配到伊甸园空间,直到它填满。在GC期间,Eden和Survivor空间中的存活对象(可达对象)被复制到另一个Survivor空间。如果任何对象变得“足够老”,它们将被复制到老年代(即它们是永久的)。

您可以通过关注具有较短生命周期的方法中的局部变量来利用年轻代系统,这样 GC 就可以专注于可以快速处理的堆子集。

Java 垃圾收集器的类型

就像 Java语言已经进化一样,运行时和工具也进化了很多,不同的 GC 已经成为 JRE 的一部分。

Java 中不同垃圾收集器的表格概述

旧 GC 代的一些提示不再适用

Deepak Sreedhar,首席软件工程师,GC 专家

近几十年来,GC 有了很大的发展。作为 Java 开发人员或 DevOps,您需要了解一些针对旧 GC 生成的技巧或不再适用。C4、ZGC、Shenandoah是真正并发的。这些现代 GC 的暂停时间非常短,通常以毫秒为单位甚至更短。活动集的大小(无法收集的对象,因为它们具有将来可能仍会使用的引用) 仍然确定 GC 周期的持续时间,但应用程序在运行时不会暂停。暂停时间不会随着活动集或堆大小的增加而增加。传统上,一直有意识地尝试以这种方式设计应用程序以避免需要更大的 Java 堆。由于并发 GC,您可以专注于业务逻辑并尝试实现程序功能流的最佳可能速度。不要再担心 GC 导致的响应时间异常! 

开发人员仍然需要注意的一件事是避免 Java 堆中的泄漏,这可能导致 GC 的高活集。大多数现代 GC 的持续时间和 CPU 消耗与活动集的大小成正比。Java 生态系统有多种工具可以帮助分析活动集和识别问题。Azul 支持随时准备提供我们力所能及的任何帮助! 

垃圾收集器对应用程序的影响

现在应该清楚了,“垃圾收集器”并不存在;但根据您的 Java 运行时版本和/或启动选项,有多个可用,您甚至可以选择要使用的一个!但这种灵活性也带来了一些责任。你只是选择默认选项,还是想使用另一个?当新客户想要评估 Azul Zulu Prime 与 OpenJDK 或其他发行版时,Azul 的专家随时可以为他们提供指导,并且他们在比较不同用例方面拥有丰富的经验。

某些编码实践可能会影响 Java 使用内存的方式

Michael Roeschter,销售工程师

GC 对应用程序的行为方式有重大影响。尽管如此,作为一名开发人员,您还应该意识到某些编码实践会对 Java 使用内存的方式产生影响,并且一些问题也可以通过代码更改得到解决!我们看到这种胜利的例子之一是在统计和解析器应用程序中,其中复制了大量数据并且只使用一次。创建和使用短暂的小对象或 ArrayLists 不是问题。但是当大型数据结构以“创建到丢弃”模式使用时,内存分配率可能失控,数据结构的重用可能是有益的. 一个示例是包含数百万个相同大小对象的一次性大型缓冲区或数组。

当存在两种“刻板印象”数据时,经过优化以区分新旧对象的分代 GC 效果最佳:

  • 交易数据:在交易或事件期间创建并在几秒或几毫秒内消亡的对象。
  • 引用数据:加载一次并引用(读取)但未被事务修改的数据。

另一方面,对于 GC 而言,“最糟糕”的内存类型是滚动缓冲区 (FIFO),数据会在其中保存数分钟或数小时。这不是编程问题,而是有“业务”原因——例如,当必须使用滚动事务日志、会话缓冲区或类似内容时。当一个应用程序不断地以高速率修改其“旧的”长期存在的数据时,非并发 GC 迟早会遇到麻烦并需要 full GC

对运行时环境的影响

Azul 在 OpenJDK 之上还有其他技术可以提高 Java 应用程序的性能,因为这不仅与应用程序本身的行为有关,而且还可能受到环境、集群或组织内使用的资源的影响。

始终考虑最紧迫的问题来解决

Daniel Witkowski,销售工程师

我们在引导潜在客户评估 Azul Zulu Prime 时,总是考虑最迫切需要解决的问题。根据该起点,我们将研究 Falcon、ReadyNow 或我们的 C4 GC 如何从一开始就取得最重要的胜利。对于特定的项目,很明显堆大小是垃圾收集器导致应用程序执行长时间停顿的原因。例如,当 GC 清理内存时,使用 100Gb 堆的项目预计会暂停 10 秒以上。在其他情况下,例如,金融和游戏应用程序,一个 10Gb 大小的较小堆停止数百毫秒可能已经是一个大问题。无论如何,拥有一个不会在不可预测的时间内完全停止您的应用程序的垃圾收集器对于每个期望一致的短响应时间的项目都是必不可少的;换句话说,低延迟。

集群是我们看到由 GC 引起的问题的另一个例子。当一个拥有大堆的节点因为在 GC 周期中没有响应而被认为死了时,一个进程开始启动一个新节点并重新分配数据……但是突然间,被认为死了的节点在 GC 之后重新出现循环,导致集群中出现一系列不良事件。

我们在不同的项目中看到,Azul Zulu Prime 的引入解决了许多低延迟专家试图在代码中解决的问题,但现在完全由 C4 Azul Zulu Prime 垃圾收集器处理,消除了他们的应用程序遇到的所有暂停。

三选二

在 IT 项目管理中,有一个著名的规则:“您需要在速度、质量和成本之间做出选择。但你只能拥有这 3 个中的 2 个。” 运行应用程序也是如此。您需要选择以下两项:

  • 极低的延迟
  • 极高的吞吐量
  • 最低资源使用率(CPU 和内存)

幸运的是,Azul Platform Prime 结合了多种技术和调整选项,可让您实现特定目标。高度优化的Falcon JIT 编译器不仅补偿了引入代码以帮助并发 GC 的“障碍”开销。Ready Now!和 Connected Compilation 有助于在不牺牲太多预热时间和 CPU 的情况下提供吞吐量。Prime GC 不断改进以找到三个目标的最佳平衡点。 

当 GC 并发时,它与并发运行的应用程序线程共享资源。因此,GC 周期的持续时间可能会受到系统或容器内 CPU 负载水平的影响。Stop-The-World GC 不会面临这个问题,因为它在运行时会停止所有 Java 线程。因此,如果系统高度饱和,并发 GC 可能会花费大量时间并引入分配暂停。要充分利用并发 GC,建议将 CPU 平均负载保持在可用内核数以下。当然,最终的 GC 行为将取决于多种因素的组合——活动集、分配率和 CPU 平均负载。

服务水平预期下的吞吐量

Azul Platform Prime有助于实现高“有用容量”——承载的负载量,同时保持合理的服务水平预期。如前所述,垃圾收集器的选择会影响应用程序的响应能力。Stop-the-world 和部分并发收集器以比 Platform Prime 的垃圾收集器小得多的负载打破响应时间目标。最终结果是,在 Prime 上配置具有响应时间预期的节点集群的成本通常要低得多。 

有关此主题的更多信息,请参阅博客文章Cassandra 性能:吞吐量、响应能力、容量和成本

监控资源使用情况以获得最佳垃圾收集器行为

VisualVM(在 OpenJDK 中提供)、Java Flight Recorder(OpenJDK 和 Azul)和 GC 日志分析器(由 Azul 提供)是可以帮助您识别潜在内存泄漏并密切关注正在使用的资源的 Java 工具。在我们的文档站点上,您可以找到有关如何使用这些工具的更多信息:

还有很多东西要学

John Cuthbertson(首席工程师,C4 GC 团队)

与即时编译和 Java 的其他重要组件一样,还有很多东西需要学习。尽管 GC 是一项成熟的技术,但作为它的开发人员,我们一直在寻找实现更改之间的最佳解决方案,以及它们如何影响 GC 本身和使用它的应用程序的行为。我们总是需要考虑“蝴蝶效应”。一侧的微小变化可能会对其他地方产生相当大的影响。预测变化的影响总是很困难。这就是为什么这么多人致力于调整 Java 虚拟机中的实现并记录所有可能的更改及其影响的原因。

联系我们

了解更多…

如果您问“关于垃圾收集,我应该了解什么”,这篇文章概述了垃圾收集器的功能以及开发人员应该知道的事情。这只是一个起点,您还可以学习更多内容以更深入地了解该主题!在我们的文档网站上,您可以通过以下链接找到与 Azul Zulu Prime 提供的 GC 优化相关的更多信息:

挑战:了解 Java 应用程序中内存泄漏如何发生的一种极好的方法是试图故意引起它们!StackOverflow 在一个很好的列表中描述了这些案例

您还可以观看 Gil Tene 的演讲,他是 Azul 的创始人之一,也是该主题的专家: