用户程序通过内存分配器(Allocator)在堆上申请内存,而垃圾收集器(Collector)负责回收堆上的内存空间,内存分配器和垃圾收集器共同管理程序中的堆内存空间。

基本概念

垃圾分类

  • 语义垃圾:也就是内存泄漏,指的是从语法上可达的对象,也就是被其他对象引用的,但是从语义上来讲是垃圾。这类垃圾,垃圾回收是不管的
  • 语法垃圾:从语法上是不可达的对象,也就是没有对象引用了,这些垃圾是垃圾回收重点照顾的对象

垃圾回收算法

常见的垃圾回收算法:

  • 引用计数:

    某个对象的根引用计数变为0时,其所有节点均需被回收

  • 标记压缩:

    存活对象移动到一起,有效解决内存碎片问题

  • 复制算法:

    将所有正在使用的对象从From空间复制到To空间,堆利用率只有一半,也能解决内存碎片问题

  • 标记清除:

    标记垃圾对象,然后再清除。解决不了内存碎片问题,需要与能尽量避免内存碎片的内存分配器使用,如tcmalloc

标记清除

标记清除算法是最常见的垃圾收集算法,标记清除收集器是跟踪式垃圾收集器,其执行过程可以分为标记、清除两个阶段:

  1. 标记阶段:从根对象出发查找并标记堆中所有存活的对象
  2. 清除阶段:遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表

传统的标记清除算法,垃圾收集器从垃圾收集的根对象出发,递归遍历这些对象指向的子对象并将所有可达的对象标记成存活。标记结束后,垃圾收集器会依次遍历堆中的对象并清除其中的垃圾,整个过程需要标记对象的存活状态,所以用户程序在垃圾收集的过程中不能执行,也就是常说的STW(Stop The World)

三色抽象

为了解决原始标记清除算法带来的长时间STW,大部分追踪式垃圾收集器都会实现三色标记算法的变种以缩短STW的时间
三色标记算法将程序中的对象分成白色、黑色、灰色:

  • 白色对象:潜在的垃圾,也就是没有被扫到的对象,其内存可能会被垃圾收集器回收
  • 黑色对象:活跃的对象,包括不存在任何引用外部指针的对象以及从根对象可达的对象
  • 灰色对象:活跃的对象,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象

标记过程:
垃圾收集器开始工作的时候,不存在任何黑色对象,根对象会被标记成灰色,垃圾收集器只会从灰色对象集合中取出对象开始扫描,当灰色集合中不存在任何对象时,标记就会结束。

大致工作原理:

  1. 从灰色对象的集合中选择一个灰色对象并将其标记成黑色
  2. 将黑色对象指向的所有对象都标记成灰色,保证该对象和被该对象引用的对象都不会被回收
  3. 重复上述两个步骤直到不存在灰色对象

当三色标记结束后,应用程序的堆中就不存在任何灰色对象,我们只能看到黑色的存活对象以及白色的垃圾对象,垃圾收集器可以回收这些白色的垃圾。
缺陷:
由于用户程序可能在标记执行的过程中修改对象的指针,所以三色标记清除算法本身是不可以并发或者增量执行的,仍需要STW。如果本来不应该被回收的对象被回收了,这在内存管理中是非常严重的错误,这种错误称为悬挂指针,也就是指针没有指向特定类型的合法对象,影响了内存的安全性,想要并发或者增量标记对象就需要使用屏障技术。

屏障技术

内存屏障技术是一种屏障指令,可以让CPU或编译器在执行内存相关的操作时遵循特定的约束,目前多数的现代处理器都会乱序执行指令以最大化性能,但是该技术能够保证内存操作的顺序性,在内存屏障前执行的操作一定会先于内存屏障后执行的操作。想在并发或增量的标记算法中保证正确性,需要达到两种三色不变性的其中一种。

三色不变性:

  • 强三色不变性:黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象
  • 弱三色不变性:黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径

屏障技术分为读屏障和写屏障,但是由于读屏障需要在读操作中加入代码片段,所以对用户程序的性能影响较大。解析一下go语言中使用的两种写屏障技术,插入写屏障和删除写屏障。

插入写屏障(DIJKSTRA)

writePointer(slot, ptr):
    shade(ptr)
    *slot = ptr

每当要执行*slot = ptr时,会先执行写屏障通过shade函数尝试改变指针颜色。如果ptr指针是白色,那么会将该对象设置成灰色。

这张图中可以看到,在一次正常的标记过程中,发生了用户程序修改了指针引用(或者新插入了一个引用关系)的情况,如果我们采用 插入写屏障 我们就需要将新指向的对象标为灰色,以此保证强三色不变性。(对于新指向的对象来说,属于被插入一个引用,所以叫插入写屏障)

在插入写屏障中,我们的标记过程变成(关键第2步):

  1. 基本标记流程不变,我们快进到垃圾收集器将根对象指向A对象标记成黑色,并将A对象指向的对象B标记成灰色。
  2. 这时用户程序修改A对象的指针,将原本A对象指向B对象的指针指向C对象,这个时候就会触发写屏障将C对象标记为灰色,避免被错误回收。
  3. 垃圾收集器依次遍历程序中的其他灰色对象,将他们分别标记成黑色

删除写屏障(YUASA)

writePointer(slot, ptr)
    shade(*slot)
    *slot = ptr

删除写屏障会在老对象的引用被删除的时候,将白色的老对象涂成灰色,这样就可以保证弱三色不变性,老对象引用的下游对象一定可以被灰色对象引用。

使用删除写屏障技术的垃圾收集器和用户程序交替运行的场景中的标记过程:

  1. 垃圾收集器将根对象指向A对象标记成黑色并将A对象指向的对象B标记成灰色
  2. 如果这时用户程序将B指向C的指针删除,那么C触发删除写屏障,由于C是白色,所以被涂成灰色
  3. 垃圾收集器依次遍历程序中的其他灰色对象,将他们分别标记成黑色

第二步触发删除写屏障的着色,因为删除了B指向C的指针,所以C和D分别违反强三色不变性和弱三色不变性,着色后保证了三色不变性,避免悬挂指针。

简单来说就是,在改变指针指向的时候,原来指向的那个对象是白色的话就要变成灰色,以此保证弱三色不变性。(对于老对象来说,引用关系被解除了,所以叫删除写屏障)

增量和并发

两种策略优化垃圾收集器不会以为回收垃圾导致长时间STW:

  • 增量垃圾收集:增量得标记和清除垃圾,降低应用程序暂停的最长时间
  • 并发垃圾手机:利用多核的计算资源,在用户程序执行时并发标记和清除垃圾

由于两种方式都需要垃圾收集器与用户程序交替执行,所以需要配合屏障技术。

增量收集器

增量收集器将原本时间较长的暂停时间切分成多个更小的GC时间片。增量收集器需要配合三色标记法和屏障技术一起使用。将GC过程分段执行,虽然拉长了总的垃圾回收时间,但是减少了程序STW的时间。不过写屏障还是有些开销的。

并发收集器

利用多核优势,将GC过程与用户程序并行执行(大部分情况下),也是要配合屏障技术。

References

  1. https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/
  2. https://spin.atomicobject.com/2014/09/03/visualizing-garbage-collection-algorithms/

版权声明:本文为codexiaoyi原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/codexiaoyi/p/15218049.html