分享
  1. 首页
  2. 文章

golang内存释放

神奇的考拉 · · 12585 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一、前言

一般在golang运行完成初始化时,会创建专门的goroutine用于后台监控、定期任务,这其中也涉及到了强制垃圾回收、内存释放等任务。

// 主goroutine.
func main() {
 // ...
 // m0: 系统主线程
 // g0:主goroutine
 // m0、g0是比较特殊的 仅用于main goroutine的父goroutine 
 g.m.g0.racectx = 0
 if sys.PtrSize == 8 { // 64bits 系统
 maxstacksize = 1000000000
 } else { // 32bits系统
 maxstacksize = 250000000
 }
 // 新建M(物理线程)
 mainStarted = true
 if GOARCH != "wasm" { // 没有线程在wasm 没必要进行系统监控的
 systemstack(func() {
 newm(sysmon, nil)
 })
 }
 
 // 在初始化时将main goroutine与系统主线程锁定
 lockOSThread()
 if g.m != &m0 {
 throw("runtime.main not on m0")
 }
 runtime_init() // 初始化
 if nanotime() == 0 {
 throw("nanotime returning zero")
 }
 // ...
}

从上面的源码可以看到在运行初始化期间通过newm(sysmon,nil)来开启一些系统监控。接下来看看sysmon的源码


// 通常该方法执行时没有关联的P(上下文环境) 以至于写屏蔽的是不允许的
// 一般来说golang中goroutine都会有与之关联的P记录上下文
func sysmon() {
 // ...
 
 // 当一块堆内存块在一次垃圾回收后5分钟没有被使用 则会被归还操作系统
 scavengelimit := int64(5 * 60 * 1e9) // 堆内存归还给操作系统时限: 5分钟
 if debug.scavenge > 0 {
 // Scavenge-a-lot for testing.
 forcegcperiod = 10 * 1e6
 scavengelimit = 20 * 1e6
 }
 lastscavenge := nanotime() // 最后执行时间
 nscavenge := 0 // 执行次数统计
 lasttrace := int64(0) // 最近一次追踪
 idle := 0 // 记录没有唤醒的次数
 delay := uint32(0)
 for {
 if idle == 0 { // 默认延迟20us
 delay = 20
 } else if idle > 50 { // 当超过1ms 延迟时间加倍 *2
 delay *= 2
 }
 // 延迟最大=10ms(当延迟时间超过了10ms) 
 if delay > 10*1000 { 
 delay = 10 * 1000
 }
 usleep(delay) // 延迟delay执行GC 
 now := nanotime()
 // ...
 
 // 是否需要强制GC
 if t := (gcTrigger{kind: gcTriggerTime, now: now});
 t.test() && atomic.Load(&forcegc.idle) != 0 {
 lock(&forcegc.lock)
 forcegc.idle = 0
 forcegc.g.schedlink = 0
 injectglist(forcegc.g)
 unlock(&forcegc.lock)
 }
 // 检查并释放物理内存 
 if lastscavenge+scavengelimit/2 < now {
 mheap_.scavenge(int32(nscavenge), uint64(now), uint64(scavengelimit))
 lastscavenge = now
 nscavenge++
 }
 // ......
 }
}

二、闲置内存

在进行内存释放时,其实针对的是闲置内存(被堆heap管理、尚未被中间部件mcentral或大对象使用的内存块),而这些内存有可能长时间不使用,那么就应该释放掉其占有的物理内存,节约系统资源。在golang本身对内存管理对象使用两个计数器:unusedsince闲置起始时间 npreleased释放归还os的页数

type mspan struct{
 unusedsince int64 // 首次被发现当前span状态=mspanfree
 npreleased uintptr // 归还给os的页数
}

其中内存块获取和归还操作时内存管理对象的计数器会被重置

