04年初做一个小程序时,goroutine开到几十万的时候,运行时间长了之后内存疯狂暴涨。
但是在本机编译运行ok,排查之后发现go版本不同,本地1.1 线上1.2
看了说明才发现1.2的默认栈大小为8K
...
The 1.2 runtime uses segmented stacks, also known as split stacks
分段栈-每个栈开始时只有一个单独的段。随着栈增长,就会分配新的段,并与上一个段相连,如此保证栈可以不断增长。每个栈都是一个或多个靠高效的双向互链的段组成的。
在栈接近满的时候,发生了一个函数调用。调用会强迫栈赠长,导致需要分配新的段。当这个函数返回时,这个新分配的段会被释放,栈也会再次收缩。这种情况被称作“切分热点”
Go 1.3 里会因为使用了连续栈实现而不再有“切分热点”问题。
现在,如果栈需要增长,不再申请新的段,而是按如下方式操作:
创建一个新的,更大的栈
将老栈的内容复制到新栈
调整所有被复制的指针到新的地址
销毁老栈
调整指针的操作会受到编译器的逃逸分析算法影响。这个算法保证只有指向栈上数据的指针会存储在同一个栈上(当然,也有一些例外)。如果某个指针有逃逸(比如,指针要返回给调用者,或者写入了一个全局变量),就意味着分配的数据需要保存在堆上。
04年初做一个小程序时,goroutine开到几十万的时候,运行时间长了之后内存疯狂暴涨。 但是在本机编译运行ok,排查之后发现go版本不同,本地1.1 线上1.2 看了说明才发现1.2的默认栈大小为8K ... The 1.2 runtime uses segmented stacks, also known as split stacks 分段栈-每个栈开始时只有一个单独的段。随着栈增长,就会分配新的段,并与上一个段相连,如此保证栈可以不断增长。每个栈都是一个或多个靠高效的双向互链的段组成的。 在栈接近满的时候,发生了一个函数调用。调用会强迫栈赠长,导致需要分配新的段。当这个函数返回时,这个新分配的段会被释放,栈也会再次收缩。这种情况被称作“切分热点”
Go 1.3 里会因为使用了连续栈实现而不再有“切分热点”问题。 现在,如果栈需要增长,不再申请新的段,而是按如下方式操作: 创建一个新的,更大的栈 将老栈的内容复制到新栈 调整所有被复制的指针到新的地址 销毁老栈 调整指针的操作会受到编译器的逃逸分析算法影响。这个算法保证只有指向栈上数据的指针会存储在同一个栈上(当然,也有一些例外)。如果某个指针有逃逸(比如,指针要返回给调用者,或者写入了一个全局变量),就意味着分配的数据需要保存在堆上。
这种方法当然也有一些挑战。1.2 版本在运行时并不知道栈上一个指针大小的字,真的是个指针,还是别的同样大小的数据。也许是浮点数或者是更不常见的将一个整形数当作指针,真的指向某个数据。
由于缺少关于数据的理解,垃圾收集器只能保守考虑,将所有位于栈帧上的地址当作根。结果就导致了内存泄露的可能,尤其是在内存池更小的 32 位架构上。
如果是复制整个栈,就能避免这种问题,在调整指针时只考虑真正的指针。
工作就这么做完了,栈上活指针的信息现在嵌入了二进制程序里,并可以在运行时使用这些信息。这意味着 1.3 版本的垃圾收集器不仅可以精确收集栈数据,还可以调整栈上的指针。
1.3 版本的初始栈大小很保守,设置为 4kb,在 1.4 版本里可能会进一步缩小。对于收缩机制,在垃圾收集器执行时,栈使用了少于 1/4 的总空间时,会缩减一半的大小。
虽然连续栈会造成一些内存碎片的问题,但是使用 json 和 html/template 做性能测试的结果显示,连续栈的性能有很大改善。