垃圾回收器的相关知识点总结

2020-06-17 05:36:08易采站长站整理

为了解决这个问题,实际上在写缓冲区中有一个列表,列表中记录了所有老生区对象指向新生区的情况。新对象诞生的时候,并不会有指向它的指针,而当有老生区中的对象出现指向新生区对象的指针时,我们便记录下来这样的跨区指向。由于这种记录行为总是发生在写操作时,它被称为写屏障——因为每个写操作都要经历这样一关。

你可能好奇,如果每次进行写操作都要经过写屏障,岂不是会多出大量的代码么?没错,这就是我们这种垃圾回收机制的代价之一。但情况没你想象的那么严重,写操作毕竟比读操作要少。某些垃圾回收算法(不是V8的)会采用读屏障,而这需要硬件来辅助才能保证一个较低的消耗。V8也有一些优化来降低写屏障带来的消耗:

大多数的脚本执行时间都是发生在Crankshaft当中的,而Crankshaft常常能静态地判断出某个对象是否处于新生区。对于指向这些对象的写操作,可以无需写屏障。

Crankshaft中新出现了一种优化,即在对象不存在指向它的非局部引用时,该对象会被分配在栈上。而一个栈上对象的相关写操作显然无需写屏障。(译注:新生区和老生区在堆上。)

“老→新”这样的情况相对较为少见,因此通过将“新→新”和“老→老”两种常见情况的代码做优化,可以相对提升多数情形下的性能。每个页都以1MB对齐,因此给定一个对象的内存地址,通过将低20bit滤除来快速定位其所在的页;而页头有相关的标识来表明其属于新生区还是老生区,因此通过判断两个对象所属的区域,也可以快速确定是否是“老→新”。

一旦我们找到“老→新”的指针,我们就可以将其记录在写缓冲区的末端。经过一定的时间(写缓冲区满的时候),我们将其排序,合并相同的项目,然后再除去已经不符合“老→新”这一情形的指针。(译注:这样指针的数目就会减少,写屏障的时间相应也会缩短)

“标记-清除”算法与“标记-紧缩”算法

Scavenge算法对于快速回收、紧缩小片内存效果很好,但对于大片内存则消耗过大。因为Scavenge算法需要出区和入区两个区域,这对于小片内存尚可,而对于超过数MB的内存就开始变得不切实际了。老生区包含有上百MB的数据,对于这么大的区域,我们采取另外两种相互较为接近的算法:“标记-清除”算法与“标记-紧缩”算法。

这两种算法都包括两个阶段:标记阶段,清除或紧缩阶段。

在标记阶段,所有堆上的活跃对象都会被标记。每个页都会包含一个用来标记的位图,位图中的每一位对应页中的一字(译注:一个指针就是一字大小)。这个标记非常有必要,因为指针可能会在任何字对齐的地方出现。显然,这样的位图要占据一定的空间(32位系统上占据3.1%,64位系统上占据1.6%),但所有的内存管理机制都需要这样占用,因此这种做法并不过分。除此之外,另有2位来表示标记对象的状态。由于对象至少有2字长,因此这些位不会重叠。状态一共有三种:如果一个对象的状态为白,那么它尚未被垃圾回收器发现;如果一个对象的状态为灰,那么它已被垃圾回收器发现,但它的邻接对象仍未全部处理完毕;如果一个对象的状态为黑,则它不仅被垃圾回收器发现,而且其所有邻接对象也都处理完毕。