// 根据指定大小分配空间,新分配的span会从freelist被移除代表该span已被使用
// 但是该span状态仍是=mspanfree(这一点需要注意)
func (h *mheap) allocSpanLocked(npage uintptr, stat *uint64) *mspan{
 // ......省略代码
 HaveSpan:
 // 刚被分配的span 状态=mspanfree
 if s.state != _MSpanFree {
 throw("MHeap_AllocLocked - MSpan not free")
 }
 if s.npages < npage {
 throw("MHeap_AllocLocked - bad npages")
 }
 if s.npreleased > 0 {
 // 已使用空间span
 sysUsed(unsafe.Pointer(s.base()), s.npages<<_PageShift) 
 // 内存统计:堆heap释放的内存
 memstats.heap_released -= uint64(s.npreleased << _PageShift)
 s.npreleased = 0
 }
 if s.npages > npage { // 申请的空间span页数 低于该空间的页数 需要进行裁剪
 // 进行多余空间裁剪 并归还给heap堆
 t := (*mspan)(h.spanalloc.alloc())
 // 更新裁剪的span
 t.init(s.base()+npage<<_PageShift, s.npages-npage)
 s.npages = npage
 p := (t.base() - h.arena_start) >> _PageShift
 if p > 0 {
 h.spans[p-1] = s
 }
 h.spans[p] = t
 h.spans[p+t.npages-1] = t
 t.needzero = s.needzero
 s.state = _MSpanManual // 防止与s结合
 t.state = _MSpanManual
 h.freeSpanLocked(t, false, false, s.unusedsince)
 s.state = _MSpanFree
 }
 s.unusedsince = 0
 
 // ......省略代码
}
// s:需要属于busy list或者没有任何引用
func (h *mheap) freeSpanLocked(s *mspan, acctinuse, acctidle bool, unusedsince int64){
 // ......省略代码
 // 标记最新未被使用的空间span
 // GC则会根据这些信息将一些页归还给OS
 s.unusedsince = unusedsince
 if unusedsince == 0 {
 s.unusedsince = nanotime()
 }
 s.npreleased = 0
 // ......省略代码 
}

在归还操作过程中,可能存在局部释放的情况:当内存空间释放了对应的物理内存,假设此时npreleased == npages,不过一旦该内存块与其他内存块进行合并,就会导致npreleased < npages.

2.1 释放

在内存分配过程中:<128页的可用内存是放置在free链表数组中,而>=128页的可用内存则是通过树堆freelarge来存储的,也就是说释放操作其实针对的就是这两个列表。

// 释放指定内存块
// 一旦仍持有堆heap锁 无论是malloc操作还是发生panic都不会产生
// 主要因为这是mheap接口的non-mallocgc入口
// now:超时判断的基准时间(首次被标记为垃圾的内存块的时间会与该时间进行比较)
// limit: now - unusedsince与该超时阈值比较 超过都可释放(默认时间5分钟)
func (h *mheap) scavenge(k int32, now, limit uint64) {
 // ......省略代码
 var sumreleased uintptr
 for i := 0; i < len(h.free); i++ {
 sumreleased += scavengelist(&h.free[i], now, limit)
 }
 sumreleased += scavengetreap(h.freelarge.treap, now, limit)
 unlock(&h.lock)
 gp.m.mallocing--
 // 输出统计结果
 if debug.gctrace > 0 {
 if sumreleased > 0 {
 print("scvg", k, ": ", sumreleased>>20, " MB released\n")
 }
 // ......省略代码
 }
}

真正的比较操作,确认符合释放要求的内存块

func scavengelist(list *mSpanList, now, limit uint64) uintptr {
 if list.isEmpty() { // 跳过空链表
 return 0
 }
 var sumreleased uintptr
 // 遍历链表内所有的span
 for s := list.first; s != nil; s = s.next {
 // 忽略不符合释放条件的: 已被释放的、闲置时间小于limit的
 if (now-uint64(s.unusedsince)) <= limit || s.npreleased == s.npages {
 continue
 }
 
 // 统计要释放的空间
 start := s.base()
 end := start + s.npages<<_PageShift
 // 物理页大小 过大,超过指定的系统页大小
 // 需要保证释放范围end-start在物理页内存块范围内
 // 否则可能超过所需要释放的范围 超出我们实际需要的释放空间
 if physPageSize > _PageSize {
 start = (start + physPageSize - 1) &^ (physPageSize - 1)
 end &^= physPageSize - 1
 if end <= start { // 忽略持续整个物理页的span
 continue
 }
 }
 len := end - start
 // 要释放的空间大小
 released := len - (s.npreleased << _PageShift)
 if physPageSize > _PageSize && released == 0 {
 continue
 }
 memstats.heap_released += uint64(released)
 sumreleased += released
 
 // 释放计数
 s.npreleased = len >> _PageShift
 // 释放物理内存(整块内存)
 sysUnused(unsafe.Pointer(start), len)
 }
 return sumreleased
}

而释放树堆freelarge里面的内存块,基本操作一致。到此那些内存可被释放?如何释放其物理内存等基本上有所了解,具体的释放因操作系统不同而异。

Unix类似的系统基本上都是通过madvise来建议内核解除物理内存映射,这样在保留虚拟内存的情况下,达到释放物理内存的目的。当这些内存被使用时,有内存来自动补齐对应所需的物理内存。
windows则不支持类似的机制,直接通过对应的系统API进行释放和重新分配的。


内存释放

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:神奇的考拉

查看原文:golang内存释放

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
12585 次点击
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