golang内存释放

栏目: Go · 发布时间: 7年前

内容简介:一般在golang运行完成初始化时,会创建专门的goroutine用于后台监控、定期任务,这其中也涉及到了强制垃圾回收、内存释放等任务。从上面的源码可以看到在运行初始化期间通过newm(sysmon,nil)来开启一些系统监控。接下来看看sysmon的源码在进行内存释放时,其实针对的是闲置内存(被堆heap管理、尚未被中间部件mcentral或大对象使用的内存块),而这些内存有可能长时间不使用,那么就应该释放掉其占有的物理内存,节约系统资源。在golang本身对内存管理对象使用两个计数器:unusedsin

一、前言

一般在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内存释放

内存释放


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Spring in Action

Spring in Action

Craig Walls / Manning Publications / 2011-6-29 / USD 49.99

Spring in Action, Third Edition has been completely revised to reflect the latest features, tools, practices Spring offers to java developers. It begins by introducing the core concepts of Spring and......一起来看看 《Spring in Action》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具