为什么 gen1/gen2 收集比 gen0 慢?
Why are gen1/gen2 collections slower than gen0?
据我所知,短期对象创建在 GC 方面并不太麻烦 - 这意味着,gen0 收集速度非常快。 Gen1/gen2 然而,集合似乎有点“可怕”,即据说通常比 gen0 慢很多。
这是为什么?是什么让 gen2 收集平均比 gen0 慢得多?
我不知道集合方法本身之间的任何结构差异(即,在 mark/sweep/compaction 阶段完成的事情),我是否遗漏了什么?还是只是那个,例如gen2 往往比 gen0 大,因此要检查的对象更多?
想到的几个原因:
- 它们更大。收集 gen1 意味着也收集 gen0,而做一个 gen2 collection 意味着收集所有三个。较低的几代也较小,因为 gen0 收集最频繁,因此需要便宜。
- collection 的主要成本是存活的 object 数量的函数,而不是死亡数量的函数。分代垃圾收集器是围绕分代假设构建的,该假设表明 objects 往往会存活很短的时间,或很长一段时间,但不会经常活在中间。根据他们的定义,Gen0 collections 主要由在那一代死亡的 objects 组成,因此 collections 很便宜:gen1 和 gen2 collections 有更高的存活下来的 object 的比例(理想情况下,gen2 应该由存活下来的 object 中的 仅 组成),因此更昂贵。
- 如果一个 object 在 gen0 中,那么它只能被其他 gen0 objects 引用,或者被更新为引用它的更高代中的 objects 引用.因此,要查看 gen0 中的 object 是否被引用,GC 需要检查其他 gen0 objects,以及仅检查更高代中已更新指向的那些 objects lower-generation objects(GC 跟踪,参见“卡片表”)。要查看 gen1 object 是否被引用,它需要检查所有 gen0 和 gen1,并更新 gen2 中的 objects。
为了进一步说明 canton7 的回答,有必要注意一些额外的事情,其中之一会增加所有集合(尤其是 gen1 和 gen2)的成本,但会降低它们之间的分配成本,其中之一降低 gen0 和 gen1 集合的成本:
许多垃圾收集者的行为有点类似于清理建筑物,将所有有价值的东西移到另一栋建筑物,炸毁原来的建筑物,然后重建空的 shell。将东西从 gen0 建筑物移动到 gen1 建筑物的 gen0 集合将相当快,因为 gen0“建筑物”中不会有太多东西。 gen2 系列必须移动更大的 gen2 建筑物中的所有东西。垃圾收集系统可以为较小的 gen2 对象和较大的对象使用单独的建筑物,并通过跟踪各个空闲区域 space 来管理较大的建筑物,但是移动较小的对象和回收存储批发比试图管理所有的对象要少工作有资格重新使用的个别存储区域。然而,这里观察几代人的一个关键点是,即使有必要扫描 gen1 或 gen2 对象,也没有必要移动它,因为它所在的“建筑物”不是立即拆除的目标。
许多系统使用“卡片table”,它可以记录自上次以来每个 4K 内存块是否已被写入,或包含用于修改对象的引用gen0 或 gen1 集合。这显着减慢了对任何此类存储区域的首次写入,但在 gen0 和 gen1 收集期间,它可以跳过对许多对象的检查。卡片 table 的使用细节各不相同,但基本概念是,如果代码有大量引用,但其中大部分落在未标记的 4K 块内,GC 甚至无需知道查看那些可以通过它们访问的任何较新对象 也 可以通过其他方式访问的块,因此无需费心查看这些块就可以找到所有 gen0 对象完全没有。
请注意,即使是没有卡 table 的简单 garbage-collection 系统也可以简单轻松地从分代 GC 的原理中获益。例如,在垃圾收集器非常慢的 Commodore 64 BASIC 上,一个创建了大量 long-lived 字符串的程序可以通过使用几个 peek 和 poke 语句来调整 top-of-string-heap 指针位于 long-lived 字符串底部的正下方,因此它们不会被考虑用于 relocation/reclamation。如果一个程序使用数百个字符串,这些字符串将在整个程序执行期间持续存在(例如 table of two-digit 十六进制字符串,从 00 到 FF),并且只有少数其他字符串,这可能会斜线 garbage-collection 倍以上。
据我所知,短期对象创建在 GC 方面并不太麻烦 - 这意味着,gen0 收集速度非常快。 Gen1/gen2 然而,集合似乎有点“可怕”,即据说通常比 gen0 慢很多。
这是为什么?是什么让 gen2 收集平均比 gen0 慢得多?
我不知道集合方法本身之间的任何结构差异(即,在 mark/sweep/compaction 阶段完成的事情),我是否遗漏了什么?还是只是那个,例如gen2 往往比 gen0 大,因此要检查的对象更多?
想到的几个原因:
- 它们更大。收集 gen1 意味着也收集 gen0,而做一个 gen2 collection 意味着收集所有三个。较低的几代也较小,因为 gen0 收集最频繁,因此需要便宜。
- collection 的主要成本是存活的 object 数量的函数,而不是死亡数量的函数。分代垃圾收集器是围绕分代假设构建的,该假设表明 objects 往往会存活很短的时间,或很长一段时间,但不会经常活在中间。根据他们的定义,Gen0 collections 主要由在那一代死亡的 objects 组成,因此 collections 很便宜:gen1 和 gen2 collections 有更高的存活下来的 object 的比例(理想情况下,gen2 应该由存活下来的 object 中的 仅 组成),因此更昂贵。
- 如果一个 object 在 gen0 中,那么它只能被其他 gen0 objects 引用,或者被更新为引用它的更高代中的 objects 引用.因此,要查看 gen0 中的 object 是否被引用,GC 需要检查其他 gen0 objects,以及仅检查更高代中已更新指向的那些 objects lower-generation objects(GC 跟踪,参见“卡片表”)。要查看 gen1 object 是否被引用,它需要检查所有 gen0 和 gen1,并更新 gen2 中的 objects。
为了进一步说明 canton7 的回答,有必要注意一些额外的事情,其中之一会增加所有集合(尤其是 gen1 和 gen2)的成本,但会降低它们之间的分配成本,其中之一降低 gen0 和 gen1 集合的成本:
许多垃圾收集者的行为有点类似于清理建筑物,将所有有价值的东西移到另一栋建筑物,炸毁原来的建筑物,然后重建空的 shell。将东西从 gen0 建筑物移动到 gen1 建筑物的 gen0 集合将相当快,因为 gen0“建筑物”中不会有太多东西。 gen2 系列必须移动更大的 gen2 建筑物中的所有东西。垃圾收集系统可以为较小的 gen2 对象和较大的对象使用单独的建筑物,并通过跟踪各个空闲区域 space 来管理较大的建筑物,但是移动较小的对象和回收存储批发比试图管理所有的对象要少工作有资格重新使用的个别存储区域。然而,这里观察几代人的一个关键点是,即使有必要扫描 gen1 或 gen2 对象,也没有必要移动它,因为它所在的“建筑物”不是立即拆除的目标。
许多系统使用“卡片table”,它可以记录自上次以来每个 4K 内存块是否已被写入,或包含用于修改对象的引用gen0 或 gen1 集合。这显着减慢了对任何此类存储区域的首次写入,但在 gen0 和 gen1 收集期间,它可以跳过对许多对象的检查。卡片 table 的使用细节各不相同,但基本概念是,如果代码有大量引用,但其中大部分落在未标记的 4K 块内,GC 甚至无需知道查看那些可以通过它们访问的任何较新对象 也 可以通过其他方式访问的块,因此无需费心查看这些块就可以找到所有 gen0 对象完全没有。
请注意,即使是没有卡 table 的简单 garbage-collection 系统也可以简单轻松地从分代 GC 的原理中获益。例如,在垃圾收集器非常慢的 Commodore 64 BASIC 上,一个创建了大量 long-lived 字符串的程序可以通过使用几个 peek 和 poke 语句来调整 top-of-string-heap 指针位于 long-lived 字符串底部的正下方,因此它们不会被考虑用于 relocation/reclamation。如果一个程序使用数百个字符串,这些字符串将在整个程序执行期间持续存在(例如 table of two-digit 十六进制字符串,从 00 到 FF),并且只有少数其他字符串,这可能会斜线 garbage-collection 倍以上。