Java 教程

Java 控制语句

面向对象编程

Java 内置类

Java 文件处理

Java 错误和异常

Java 多线程

Java 同步

Java 网络编程

Java 集合

Java 接口

Java 数据结构

Java 集合算法

高级 Java

Java 杂项

Java API 和框架

Java 类引用

Java 有用资源

Java - 垃圾回收



一个Java对象的生命周期由JVM管理。一旦程序员创建了一个对象,我们就无需担心它的其余生命周期。JVM 将自动查找不再使用的对象,并从堆中回收它们的内存。

什么是 Java 垃圾回收?

垃圾回收是 JVM 执行的一项主要操作,对其进行调整以满足我们的需求可以极大地提高应用程序的性能。现代 JVM 提供了各种各样的垃圾回收算法。我们需要了解应用程序的需求才能决定使用哪种算法。

您不能像在 C 和 C++ 等非 GC 语言中那样以编程方式释放 Java 中的对象。因此,您在 Java 中不会有悬空引用。但是,您可能有空引用(引用指向 JVM 永远不会存储对象的内存区域)。每当使用空引用时,JVM 都会抛出 NullPointerException。

请注意,由于GC,在 Java 程序中很少发现内存泄漏,但它们确实会发生。我们将在本章末尾创建一个内存泄漏。

垃圾收集器的类型

现代 JVM 使用以下 GC

  • 串行收集器
  • 吞吐量收集器
  • CMS 收集器
  • G1 收集器

上述每种算法都执行相同的任务 - 查找对象,这些对象不再使用,并回收它们在堆中占用的内存。一种简单的方法是计算每个对象具有的引用数,并在引用数变为 0 时将其释放(这也称为引用计数)。为什么这是天真的?考虑一个循环链表。它的每个节点都会对其有一个引用,但是整个对象并没有从任何地方被引用,理想情况下应该被释放。

内存合并

JVM 不仅释放内存,还会将小的内存块合并成更大的内存块。这样做是为了防止内存碎片。

简单来说,典型的 GC 算法执行以下活动:

  • 查找未使用的对象
  • 释放它们在堆中占用的内存
  • 合并碎片

GC 在运行时必须停止应用程序线程。这是因为它在运行时会移动对象,因此无法使用这些对象。此类停止称为“停止世界”暂停,我们的目标是在调整 GC 时最大限度地减少这些暂停的频率和持续时间。

下面显示了一个内存合并的简单演示

阴影部分是需要释放的对象。即使在回收所有空间后,我们也只能分配最大大小为 75Kb 的对象。即使我们有 200Kb 的可用空间,如下所示

垃圾回收中的代

大多数 JVM 将堆分为三代:年轻代 (YG)、老年代 (OG) 和永久代(也称为终身代)

我们来看一个简单的例子。Java 中的 String 类是不可变的。这意味着每次您需要更改 String 对象的内容时,都必须创建一个全新的对象。假设您在循环中对字符串进行了 1000 次更改,如下面的代码所示:

String str = "G11 GC";

for(int i = 0 ; i < 1000; i++) {
   str = str + String.valueOf(i);
}

在每次循环中,我们都会创建一个新的字符串对象,而上一次迭代创建的字符串就变得无用了(也就是说,没有任何引用指向它)。该对象的生存期只有一次迭代——它们很快就会被垃圾收集器 (GC) 回收。这种短暂存在的对象保存在堆的年轻代区域中。从年轻代收集对象的进程称为次要垃圾回收,它总是会导致“stop-the-world”暂停。

次要垃圾回收

随着年轻代被填满,GC 会进行次要垃圾回收。已死亡的对象会被丢弃,而存活的对象会被移动到老年代。在此过程中,应用程序线程会停止。

在这里,我们可以看到这种分代设计带来的优势。年轻代只是堆的一小部分,很快就满了。但是处理它所需的时间远远小于处理整个堆所需的时间。因此,在这种情况下,“stop-the-world”暂停时间要短得多,尽管频率更高。我们应该始终追求更短的暂停时间,即使它们可能更频繁。

完全垃圾回收

年轻代被分为两个空间——**伊甸园区 (eden)** 和**幸存者区 (survivor space)**。在伊甸园区收集期间幸存下来的对象会被移动到幸存者区,而那些在幸存者区幸存下来的对象会被移动到老年代。年轻代在收集时会被压缩。

随着对象被移动到老年代,它最终会被填满,必须进行收集和压缩。不同的算法采用不同的方法来实现这一点。有些算法会停止应用程序线程(这会导致长时间的“stop-the-world”暂停,因为老年代与年轻代相比相当大),而有些算法则在应用程序线程继续运行的同时并发地执行此操作。这个过程称为完全 GC (full GC)。CMS 和 G1 就是两种这样的垃圾收集器。

垃圾收集器调优

我们也可以根据需要调整垃圾收集器。以下是可以根据情况配置的区域

  • 堆大小分配
  • 分代大小分配
  • 永久代和元空间配置

