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