go语言内存 go语言内存申请和释放( 六 )


向 channel 中发送数据,本质上就是为 channel 内部的成员赋值,就像给一个 slice 中的某一项赋值一样 。所以chan *Type,chan map[Type]Type,chan []Type,chan interface{}类型都会导致发送到 channel 中的数据逃逸 。
这本来也是情理之中的,发送给 channel 的数据是要与其他函数分享的,为了保证发送过去的指针依然可用,只能使用堆分配 。
可变参数如func(arg ...string)实际与func(arg []string)是一样的,会增加一层访问路径 。这也是fmt.Sprintf总是会使参数逃逸的原因 。
例子非常多,这里不能一一列举,我们只需要记住分析方法就好,即,2 级或更多级的访问赋值会容易导致数据逃逸 。这里加上容易二字是因为随着语言的发展,相信这些问题会被慢慢解决,但现阶段,这个可以作为我们分析逃逸现象的依据 。
下面代码中包含 2 种很常规的写法,但他们却有着很大的性能差距,建议自己想下为什么 。
Benchmark 和 pprof 给出的结果:
熟悉堆栈概念可以让我们更容易看透 Go 程序的性能问题 , 并进行优化 。
多级间接赋值会导致 Go 编译器出现不必要的逃逸,在一些情况下可能我们只需要修改一下数据结构就会使性能有大幅提升 。这也是很多人不推荐在 Go 中使用指针的原因,因为它会增加一级访问路径,而map,slice,interface{}等类型是不可避免要用到的 , 为了减少不必要的逃逸,只能拿指针开刀了 。
大多数情况下,性能优化都会为程序带来一定的复杂度 。建议实际项目中还是怎么方便怎么写,功能完成后通过性能分析找到瓶颈所在,再对局部进行优化 。
golang内存扩容一般来说当内存空间span不足时,需要进行扩容 。而在扩容前需要将当前没有剩余空间的内存块相关状态解除 , 以便后续的垃圾回收期能够进行扫描和回收,接着在从中间部件(central)提取新的内存块放回数组中 。
需要注意由于中间部件有scan和noscan两种类型 , 则申请的内存空间最终获取的可能是其两倍 , 并由heap堆进行统一管理 。中间部件central是通过两个链表来管理其分配的所有内存块:
1、empty代表“无法使用”状态,没有剩余的空间或被移交给缓存的内存块
2、noempty代表剩余的空间 , 并这些内存块能够提供服务
由于golang垃圾回收器使用的累增计数器(heap.sweepgen)来表达代龄的:
从上面内容可以看到每次进行清理操作时该计数器 +2
再来看下mcentral的构成
当通过mcentral进行空间span获取时,第一步需要到noempty列表检查剩余空间的内存块 , 这里面有一点需要说明主要是垃圾回收器的扫描过程和清理过程是同时进行的,那么为了获取更多的可用空间 , 则会在将分配的内存块移交给cache部件前 , 先完成清理的操作 。第二步当noempty没有返回时,则需要检查下empty列表(由于empty里的内存块有可能已被标记为垃圾,这样可以直接清理,对应的空间则可直接使用了) 。第三步若是noempty和empty都没有申请到,这时需要堆进行申请内存的
通过上面的源码也可以看到中间部件central自身扩容操作与大对象内存分配差不多类似 。
在golang中将长度小于16bytes的对象称为微小对象(tiny),最常见的就是小字符串,一般会将这些微小对象组合起来,并用单块内存存储 , 这样能够有效的减少内存浪费 。
当微小对象需要分配空间span,首先缓存部件会按指定的规格(tiny size class)取出一块内存 , 若容量不足,则重新提取一块;前面也提到会将微小对象进行组合,而这些组合的微小对象是不能包含指针的,因为垃圾回收的原因,一般都是当前存储单元里所有的微小对象都不可达时 , 才会将该块内存进行回收 。

推荐阅读