让我们在了解它们的影响的同时详细了解每一个方面。我们还将讨论基于可用内存、CPU 配置和其他相关因素的建议。

堆大小分配

堆大小是 Java 应用程序性能的一个重要因素。如果它太小,它将频繁填满,结果将不得不被 GC 频繁收集。另一方面,如果我们只是增加堆的大小,虽然它需要收集的频率较低,但暂停的长度会增加。

此外,增加堆大小会对底层操作系统造成严重的惩罚。通过分页,操作系统使我们的应用程序程序看到的内存比实际可用的内存多得多。操作系统通过使用磁盘上的某些交换空间来管理这一点,将程序的非活动部分复制到其中。当需要这些部分时,操作系统会将它们从磁盘复制回内存。

让我们假设一台机器有 8G 内存,而 JVM 看到了 16G 虚拟内存,JVM 将不知道实际上系统上只有 8G 可用。它只会向操作系统请求 16G,一旦获得该内存,它将继续使用它。操作系统将不得不大量地交换数据进出,这对系统来说是一个巨大的性能损失。

然后是这种虚拟内存完全 GC 期间发生的暂停。由于 GC 将对整个堆进行收集和压缩,它将不得不等待很长时间才能将虚拟内存从磁盘交换出去。如果是并发收集器,后台线程将不得不等待很长时间才能将数据从交换空间复制到内存。

因此,这里就出现了如何确定最佳堆大小的问题。第一条规则是永远不要向操作系统请求超过实际存在的内存。这将完全防止频繁交换的问题。如果机器安装并运行多个 JVM,则所有 JVM 的总内存请求小于系统中实际存在的RAM

您可以使用两个标志控制 JVM 的内存请求大小:

  • -XmsN − 控制请求的初始内存。
  • -XmxN − 控制可以请求的最大内存。

这两个标志的默认值取决于底层操作系统。例如,对于在 MacOS 上运行的 64 位 JVM,-XmsN = 64M,-XmxN = 最小 1G 或总物理内存的 1/4。

请注意,JVM 可以自动在这两个值之间进行调整。例如,如果它注意到 GC 发生得太频繁,它将不断增加内存大小,只要它低于 -XmxN 并满足所需的性能目标。

如果您确切知道您的应用程序需要多少内存,那么您可以设置 -XmsN = -XmxN。在这种情况下,JVM 不需要计算堆的“最佳”值,因此 GC 过程变得更高效。

分代大小分配

您可以决定要为年轻代分配多少堆内存,以及要为老年代分配多少堆内存。这两个值都会以以下方式影响应用程序的性能。

如果年轻代的大小非常大,那么它将被收集的频率较低。这将导致晋升到老年代的对象数量较少。另一方面,如果您过分增加老年代的大小,那么收集和压缩它将花费太多时间,这将导致长时间的 STW 暂停。因此,用户必须在这两个值之间找到平衡。

以下是您可以用来设置这些值的标志:

  • -XX:NewRatio=N:年轻代与老年代的比率(默认值为 2)
  • -XX:NewSize=N:年轻代的初始大小
  • -XX:MaxNewSize=N:年轻代的最大大小
  • -XmnN:使用此标志将 NewSize 和 MaxNewSize 设置为相同的值

年轻代的初始大小由 NewRatio 的值根据以下公式确定:

(total heap size) / (newRatio + 1)

由于 newRatio 的初始值为 2,上述公式得出年轻代的初始值为总堆大小的 1/3。您可以始终通过显式指定年轻代的大小来覆盖此值,使用 NewSize 标志。此标志没有任何默认值,如果未显式设置,年轻代的大小将继续使用上述公式计算。

永久代和元空间配置

永久代和元空间是 JVM 保留类元数据的堆区域。在 Java 7 中,该空间称为“永久代”,在 Java 8 中,它被称为“元空间”。编译器和运行时使用此信息。
您可以使用以下标志控制永久代的大小:-XX:PermSize=N-XX:MaxPermSize=N。元空间的大小可以使用:-XX:MetaspaceSize=N-XX:MaxMetaspaceSize=N 控制。

未设置标志值时,永久代和元空间的管理方式存在一些差异。默认情况下,两者都有一个默认的初始大小。但是,虽然元空间可以占用所需的大量堆内存,但永久代最多只能占用默认初始值。例如,64 位 JVM 的最大永久代大小为 82M。

请注意,除非指定不占用,否则元空间可以占用无限量的内存,因此可能会出现内存不足错误。每当这些区域调整大小时,都会发生完全 GC。因此,在启动过程中,如果有很多类正在加载,元空间可能会不断调整大小,每次都会导致完全 GC。因此,对于大型应用程序来说,如果初始元空间大小太小,启动时间会很长。最好增加初始大小,因为它可以减少启动时间。

尽管永久代和元空间保存类元数据,但它不是永久的,并且像对象一样,空间会被 GC 回收。这通常是服务器应用程序的情况。每当您向服务器进行新的部署时,都必须清理旧的元数据,因为新的类加载器现在需要空间。此空间由 GC 释放。

广告