Go GC 栈对象与栈跟踪

GC扫描栈

问题的关键在于这段代码:

t := T{...}
p := &t
for {
   if … { p = … }
}

编译器决定在栈上分配 T,并且因为编译器无法跟踪其地址结束的位置,所以编译器保守地决定 t 始终是存活的。

但是在 for 循环中,当 p 被赋值时,t 变成死亡的。
所以在 p 被重新分配之后,t 中的任何指针都可能保持一个它不应该保持的对象存活。

我们将在栈上调用其地址被 “栈对象” 占用的变量,目标是仅在栈对象中的数据真实存活时扫描栈对象,即如果对象再次直接引用(如,t.x)或间接引用(如,p.x)。

我们开始像往常一样扫描一个栈,从最低(最新)的栈帧开始,一直向上。
我们使用指针位图来查找每个栈帧中的所有存活的指针。
通常,栈对象的指针不会标记为存活(但请参见下文)。
所有指向堆的指针都像往常一样被标记,任何指向栈的指针都被忽略(目前)。
对于遇到的每一帧,我们查看该帧是否有栈对象。
对于该帧中的每个栈对象,我们将其添加到栈对象列表中。

如果我们到达栈顶部并找不到栈对象,那么就结束了。

如果我们确实找到了一些栈对象,我们必须再次扫描栈。

首先,我们将栈对象组织成一个数据结构,该结构提供按地址快速查找的功能,可能是一个二叉树。由于我们可以按地址顺序枚举栈对象,所以应该很容易使用它们的地址作为 key 来生成二叉树(在 O(n) 时间内)。

然后我们再次扫描栈,只查找指向栈的指针。
对于任何这样的指针,我们在二叉树中查找它们,看看它们是否指向任何栈对象。
如果是,我们将栈对象标记为存活(如果我们还没有这样做),并将其放入正在扫描的工作列表中。当我们完成栈扫描时,我们现在在工作列表中有了栈对象的根集(root set)。
然后,我们不断从工作列表中弹出一个对象并扫描它(可能将堆对象或其他栈对象标记为存活),直到工作列表为空。

我们可以为每个栈对象分配相邻的空间来使用这个算法。我们称之为栈对象“header”
header 可能包含二叉树的左右指针,一个标记位和用于单链表工作队列的指针。
header 有一些空间开销,但是它应该很小,并且可以自由分配,因为它只是使栈帧更大一点。
header 只需要在第一次扫描是找到栈对象时才初始化。因此我们可以在正常执行期间将垃圾留在那里。

记录栈对象

我们在funcdata中为函数保留了一个额外的映射,该函数包含帧中每个栈对象的帧偏移,大小和指针映射的列表。

请注意,只需要记录具有指针的栈对象。

_FUNCDATA_StackObjects = 4

堆栈对象相对较少,因此栈对象数据的编码不是很关键。
我们可以像这样编码栈对象funcdata

(偏移大小(isptr)*)*

其中,每个对象在帧中编码它的起始偏移量、它的大小以及该对象中每个指针 slotsize/ptrSize 0/1项。

含糊不清的存活对象(ambiguously live objects)

有了这种新机制,我们就可以在当前编译器中为含糊不清的存活对象去掉所有代码。
我们只需要在存活的 ptr 位图中标记那些通过直接访问(而不是通过指针)存活的指针。
我们的新机制包含以前用于跟踪栈对象的逻辑,该逻辑是:存活只是因为指向它们的指针可能是存活的。

其他说明

对象可以作为栈对象被列出,并在存活的 ptr 位图中被标记。
例如:

t := T{}
p := &t
f()
println(t.a)
g()
println(p.b)

在调用 f 时,t 中的指针将在 ptr 位图中标记为存活的(可能只标记 ta 字段)。在调用 g 时不会。

清除

清扫器(sweeper)由两种不同的算法组成:

  • 对象回收器查找和释放 span 中未标记的 slots
    如果没有标记任何对象,它可以释放整个 span,但这不是它的目标。
    可以通过 mcentral.cacheSpan(for mcentral spans)同步驱动,也可以通过 mheap_.sweepSpans 中所有正在使用的 spans 列表中的 sweepone 异步驱动。

  • span 回收器查找不包含标记对象的 span,并释放整个 span
    这是一个单独的算法,因为释放整个 spans 对于对象回收器来说是最困难的任务,但是在分配新 spans 时非常关键。
    这个的入口点是 mheap_.reclaim,它是由堆区域中的页面标记位图的顺序扫描驱动的。

两种算法最终都调用 mspan.sweep,它扫描单个堆 span

栈对象和栈跟踪

堆跟踪解决了确定栈的哪些部分是存活的并且应该被扫描的问题。它作为扫描单个 goroutine 栈的一部分运行。

