goroutine究竟占用多少内存?

引言

相信接触过 Go 语言的同窗,都应该有据说过 Go 协程,也就是 goroutine 的概念,对于 goroutine 的介绍,大部分文章中提到的都是,相较于线程,goroutine 十分轻量,相同大小的内存,能够运行更多的 goroutine。可是不多有文章解释 goroutine 是如何作到占用更少资源的,单个 goroutine 究竟占用多少内存?本文将针对这些问题进行解释。程序员

一些基本结论

  • goroutine 所占用的内存,均在栈中进行管理
  • goroutine 所占用的栈空间大小,由 runtime 按需进行分配
  • 以 64位环境的 JVM 为例,会默认固定为每一个线程分配 1MB 栈空间,若是大小分配不当,便会出现栈溢出的问题

聪明的你应该不难从上面这些结论中看出,goroutine 相较于线程更加轻量,关键点就在于栈空间的动态分配,这样即可以最大限度的利用内存资源。既然是动态分配,那脱离实际状况而单纯说单个 goroutine 占用多大内存,就有点吹毛求疵了。因此接下来,咱们就先来看看,goroutine 是如何作到栈空间动态分配的。安全

分段栈

在 Go 的早期版本中,使用分段栈的方式进行内存管理,当一个goroutine被建立时,runtime 会为协程分配 8KB 的内存区域。那么问题来了,8KB 空间不够了怎么办?bash

为了解决这个问题,Go 会在每一个函数的入口处都插入一小段前置代码,它可以检查栈空间是否被消耗殆尽,若是用完了,便会调用 morestack() 函数来扩展空间。less

morestack()函数机理,即分段栈扩张机理:为栈空间分配一块新的内存区域。而后在这个新栈的底部的结构体中填充关于该栈的各类数据,包括刚刚来自的旧栈的地址。当获得了一个新的栈分段以后,经过从新执行,致使栈被用完的函数,来重启goroutine。这就被称为栈的分裂函数

+---------------+
  |               |
  |   unused      |
  |   stack       |
  |   space       |
  +---------------+
  |    test       |
  |               |
  +---------------+
  |               |
  |  lessstack    |
  +---------------+
  | Stack info    |
  |               |-----+
  +---------------+     |
                        |
                        |
  +---------------+     |
  |    test       |     |
  |               | <---+
  +---------------+
  | rest of stack |
  |               |
复制代码

分段栈回溯机理:如上图所示,新栈会为lessstack()插入一个栈条目。这个函数并不实际显式调用。它会在耗尽旧栈的那个函数返回的时候被设置,例如图中的test(),当test()运行完毕返回时,会返回到lessstack()中,它会查询栈底部的结构体信息,并调整栈指针(SP),以便可以回溯到上一个栈分段。而后,就能够释放新栈段空间了。ui

分段栈存在的问题

分段栈机制使得栈能够按需扩张收缩。而程序员不须要在乎栈的大小。spa

可是分段栈也有瑕疵。收缩栈是一个相对昂贵的操做。若是是在一个循环中分裂栈状况更明显。函数会增加栈,分裂栈,返回栈,而且释放栈分段。若是是在循环里面作这些操做,那么将会付出很大的开销。例如循环一次经历了这些过程,当下一次循环时栈又被耗尽,又得从新分配栈分段,而后又被释放掉,周而复始,循环往复,开销就会巨大。线程

这就是熟知的 hot split problem (热点分裂问题)。这是Golang开发组切换到新的栈管理方式的主要缘由,新方式称为栈拷贝。指针

连续栈

从GO1.4以后,开始正式使用了连续栈机制。rest

栈拷贝开始很像分段栈。协程运行,使用栈空间,当栈将要耗尽时,触发相同的栈溢出检测。

可是,不像分段栈里有一个回溯连接,栈拷贝的方式则是建立了一个新的分段,它是旧栈的两倍大小,而且把旧栈彻底拷贝进来。 这样当栈收缩为旧栈大小时,runtime不会作任何事情。收缩变成了一个no op免费操做。此外,当栈再次增加时,runtime也不须要作任何事情,从新使用刚才扩容的空间便可。

不像听起来那么容易,其实拷贝栈是一项艰巨的任务。因为栈中的变量在Golang中可以获取其地址,所以最终会出现指向栈的指针。而若是轻易拷贝移动栈,任何指向旧栈的指针都会失效。

而Golang的内存安全机制规定,任何可以指向栈的指针都必须存在于栈中。

因此能够经过垃圾收集器协助栈拷贝,由于垃圾收集器须要知道哪些指针能够进行回收,因此能够查到栈上的哪些部分是指针,当进行栈拷贝时,会更新指针信息指向新目标,以及它相关的全部指针。

可是,runtime中大量核心调度函数和GC核心都是用C语言写的,这些函数都获取不到指针信息,那么它们就没法复制。这种都会在一个特殊的栈中执行,而且由runtime开发者分别定义栈尺寸。

相关文章
相关标签/搜索