Java
作为 Java 开发人员,对垃圾收集的概念并不陌生。应用程序一直在生成垃圾,这些垃圾会被 CMS、G1、Azul C4 和其他类型的收集器给仔细清理掉。基本上,应用程序生来就是为这个世界带来价值的。但是,没有什么是完美的——包括在 Java 堆中留下垃圾的应用程序。
然而,这并没有结束于 Java 堆。事实上,这只是开始。下面将基于一个基本的 Java 应用程序为例,使用了关系数据库(如 PostgreSQL)和固态硬盘(SSD)作为存储设备。这里将探索应用程序是如何在超出 Java 运行的界限时生成垃圾。

用死亡元组填充关系数据库

当 Java 应用程序对 PostgreSQL 数据库执行 DELETE 或 UPDATE 语句时,删除的记录不会立即删除,现有的记录也不会更新。相反,被删除的记录会被标记为死亡元组,并将其保留在存储中。实际上,更新后的记录是一个全新的记录, PostgreSQL 通过赋值前一个版本的记录并更新请求的列来插入。该更新记录的前一个版本会被认为已删除,并且和 DELETE 操作一样,被标记为死亡元组。
数据库引擎在其存储中保存已删除和更新记录的旧版本是有原因的。对于初学者而言,应用程序可以在 PostgreSQL 上并行运行一堆事务。其中一些事务确实比其他事务开始得更早。但是,如果一个事务删除了一条记录,而这条记录对于之前启动的一些事务来说仍然是有意义的,那么需要将该记录保存在数据库中(至少在所有较早开始的事务完成之前)。这就是 PostgreSQL 如何实现 MVCC(多版本并发协议)的。
很显然,PostgreSQL 不能也不想永远保存死亡元组。这就是为什么数据库有自己的垃圾收集过程,称之为 VACUUM(真空吸尘器)。VACUUM 有两种类型——普通的和完整地。普通的 VACUUM 与应用程序负载并行工作,不会阻塞查询。这种类型的 VACUUM 会将死亡元组占用的空间标记为空闲空间,使其可用于应用程序稍后将添加到同一个表中的新数据。普通的 VACUUM 不会将空间返回给操作系统,这样它就可以被其他表或第三方应用程序重用(除非在某些极端情况下,一个页只包含死亡元组,并且该页位于表的末尾)。
Java 应用程序是如何在堆之外丢弃垃圾? - 图1
相比之下,完整的 VACUUM 会将空闲空间回收给操作系统,但是它会阻塞应用程序的工作负载。可以将其视为 Java 的 STW(Stop The World) 垃圾收集暂停。只有在 PostgreSQL 中,这样的暂停可以持续数小时(或数天)。因此,数据库管理员尽最大努力防止发生完全的真空。

在固态硬盘上生成过期数据

如果认为垃圾收集只适用于软件,那么将会发生许多意外!一些硬件设备也需要执行垃圾收集例程。SSD 一直在进行垃圾收集!
每当Java 应用程序删除或更新磁盘上的任何数据(通过上面讨论的 PostgreSQL 或直接通过 Java 文件 API),然后应用程序会在 SSD 上生成垃圾。
SSD 是以页面的形式存储数据(通常大小在 4KB 到 16KB 之间),后者以块的形式分组。虽然可以在页面级写入或读取数据,但过时的(已删除的)数据只能在块级删除。与读/写操作相比,删除操作需要更多的电压,并且很难在不影响相邻单元的情况下在页面级指定电压。
所以,如果Java应用更新了一个文件,那么,实际上,一个更新的段将被写入一个空页,可能在不同的块中。带有旧数据的段将被标记为陈旧的,稍后将被垃圾收集。首先,ssd中的垃圾收集器遍历带有过期数据的页面块,并将好的数据移动到其他块(类似于Java G1收集器中的压缩阶段)。其次,收集器擦除只剩下过时数据的块,并使这些块对未来的数据可用。
Java 应用程序是如何在堆之外丢弃垃圾? - 图2

总结

总的来说,垃圾收集是一种广泛使用的技术,其使用范围远远超出Java生态系统。如果实现得当,垃圾收集可以简化软件和硬件的架构,而不会影响性能。Java、PostgreSQL 和 ssd 都是很好的例子,它们成功地利用了垃圾收集,并且仍然是同类产品中的佼佼者。