目录
在我的上一篇文章中,我写了关于 Java 虚拟机 (JVM) 的文章并解释了它的架构。作为 Execution Engine 组件的一部分,我还简要介绍了 Java 垃圾回收器 (GC)。另外博主发现了一个非常全面的JVM文章免费的,我的JVM,垃圾处理等知识就是在那里学习的链接放在最后了。
在本文中,您将了解有关垃圾回收器、它的工作原理以及 Java 中可用的各种类型的 GC 及其优势的更多信息。我还将介绍最新 Java 版本中提供的一些新的实验性垃圾回收器。
什么是 Java 中的垃圾收集?
垃圾回收是通过销毁未使用的对象来回收运行时未使用的内存的过程。
在 C 和 C++ 等语言中,程序员负责对象的创建和销毁。有时,程序员可能会忘记销毁无用的对象,并且分配给它们的内存没有被释放。系统已用内存不断增长,最终系统中没有内存可供分配。此类应用程序存在 “内存泄漏”。
在某个时间点之后,没有足够的内存可用于创建新对象,并且整个程序由于 OutOfMemoryErrors 而异常终止。
您可以使用 C 和 C++ 中的方法来执行垃圾回收。在 Java 中,垃圾回收在程序的生命周期内自动进行。这样就无需取消分配内存,从而避免了内存泄漏。free()
delete()
Java 垃圾回收是 Java 程序执行自动内存管理的过程。Java 程序编译成可在 Java 虚拟机 (JVM) 上运行的字节码。
当 Java 程序在 JVM 上运行时,将在堆上创建对象,堆是专用于程序的内存的一部分。
在 Java 应用程序的生命周期内,将创建和发布新对象。最终,不再需要某些对象。可以说,在任何时间点,堆内存都由两种类型的对象组成:
- Live - 这些对象正在从其他位置使用和引用
- Dead - 不再使用或从任何位置引用这些对象
垃圾回收器会找到这些未使用的对象并删除它们以释放内存。
如何在 Java 中取消引用对象
Garbage Collection 的主要目标是通过销毁不包含引用的对象来释放堆内存。当没有对对象的引用时,假定它是死的,不再需要。因此,可以回收对象占用的内存。
有多种方法可以释放对对象的引用,使其成为垃圾回收的候选对象。他们之中有一些是:
通过将引用设为 null
Student student = new Student();
student = null;
通过将引用分配给另一个
Student studentOne = new Student();
Student studentTwo = new Student();
studentOne = studentTwo; // now the first object referred by studentOne is available for garbage collection
通过使用匿名对象
register(new Student());
垃圾回收在 Java 中是如何工作的?
Java 垃圾回收是一个自动过程。程序员不需要显式标记要删除的对象。
垃圾回收实现位于 JVM 中。每个 JVM 都可以实现自己的垃圾回收版本。但是,它应该满足标准的 JVM 规范,即使用堆内存中存在的对象,标记或识别无法访问的对象,并通过压缩销毁它们。
Java 中的垃圾回收根是什么?
垃圾回收器根据垃圾回收根 (GC Roots) 的概念来识别活动对象和死对象。
此类垃圾回收根的示例包括:
- 由系统类加载器(不是自定义类加载器)加载的类
- 活动线程
- 当前正在执行的方法的局部变量和参数
- JNI 方法的局部变量和参数
- 全局 JNI 参考
- 用作同步监视器的对象
- JVM 出于其目的而从垃圾回收中持有的对象
垃圾回收器遍历内存中的整个对象图,从那些垃圾回收根开始,然后跟踪从根到其他对象的引用。
Java 中垃圾回收的阶段
标准的 Garbage Collection 实现包括三个阶段:
将对象标记为活动状态
在此步骤中,GC 通过遍历对象图来识别内存中的所有活动对象。
当 GC 访问一个对象时,它会将其标记为可访问,因此是活动的。垃圾回收器访问的每个对象都标记为活动状态。所有无法从 GC 根访问的对象都是垃圾,并被视为垃圾回收的候选对象。
清扫死对象
在 marking 阶段之后,我们得到了内存空间,它被 live (visited) 和 dead (unvisited) 对象占据。扫描阶段释放包含这些 dead 对象的内存片段。
压缩内存中的剩余对象
在扫描阶段删除的死对象不一定彼此相邻。因此,您最终可能会拥有碎片化的内存空间。
在垃圾回收器删除死对象后,可以压缩内存,以便剩余对象位于堆开始时的连续块中。
压缩过程可以更轻松地按顺序将内存分配给新对象。
什么是 Java 中的分代垃圾回收?
Java 垃圾回收器实现分代垃圾回收策略,该策略按期限对对象进行分类。
必须在 JVM 中标记和压缩所有对象是低效的。随着分配的对象越来越多,对象列表会增长,从而导致垃圾回收时间变长。对应用程序的实证分析表明,Java 中的大多数对象都是短暂的。
在上面的示例中,Y 轴显示分配的字节数,X 轴显示随时间推移分配的字节数。如您所见,随着时间的推移,保持分配的对象越来越少。
事实上,大多数对象的生命周期都非常短,如图表左侧的较高值所示。这就是 Java 将对象分类为几代并相应地执行垃圾回收的原因。
JVM 中的堆内存区域分为三个部分:
年轻一代
新创建的对象从 Young Generation 开始。年轻一代进一步细分为:
- Eden 空间 - 所有新对象都从这里开始,并为它们分配初始内存
- 幸存者空间 (FromSpace 和 ToSpace) - 对象在经历一个垃圾回收周期后从 Eden 移动到这里。
当对象从 Young Generation 进行垃圾回收时,它是一个次要的垃圾回收事件。
当 Eden 空间被对象填满时,将执行 Minor GC。所有活动对象都将被删除,并且所有活动对象都将移动到幸存者空间之一。Minor GC 还会检查幸存者空间中的对象,并将它们移动到另一个幸存者空间。
以以下序列为例:
- Eden 拥有所有对象(活的和死的)
- 发生次要 GC - 从 Eden 中删除所有死对象。所有活动对象都移动到 S1 (FromSpace)。Eden 和 S2 现在是空的。
- 创建新对象并将其添加到 Eden。Eden 和 S1 中的一些对象会失效。
- 发生次要 GC - 从 Eden 和 S1 中删除所有死对象。所有活动对象都将移动到 S2 (ToSpace)。Eden 和 S1 现在是空的。
因此,在任何时候,其中一个幸存者空间总是空的。当幸存的对象达到在幸存者空间中移动的某个阈值时,它们将被移动到 Old Generation。
您可以使用该标志来设置 Young Generation 的大小。-Xmn
老一代
长期存在的对象最终会从 Young Generation 移动到 Old Generation。这也称为 Tenured Generation,包含已在幸存者空间中保留很长时间的对象。
为对象的 tenure 定义了一个阈值,它决定了它在移动到 Old Generation 之前可以存活多少个垃圾回收周期。
当对象从 Old Generation 进行垃圾回收时,这是一个主要的垃圾回收事件。
您可以使用 and 标志来设置堆内存的初始大小和最大大小。-Xms
-Xmx
由于 Java 使用分代垃圾回收,因此对象存活的垃圾回收事件越多,它在堆中的提升就越远。它从年轻一代开始,如果存活足够长,最终会成为终身一代。
请考虑以下示例,以了解对象在空间和层代之间的提升:
当一个对象被创建时,它首先被放入年轻一代的 Eden 空间。一旦发生次要垃圾回收,来自 Eden 的 live objects 就会被提升到 FromSpace。当下一次次要垃圾回收发生时,来自 Eden 和 FromSpace 的活动对象将移动到 ToSpace。
此循环持续特定次数。如果在此之后该对象仍在使用,则下一个垃圾回收周期会将其移动到旧的生成空间。
永久一代
类和方法等元数据存储在 Permanent Generation 中。它由 JVM 在运行时根据应用程序使用的类进行填充。不再使用的类可能是从 Permanent Generation 进行垃圾回收。
您可以使用 和 标志来设置 Permanent Generation 的初始大小和最大大小。-XX:PermGen
-XX:MaxPermGen
元空间
从 Java 8 开始,MetaSpace 内存空间取代了 PermGen 空间。该实现与 PermGen 不同,堆的这个空间现在会自动调整大小。
这避免了由于堆的 PermGen 空间大小有限而导致应用程序内存不足的问题。Metaspace 内存可以被垃圾回收,当 Metaspace 达到其最大大小时,可以自动清理不再使用的类。
Java 虚拟机中的垃圾回收器类型
垃圾回收可以提高 Java 内存效率,因为它从堆内存中删除未引用的对象,并为新对象腾出可用空间。
Java 虚拟机有八种类型的垃圾回收器。让我们详细看看每一个。
串行 GC
这是最简单的 GC 实现,专为在单线程环境中运行的小型应用程序而设计。所有垃圾回收事件都在一个线程中串行执行。压缩在每次垃圾回收后执行。
当它运行时,它会导致一个 “stop the world” 事件,其中整个应用程序都暂停了。由于整个应用程序在垃圾回收期间被冻结,因此不建议在需要低延迟的实际场景中使用。
使用 Serial Garbage Collector 的 JVM 参数为 .-XX:+UseSerialGC
并行 GC
并行收集器适用于在多处理器或多线程硬件上运行的具有中型到大型数据集的应用程序。这是 JVM 中 GC 的默认实现,也称为 Throughput Collector。
多个线程用于 Young Generation 中的次要垃圾回收。在老一代中,单个线程用于主要垃圾回收。
运行 Parallel GC 还会导致 “stop the world event” 并且应用程序冻结。由于它更适合多线程环境,因此当需要完成大量工作并且可以接受长时间暂停时(例如,运行批处理作业),可以使用它。
使用并行垃圾回收器的 JVM 参数是 .-XX:+UseParallelGC
并行旧 GC
这是自 Java 7u4 以来的默认并行 GC 版本。它与并行 GC 相同,只是它对 Young Generation 和 Old Generation 都使用多个线程。
使用 Parallel Garbage Collector 的 JVM 参数是 .-XX:+UseParallelOldGC
CMS(并发标记扫描)GC
这也称为并发低暂停收集器。使用与 Parallel 相同的算法,使用多个线程进行次要垃圾回收。主要垃圾回收是多线程的,就像 Parallel Old GC 一样,但 CMS 与应用程序进程同时运行,以最大限度地减少“停止世界”事件。
因此,CMS 收集器使用的 CPU 比其他 GC 多。如果您可以分配更多的 CPU 以获得更好的性能,那么 CMS 垃圾回收器是比并行回收器更好的选择。在 CMS GC 中不执行压缩。
使用 Concurrent Mark Sweep Garbage Collector 的 JVM 参数是 .-XX:+UseConcMarkSweepGC
G1 (垃圾优先) GC
G1GC 旨在替代 CMS,专为具有较大可用堆大小(超过 4GB)的多线程应用程序而设计。它与 CMS 一样是并行和并发的,但与较旧的垃圾回收器相比,它在后台的工作方式完全不同。
虽然 G1 也是代际的,但它没有针对年轻一代和老一代的单独区域。相反,每一代都是一组区域,这允许以灵活的方式调整年轻一代的大小。
它将堆划分为一组大小相等的区域(1MB 到 32MB – 取决于堆的大小),并使用多个线程来扫描它们。在程序运行期间,区域可以随时是旧区域或年轻区域。
在 mark 阶段完成后,G1 知道哪些区域包含最多的垃圾对象。如果用户对最短的暂停时间感兴趣,G1 可以选择只撤离几个区域。如果用户不担心暂停时间或已提出相当大的暂停时间目标,G1 可能会选择包含更多区域。
由于 G1GC 会识别垃圾最多的区域,并首先对该区域执行垃圾回收,因此称为 Garbage First。
除了 Eden、Survivor 和 Old 内存区域外,G1GC 中还存在另外两种类型的区域:
- Humongous - 用于大型对象(大于堆大小的 50%)
- 可用 - 未使用或未分配的空间
使用 G1 垃圾回收器的 JVM 参数是 .-XX:+UseG1GC
Epsilon 垃圾回收器
Epsilon 是一个无作 (no-op) 垃圾回收器,作为 JDK 11 的一部分发布。它处理内存分配,但不实现任何实际的内存回收机制。一旦可用的 Java 堆耗尽,JVM 就会关闭。
它可用于对延迟非常敏感的应用程序,在这些应用程序中,开发人员可以确切地了解应用程序内存占用情况,甚至拥有(几乎)完全无垃圾的应用程序。否则,不建议在任何其他情况下使用 Epsilon GC。
使用 Epsilon 垃圾回收器的 JVM 参数是 .-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
Shenandoah
Shenandoah 是作为 JDK 12 的一部分发布的新 GC。与 G1 相比,Shenandoah 的主要优势在于,它与应用程序线程同时执行更多的垃圾回收周期工作。G1 只能在应用程序暂停时撤离其堆区域,而 Shenandoah 可以与应用程序同时重新定位对象。
Shenandoah 可以在检测到可用内存后几乎立即压缩活动对象、清理垃圾并将 RAM 释放回作系统。由于所有这些都在应用程序运行时同时发生,因此 Shenandoah 的 CPU 占用量更大。
使用 Epsilon 垃圾回收器的 JVM 参数是 .-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
ZGC
ZGC 是另一个 GC,作为 JDK 11 的一部分发布,并在 JDK 12 中得到了改进。它适用于需要低延迟(暂停时间少于 10 毫秒)和/或使用非常大的堆(数 TB)的应用程序。
ZGC 的主要目标是低延迟、可扩展性和易用性。为了实现这一点,ZGC 允许 Java 应用程序在执行所有垃圾收集作时继续运行。默认情况下,ZGC 会取消提交未使用的内存并将其返回给作系统。
因此,ZGC 通过提供极低的暂停时间(通常在 2ms 内),比其他传统 GC 有了显着改进。
使用 Epsilon 垃圾回收器的 JVM 参数是 .-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
注意:Shenandoah 和 ZGC 都计划在 JDK 15 中成为生产功能并退出实验阶段。
如何选择合适的垃圾回收器
如果您的应用程序没有严格的暂停时间要求,您应该只运行您的应用程序并允许 JVM 选择正确的收集器。
大多数情况下,默认设置应该可以正常工作。如有必要,您可以调整堆大小以提高性能。如果性能仍未达到您的目标,您可以根据应用程序要求修改收集器:
- 串行 - 如果应用程序具有较小的数据集(最多约 100 MB)和/或它将在单个处理器上运行,没有暂停时间要求
- 并行 - 如果优先考虑峰值应用程序性能,并且没有暂停时间要求,或者可以接受 1 秒或更长时间的暂停
- CMS/G1 - 如果响应时间比总体吞吐量更重要,并且垃圾回收暂停时间必须短于大约一秒
- ZGC - 如果响应时间是高优先级的,并且/或您正在使用非常大的堆
垃圾回收的优点
Java 中的垃圾回收有很多好处。
首先,它使您的代码变得简单。您不必担心正确的内存分配和发布周期。您只需停止在代码中使用对象,它正在使用的内存将在某个时候自动回收。
使用没有垃圾回收的语言(如 C 和 C++)的程序员必须在其代码中实现手动内存管理。
它还提高了 Java 的内存效率,因为垃圾回收器从堆内存中删除了未引用的对象。这将释放堆内存以容纳新对象。
虽然一些程序员主张手动内存管理而不是垃圾回收,但垃圾回收现在是许多流行编程语言的标准组件。
对于垃圾回收器对性能产生负面影响的情况,Java 提供了许多选项来调整垃圾回收器以提高其效率。
垃圾回收最佳实践
避免手动触发
除了垃圾回收的基本机制外,了解 Java 中的垃圾回收最重要的一点是它是非确定性的。这意味着无法预测运行时何时进行垃圾回收。
可以在代码中包含提示以使用 or 方法运行垃圾回收器,但它们不能保证垃圾回收器将实际运行。System.gc()
Runtime.gc()
使用分析工具
如果您没有足够的内存来运行应用程序,您将遇到速度变慢、垃圾回收时间长、“停止世界”事件,并最终出现内存不足错误。这可能表示您的堆太小,但也可能意味着您的应用程序中存在内存泄漏。
您可以从 Java Flight Recorder 等监控工具获得帮助,以查看堆使用量是否无限增长,这可能表明您的代码中存在错误。jstat
默认设置为“良好”
如果您正在运行一个小型的独立 Java 应用程序,则很可能不需要任何类型的垃圾回收调整。默认设置应该可以正常工作。
使用 JVM 标志进行优化
调整 Java 垃圾回收的最佳方法是在 JVM 上设置标志。标志可以调整要使用的垃圾回收器(例如 Serial、G1 等)、堆的初始大小和最大大小、堆部分的大小(例如,Young Generation、Old Generation)等。
选择正确的收集器
正在优化的应用程序的性质是设置的良好初始指南。例如,Parallel garbage collector 效率很高,但经常会导致 “stop the world” 事件,因此它更适合于可以接受垃圾回收长时间暂停的后端处理。
另一方面,CMS 垃圾回收器旨在最大限度地减少暂停,使其成为响应能力很重要的基于 Web 的应用程序的理想选择。
结论
在本文中,我们讨论了 Java 垃圾回收、它的工作原理及其各种类型。
对于许多简单的应用程序,Java 垃圾回收不是程序员需要有意识地考虑的事情。但是,对于想要提高 Java 技能的程序员来说,了解 Java 垃圾回收的工作原理非常重要。
这也是一个非常受欢迎的面试问题,无论是初级还是高级后端角色。
开篇:为什么要学虚拟机?http://jvmtutorial.com/#/jvm/jvm_serial_00_why_learn_jvm