Java中的垃圾收集

垃圾收集在Java中是一个高级主题。Java的GC知识帮助我们微调应用程序的运行性能。

Java中的垃圾收集

  • 在Java中,程序员不需要关心销毁不再使用的对象。垃圾收集器会负责这个任务。
  • 垃圾收集器是一个守护线程,在后台持续运行。基本上,它通过销毁不可达对象来释放堆内存
  • 不可达对象是程序的任何部分都不再引用的对象。
  • 我们可以通过JVM选项为我们的Java程序选择垃圾收集器,在本教程的后面部分我们将详细介绍这些选项。

自动垃圾收集的工作原理是怎样的?

自动垃圾收集是一种查看堆内存、识别(也称为“标记”)不可达对象并通过压缩销毁它们的过程。这种方法的一个问题是,随着对象数量的增加,垃圾收集时间不断增加,因为它需要遍历整个对象列表,寻找不可达对象。然而,对应用程序的经验分析显示,大多数对象的生存周期很短。这种行为被用来改进JVM的性能,采用的方法通常称为分代垃圾收集。在这种方法中,堆空间被划分为年轻代、老年代或幼年代和永久代等代。年轻代堆空间是新对象创建的地方。一旦填满,将进行次要垃圾收集(也称为次要GC)。这意味着,这一代的所有死对象都被销毁,这个过程很快,因为从图表中可以看出,大多数对象都已经死亡。年轻代中幸存的对象会逐渐变老并最终转移到老年代。老年代用于存储长期存活的对象。通常,为年轻代对象设置一个阈值,当达到该年龄时,对象会被移动到老年代。最终,需要对老年代进行收集。这个事件被称为主要GC(major garbage collection)。通常情况下,这要慢得多,因为它涉及到所有存活对象。此外,还有全局GC,意味着清理整个堆 – 包括年轻代和老年代空间。最后,在Java 7之前,存在永久代(或Perm Gen),其中包含JVM描述应用程序中使用的类和方法所需的元数据。它在Java 8中被移除。

Java垃圾收集器

JVM实际上提供了四种不同的垃圾收集器,它们都是分代的。每种都有其优缺点。选择使用哪个垃圾收集器取决于我们,吞吐量和应用程序暂停之间可能存在显著差异。所有这些都将托管堆分为不同的段,使用古老的假设,即堆中的大多数对象具有短寿命并且应该快速回收。因此,这四种垃圾收集器分别是:

Serial GC

这是最简单的垃圾收集器,专为单线程系统和小堆大小设计。在工作时冻结所有应用程序。可以使用-XX:+UseSerialGC JVM选项启用。

Parallel/Throughput GC

这是 JDK 8 中的 JVM 默认收集器。顾名思义,它使用多个线程扫描堆空间并执行压缩。该收集器的缺点是,在执行部分或完全 GC 时会暂停应用程序线程。它最适合于能够处理此类暂停并尝试优化收集器引起的 CPU 开销的应用程序。

CMS 收集器

CMS 收集器(“并发标记清除”)算法使用多个线程(“并发”)扫描堆(“标记”)以查找可以回收的未使用对象(“清除”)。此收集器在两种情况下进入停止-世界(STW)模式:-初始化根的初始标记时,即。从线程入口点或静态变量可达的老一代对象-当应用程序在算法正在并发运行时更改堆的状态并强制其返回并进行一些最后的触摸以确保它已标记正确的对象。此收集器可能会面临提升失败。如果要将某些对象从年轻一代移动到老一代,并且收集器没有足够的时间来在老一代空间中腾出空间,则会发生提升失败。为了防止这种情况,我们可以为老一代提供更多的堆大小或为收集器提供更多的后台线程。

垃圾优先收集器

最后但同样重要的是垃圾优先收集器(G1收集器),专为大于4GB的堆大小设计。它将堆大小划分为区域,区间从1MB到32MB,根据堆大小而定。有一个并发的全局标记阶段,用于确定整个堆中对象的存活情况。标记阶段完成后,G1知道哪些区域大多是空的。它首先收集这些区域中的不可达对象,通常会释放大量的空间。因此,G1首先收集这些包含垃圾的区域,因此得名垃圾优先收集器。G1还使用暂停预测模型,以满足用户定义的暂停时间目标。它根据指定的暂停时间目标选择要收集的区域数量。G1垃圾收集周期包括如图所示的阶段:

  1. 仅Young阶段:此阶段仅包括Young代对象,并将它们提升到Old代。在Old代占用到达某个阈值,即初始堆占用阈值时,Young阶段和空间回收阶段之间的转换开始。此时,G1安排进行初始标记的Young阶段收集,而不是常规的Young阶段收集。

  2. 初始标记:这种类型的收集开始标记过程,除了进行常规的年轻阶段收集外。并发标记确定老一代区域中当前存活的所有对象,以便在接下来的空间回收阶段保留它们。在标记尚未完全完成时,可能会发生常规的年轻阶段收集。标记在两个特殊的停顿阶段完成:标记(Remark)和清理(Cleanup)。

  3. 标记(Remark):此停顿最终完成标记本身,并执行全局引用处理和类卸载。在标记和清理之间,G1并发地计算活跃信息的摘要,该摘要将在清理停顿中得以最终确定并用于更新内部数据结构。

  4. 清理:此暂停还考虑完全空的区域,并确定是否实际上会跟随空间回收阶段。如果有空间回收阶段,则仅包含年轻代的阶段将完成一次年轻代收集。

  5. 空间回收阶段:此阶段由多个混合收集组成 – 除了年轻代区域外,还疏散老年代区域的活动对象。当G1确定疏散更多老年代区域不值得付出足够的空间时,空间回收阶段结束。

G1 可以使用 –XX:+UseG1GC 标志启用。该策略降低了在后台线程完成对不可达对象的扫描之前堆耗尽的可能性。此外,它可以在进行堆压缩时进行,而 CMS 收集器只能在 STW 模式下执行。在 Java 8 中,G1 收集器提供了一个称为 字符串去重 的美妙优化。正如我们所知,表示字符串的字符数组占用了大部分堆空间。进行了一项新的优化,使 G1 收集器能够识别堆中多次重复的字符串,并将它们修改为指向相同的内部 char[] 数组,以避免不必要地在堆中存在多个相同的字符串副本。我们可以使用 -XX:+UseStringDeduplication JVM 参数来启用这个优化。G1 是 JDK 9 中的默认垃圾收集器。

Java 8 的 PermGen 和 Metaspace

如前所述,自Java 8起,永久代空间已被移除。因此,JDK 8 HotSpot JVM现在使用本地内存来表示类元数据,称为Metaspace。大多数类元数据的分配都是使用本地内存进行的。此外,还有一个新的标志MaxMetaspaceSize,用于限制用于类元数据的内存量。如果我们不为其指定值,则Metaspace会根据运行应用程序的需求在运行时重新调整大小。当类元数据使用量达到MaxMetaspaceSize限制时,将触发Metaspace垃圾回收。过度的Metaspace垃圾回收可能是类、类加载器内存泄漏或应用程序大小不足的症状。至此,关于Java中的垃圾回收就介绍完毕了。希望你对Java中不同的垃圾收集器有了了解。参考文献:Oracle文档G1 GC

Source:
https://www.digitalocean.com/community/tutorials/garbage-collection-in-java