通常,静态地确定堆栈的哪些部分是存活的很容易,因为用户代码对栈对象有显式的引用(读和写)。
编译器可以进行简单的数据流分析,以确定代码中每个点的栈变量的活跃性。
有关该分析,请参阅:$GOROOT/src/cmd/compile/internal/gc/plive.go

但是,当我们获取堆栈变量的地址时,确定该变量是否仍然存活就不太清楚了。
我们仍然可以查找静态访问,但是通过指向变量的指针进行的访问通常很难静态跟踪。
该指针可以在栈上的函数之间传递,有条件地保留等。

相反,我们将动态跟踪指向栈变量的指针。
所有指向栈分配的变量的指针本身都将位于栈的某个位置(或者在相关位置,如 defer),因此我们可以有效地找到它们。

栈跟踪被组织为迷你的垃圾收集跟踪通道。这个垃圾收集中的对象是栈上的所有变量,这些变量的地址已被占用,它们本身包含一个指针。我们称这些变量为 “栈对象”

我们首先确定堆栈上的所有栈对象和可能指向栈的所有静态存活指针。然后我们处理每个指针,看它是否指向栈对象。如果是,我们扫描该栈对象。栈对象可能包含指向堆的指针,在这种情况下,这些指针将被传递给 主 GC。栈对象也可能包含指向栈的指针,在这种情况下,我们将它们添加到栈指针集中。

一旦我们处理完所有指针(包括我们在处理过程中添加的指针),我们就找到了所有存活的栈对象。
任何死亡的栈对象都不会被扫描,并且它们的内容也不会保持堆对象存活。与 主 GC 不同,我们无法清除死亡的栈对象;它们一直处于垂死状态,直到包含它们的栈帧被弹出。

栈可能如下所示:

 +----------+
 | foo()    |
 | +------+ |
 | |  A   | | <---\
 | +------+ |     |
 |          |     |
 | +------+ |     |
 | |  B   | |     |
 | +------+ |     |
 |          |     |
 +----------+     |
 | bar()    |     |
 | +------+ |     |
 | |  C   | | <-\ |
 | +----|-+ |   | |
 |      |   |   | |
 | +----v-+ |   | |
 | |  D  ---------/
 | +------+ |   |
 |          |   |
 +----------+   |
 | baz()    |   |
 | +------+ |   |
 | |  E  -------/
 | +------+ |
 |      ^   |
 | F: --/   |
 |          |
 +----------+

foo() 调用 bar() 调用 baz()。每个方法在栈上都有一个桢。

foo() 有栈对象 AB

bar() 有栈对象 CD, C 指向 D, D 指向 A

baz() 有一个栈对象 E 指向 C,一个局部变量 F 指向 E

从局部变量 F 中的指针开始,我们最终将扫描所有 ECDA (按照这个顺序)。

B 不会被扫描,因为没有指向它的存活指针。

如果 B 也是静态死的(这意味着 foo() 在调用 bar() 之后再也不会访问 B),那么 B 指向堆的指针就不被认为是存活的。

tips:栈对象与栈扫描

// stackObject 表示栈上的一个变量,该变量的地址已被占用。
//
//go:notinheap
type stackObject struct {
    off   uint32       // stack.lo 之上的偏移
    size  uint32       // 对象的大小
    typ   *_type       // 类型信息(对于 ptr/nonptr 位),如果对象已被扫描,则为 nil
    left  *stackObject // 低地址对象
    right *stackObject // 高地址对象
}

// 如上可以看出栈对象是一个双向链表
// 参见:go1.12beta2/src/runtime/mgcstack.go


// scanstack 扫描 goroutine 的栈,标灰所有在该栈上找到的指针。
//
// scanstack 被标记为 go:systemstack,因为它在使用 workbuf 时不能被抢占。
//
//go:nowritebarrier
//go:systemstack
func scanstack(gp *g, gcw *gcWork) {
......
    var state stackScanState
    state.stack = gp.stack
......
    // 查找并且扫描所有可达的栈对象。
    // buildIndex 将 state.root 初始化为二叉搜索树。
    // 应该在所有 addObject 调用之后但在调用 findObject 之前调用它。
    // 使用栈对象数组构造了一颗二叉查找树。
    state.buildIndex()
    for {

        // 删除并返回指向堆栈对象的潜在指针。
        // 如果没有更多可用指针,则返回0。
        p := state.getPtr()
        if p == 0 {
            break
         }

        // findObject 返回包含地址 a 的堆栈对象(如果有)
        obj := state.findObject(p)
        if obj == nil {
            continue
         }
......
     }
......
}

// 参见:go1.12beta2/src/runtime/mgcmark.go

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,174评论 11 349
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,527评论 3 83
  • 第二部分 自动内存管理机制 第二章 java内存异常与内存溢出异常 运行数据区域 程序计数器:当前线程所执行的字节...
    小明oh阅读 1,126评论 0 2
  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,690评论 0 11