背景

在开发的过程中,应用golang syncmap存储连贯信息,其中将本人封装的连贯对象指针作为key,
连贯对象大略是上面的构造

type Con struct {    con net.Conn    addr string    readBuffer []byte    writeBuffer []byte    ......}

在跑benchmark测试的时候,发现了一个问题,就是每次跑完benchmark,无论工夫过来多久,内存始终回不到程序刚启动的水准。然而无论跑多少次benchmark,内存并不会有限升高,而是维持在方才的水准高低。过后感觉这个不是简略的内存透露。go pprof剖析内存发现是readbuffer和writeBuffer那里好多调配的内存没有开释。看到这个后果也是有点懵逼,一个会话完结的时候不仅连贯会断开,con对象也会从map中被删除,为什么还没有被开释呢。因为只有这个map的逻辑是前面加的,过后判断问题大概率出在这里,所以仔细阅读了sync map的源码。

源码

源码在sync包的map.go

type Map struct {    mu Mutex //操作dirty时用到的锁        read atomic.Value // 只读map,只读的删除是通过更新entry中unsafe point的指向来实现,所以其实这样的删除,key占用的内存空间并没有被开释(这个场景在下文会说到)        dirty map[any]*entry         misses int}
type readOnly struct {    m       map[interface{}]*entry    amended bool // 如果dirty map比read map中存储的数据要全,则该字段的值为true}type entry struct {    p unsafe.Pointer // *interface{}}

加载key

func (m *Map) Load(key any) (value any, ok bool) {    read, _ := m.read.Load().(readOnly)    e, ok := read.m[key] //先尝试从read map加载数据    if !ok && read.amended {    //如果没有找到数据,并且read map和dirty有差别        m.mu.Lock()        read, _ = m.read.Load().(readOnly)        e, ok = read.m[key]        if !ok && read.amended {            //这里是进行一个double check            e, ok = m.dirty[key]            //减少一下miss的次数,miss次数达到dirty长度就会用dirty笼罩readmap            m.missLocked()        }        m.mu.Unlock()    }    if !ok {        return nil, false    }    return e.load()}func (m *Map) missLocked() {    m.misses++    if m.misses < len(m.dirty) {        return        //当miss的次数大于等于dirty的长度的时候,就用dirty笼罩readmap    m.read.Store(readOnly{m: m.dirty})    m.dirty = nil    m.misses = 0}func (e *entry) load() (value any, ok bool) {    p := atomic.LoadPointer(&e.p)    //不为空并且不是擦除状态,这个擦除状态是指的是,当从read map向dirty同步数据的时候,如果这个key对应value的entry指向的是nil(删除状态),就不同步到dirty,而是标记为擦除(expunged)状态思考 为什么要设置为擦除状态当发现read map中的一个key是擦除状态的时候,能够阐明dirty非空,并且dirty外面没有这个key。这样在store的时候就能够通过判断擦除状态来保障,dirty外面没有的key,会被sotre到dirty。    if p == nil || p == expunged {        return nil, false    }    return *(*any)(p), true}

存储key

func (m *Map) Store(key, value any) {    read, _ := m.read.Load().(readOnly)    if e, ok := read.m[key]; ok && e.tryStore(&value) {        return    }    m.mu.Lock()    read, _ = m.read.Load().(readOnly)    if e, ok := read.m[key]; ok {        if e.unexpungeLocked() {            m.dirty[key] = e        }        e.storeLocked(&value)    } else if e, ok := m.dirty[key]; ok {        e.storeLocked(&value)    } else {        if !read.amended {            m.dirtyLocked()            m.read.Store(readOnly{m: read.m, amended: true})        }        m.dirty[key] = newEntry(value)    }    m.mu.Unlock()}func (e *entry) tryStore(i *any) bool {    for {        p := atomic.LoadPointer(&e.p)        if p == expunged {        //响应在load办法中的思考            return false        }        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {            return true        }    }}func (m *Map) dirtyLocked() {    if m.dirty != nil {        return    }    read, _ := m.read.Load().(readOnly)    m.dirty = make(map[any]*entry, len(read.m))    for k, e := range read.m {    //这里就是把readmap外面value的entry外面unsafe point不是nil的同步到dirty,是nil的原地标记为擦除状态        if !e.tryExpungeLocked() {            m.dirty[k] = e        }    }}func (e *entry) tryExpungeLocked() (isExpunged bool) {    p := atomic.LoadPointer(&e.p)    for p == nil {        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {            return true        }        p = atomic.LoadPointer(&e.p)    }    return p == expunged}

删除key

删除这里后面局部跟load的原理差不多

func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {    read, _ := m.read.Load().(readOnly)    e, ok := read.m[key]    if !ok && read.amended {        m.mu.Lock()        read, _ = m.read.Load().(readOnly)        e, ok = read.m[key]        if !ok && read.amended {            e, ok = m.dirty[key]            delete(m.dirty, key)//先删除后计数miss            m.missLocked()        }        m.mu.Unlock()    }    if ok {        return e.delete()    }    return nil, false}// Delete deletes the value for a key.func (m *Map) Delete(key any) {    m.LoadAndDelete(key)}func (e *entry) delete() (value any, ok bool) {    for {        p := atomic.LoadPointer(&e.p)        if p == nil || p == expunged {            return nil, false        }        if atomic.CompareAndSwapPointer(&e.p, p, nil) {            return *(*any)(p), true        }    }}

试验

如图代码在14行处断点
此时sync map中各个字段的内容如下

此时因为13行的删除其实有一次load操作,此时misses是1,那么实践上14行运行实现,后dirty的长度会变成1,missing会变成2.此时会用dirty笼罩read map,并且置空dirty,如图

剖析是正确

而后如果跑完15行,则会从read map删除"1",然而不是真正删除,只是unsafe point指针为空
继续执行15行后果如下

能够看出的确是空值

剖析

其实依据试验曾经比拟显著了,当在readmap删除的时候,并不是传统的delete(m,key)的形式,key并不会删除,所以,当咱们的key是一个指针,刚好指向的对象占用的对象空间比拟大的时候。这里我只是拿只有一个key的状况做举例,在做benchmark的时候,这种key会很多,就比方结尾说的,好多conn对象的指针作为key,导致这个对象不能gc,所以就会呈现,明明删除了,然而内存还没上来的状况。

持续试验

持续写入加load触发 dirty笼罩readmap的操作


17行执行完,发现readmap中的 key"1"变成如图,其实就是在把readmap同步到dirty过程中,把key"1"的value设置成擦除状态
继续执行18行

能够看到readmap曾经被笼罩,如果后面是很多个key的话,此时就会发现有大量的内存空间被开释