Java 应用程序如何在堆外乱扔垃圾

作为 Java 开发人员,我们对垃圾收集的概念并不陌生。我们的应用程序一直在产生垃圾,这些垃圾被 CMS、G1、Azul C4 和其他类型的收集器精心清理。基本上,我们的应用程序生来就是为了给这个世界带来价值,但是,没有什么是完美的——包括我们在 Java 堆中留下垃圾的应用程序。

然而,故事并没有以 Java 堆结束。事实上,它只是从那里开始。让我们以一个基本的 Java 应用程序为例,该应用程序使用关系数据库(如 PostgreSQL)和固态驱动器 (SSD) 作为存储设备。从这里,我们将探索我们的应用程序如何在 Java 运行时边界之外生成垃圾。

用死元组填充 PostgreSQL

当您的 Java 应用程序对 PostgreSQL 数据库执行 DELETE 或 UPDATE 语句时,不会立即删除已删除的记录,也不会在其位置更新现有记录。相反,删除的记录被标记为死元组并将保留在存储中。更新的记录实际上是 PostgreSQL 通过复制先前版本的记录并更新请求的列来插入的全新记录。该更新记录的先前版本被视为已删除,并且与 DELETE 操作一样,被标记为死元组。

数据库引擎在其存储中保留旧版本的已删除和更新记录是有充分理由的。对于初学者,您的应用程序可以针对 PostgreSQL 并行运行一堆事务。其中一些交易确实比其他交易更早开始。但是,如果一个事务删除了一个记录,而该记录可能对一些较早开始的事务仍然感兴趣,那么该记录需要保存在数据库中(至少直到所有较早开始的事务完成的时间点)。这就是 PostgreSQL 实现 MVCC(多版本并发协议)的方式

很明显,PostgreSQL 不能也不想永远保留死元组。这就是为什么数据库有自己的垃圾收集过程,称为 清理。有两种类型的 VACUUM—— 普通的和完整的。普通的 VACUUM 与您的应用程序工作负载并行工作,并且不会阻止您的查询。这种类型的清理将死元组占用的空间标记为空闲,使其可用于您的应用稍后将添加到同一个表中的新数据。普通的 VACUUM 不会将空间返回给操作系统,以便它可以被其他表或 3rd 方应用程序重用(在某些极端情况下,当页面仅包含死元组并且页面位于表的末尾时) .

并发 VACUUM 的一个例子

相比之下,完整的 VACUUM 确实将可用空间回收给操作系统,但它会阻止应用程序工作负载。您可以将其视为 Java 的“stop-the-world”垃圾收集暂停。只有在 PostgreSQL 中,这样的暂停才能持续数小时(或数天)。因此,数据库管理员尽最大努力防止完全 VACUUM 发生。

让我在这里停下来,进入下一个层次——SSD。 如果您想更深入地了解吸尘,请查看 这篇演示驱动的文章。

在 SSD 中生成陈旧数据

如果您认为垃圾收集只是为了软件,那么……惊喜,惊喜!一些硬件设备也需要执行垃圾收集例程。 SSD 一直在做垃圾收集!

每当您的 Java 应用程序删除或更新磁盘上的任何数据时——通过上面讨论的 PostgreSQL 或直接通过 Java 文件 API——应用程序就会在 SSD 上生成垃圾。

SSD 将数据存储在页面中(通常大小在 4KB 到 16KB 之间),后者按块分组。虽然您的数据可以在页面级别写入或读取,但陈旧(已删除)的数据只能在块级别擦除。擦除需要比读/写操作更多的电压,并且很难在不影响相邻单元的情况下以页面级别为目标。

因此,如果您的 Java 应用程序更新了文件,那么事实上,更新的段将被写入可能位于不同块中的空页面。带有旧数据的段将被标记为过时,稍后将被垃圾收集。首先,  SSD 中的垃圾收集器 遍历具有陈旧数据的页面块,并将好的数据移动到其他块(类似于 Java 的 G1 收集器中的压缩阶段)。其次,收集器擦除只剩下陈旧数据的块,并使这些块可用于未来的数据。

SSD 中的垃圾收集示例

好奇 SSD 制造商如何防止或尽量减少“stop-the-world”暂停的次数?有一个 SSD 过度配置的概念,即每台设备都有一个额外的空间供您的应用程序使用。该空间是一种安全缓冲区,允许应用程序继续写入或修改数据,同时垃圾收集器同时擦除陈旧数据。在此处阅读有关过度配置的更多信息 。

概括

因此,下次有人要求您解释 Java 垃圾收集的内部原理时,请继续通过将主题扩展到包括数据库和硬件来让他们感到惊讶。

严肃地说,垃圾收集是一种广泛使用的技术,其使用范围远远超出 Java 生态系统。如果实施得当,垃圾收集可以在不影响性能的情况下简化软件和硬件的架构。Java、PostgreSQL 和 SSD 都是成功利用垃圾收集的产品的好例子,并且仍然是其类别中的顶级产品。