返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

2.4 回收

发布于 2024-10-12 19:16:02 字数 4623 浏览 0 评论 0 收藏 0

内存回收由垃圾回收器引发,内存分配器执行。

缓存

缓存部件(cache)是垃圾回收作根对象。在 mark 之前,它们所持有的 span 被重新放回 central 。如此,剩余闲置内存(span.objects)就可调配给其他 cache 使用,避免浪费。

// mgcmark.go

func markroot(gcw *gcWork, i uint32) {
    flushmcache(...)
}
// mstats.go

// flushmcache flushes the mcache of allp[i].

func flushmcache(i int) {
    p := allp[i]
    c := p.mcache
    
    c.releaseAll()
    stackcache_clear(c)
}

扩容时,cache 每次从 central 提取一个 span,但不保证会将该 span 内的 object 全部分配出去。

如此,剩余内存会被闲置。将包含剩余空间的 span 重新放回 central,就可转交给其他 cache,从而平衡资源使用,避免内存浪费。


不用担心一个 span 被多个 cache 使用会有什么影响。
分配操作只是从内存块中 “切” 出一个片段交给某个对象,剩余空间后续如何,与之无关。

// mcache.go

func (c *mcache) releaseAll() {
    
    // 遍历内存块数组。
    for i := range c.alloc {
        s := c.alloc[i]
        
        // 如果是合法内存块,上交给 central。
        if s != &emptymspan {
            mheap_.central[i].mcentral.uncacheSpan(s)
            c.alloc[i] = &emptymspan
        }
    }
    
    // 释放微⼩小对象分配内存。
    c.tiny = 0
    c.tinyoffset = 0
}

emptymspan 用于占位,并不持有内存。

// dummy MSpan that contains no free objects.
var emptymspan mspan

收归 central 时,根据是否有剩余空间决定存储列表。

上述操作是将所有 span 上交,并不仅仅是有剩余空间的。兴许某个内存块正好内存耗尽,缓存部件尚未及扩容。

// mcentral.go

// Return span from an mcache.
//
// s must have a span class corresponding to this
// mcentral and it must not be empty.

func (c *mcentral) uncacheSpan(s *mspan) {
    
    if !go115NewMCentralImpl {
        c.oldUncacheSpan(s)
        return
    }
    
    sg := mheap_.sweepgen
    stale := s.sweepgen == sg+1

    // 修正代龄。
    if stale {
        atomic.Store(&s.sweepgen, sg-1)
    } else {
        atomic.Store(&s.sweepgen, sg)
    }
    
    // 剩余空间。
    n := int(s.nelems) - int(s.allocCount)

    if stale {
        // 清理结束会放回相关集合内。
        s.sweep(false)
    } else {
        // 按是否有剩余空间,放置合适的集合内。
        if n > 0 {
            c.partialSwept(sg).push(s)
        } else {
            c.fullSwept(sg).push(s)
        }
    }
}

清理

垃圾标记完成后,以 span 为单位进行清理(sweep)操作。

所谓清理,并非挨个处理所有 object。

实际上,内存块除分配位图(allocBits)外,还有个一摸一样的垃圾标记位图(gcmarkBits)。

垃圾回收器以此标记出可回收,也就是可重新使用的内存位置。

如此,只需将 markBits 数据 “复制” 给 allocBits 就可实现清理目的。

至于内存单元里的遗留数据清除与否,则是分配操作要考虑的。

// mheap.go

type mspan struct {
    gcmarkBits *gcBits   // 标记位图 
    allocBits  *gcBits   // 分配位图
}
// mgcsweep.go

// Sweep frees or collects finalizers for blocks not marked in the mark phase.
// It clears the mark bits in preparation for the next GC round.

// Returns true if the span was returned to heap.

// If preserve=true, don't return it to heap nor relink in mcentral lists;
// caller takes care of it.

func (s *mspan) sweep(preserve bool) bool {
    
    if !go115NewMCentralImpl {
        return s.oldSweep(preserve)
    }
    
    spc := s.spanclass
    sweepgen := mheap_.sweepgen

    // 基于标记位图统计已分配数量 (不含可回收部分)。
    nalloc := uint16(s.countAlloc())
    
    // 计算本次回收数量 (标记前分配数量 - 标记后分配数量)。
    nfreed := s.allocCount - nalloc

    // 重置属性。
    s.allocCount = nalloc
    s.freeindex = 0

    // 将标记位图直接当作分配位图使⽤,便可实现 “复制”。
    s.allocBits = s.gcmarkBits
    s.gcmarkBits = newMarkBits(s.nelems)

    // 初始化缓存。
    s.refillAllocCache(0)

    atomic.Store(&s.sweepgen, sweepgen)

    if spc.sizeclass() != 0 {
        
        // 小对象 ...
        
        if !preserve {
            
            // 回收后分配数量为 0,则整体归还给 heap。
            if nalloc == 0 {
                mheap_.freeSpan(s)
                return true
            }
            
            // 按是否有剩余空间,放置到 central 合适集合中。
            if uintptr(nalloc) == s.nelems {
                mheap_.central[spc].mcentral.fullSwept(sweepgen).push(s)
            } else {
                mheap_.central[spc].mcentral.partialSwept(sweepgen).push(s)
            }
        }
        
    } else if !preserve {
        
        // 大对象 ...
        
        if nfreed != 0 {
            // 归还给 heap。
            mheap_.freeSpan(s)
            return true
        }

        // 没能回收空间,放到 central。
        // 大对象内存块只有一个 object,没有回收,就表示对象依然存活。
        mheap_.central[spc].mcentral.fullSwept(sweepgen).push(s)
    }
    
    return false
}

如果 central 持有的 span 收回其全部空间,则将其上交给 heap,以便调剂给其他 central 使用。

这是第二级资源平衡,使得不同 sizeclass 的请求能充分利用已有内存。

为什么上交的是完整收回空间的 span ?

因为每个 span 仅服务于一种 sizeclass,如果将剩余空间转给其他 central,就必须将其剩余空间切分成新的 span。

这么做将导致内存碎片化,最终得不偿失。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文