Golang中goroutine的内存占用是多少?

Golang中goroutine的内存占用是多少? 各位 Gopher 们好,

一个 goroutine 的内存占用是多少?这里有一段引述:

一个新创建的 goroutine 会被分配几千字节,这几乎总是足够的。 当不够时,运行时会自动增长(和缩小)用于存储栈的内存,使得许多 goroutine 可以存在于适度的内存中。

所以它开始时是几千字节(具体是多少?),然后会自动增长和缩小。

增长和缩小的规则是什么?当需要更多内存时,它的容量会翻倍吗,还是会增加 50% 的内存?

同样地,缩小的规则是什么?缩小操作何时进行?我猜想是在一个 GC 周期释放了足够的内存,以至于其总占用小于某个阈值之后,那么这个阈值是多少?

此外,一个经常增加和减少其内存占用的 goroutine 会产生哪些性能开销?


更多关于Golang中goroutine的内存占用是多少?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

10 回复

一根绳子有多长? Goroutine 占用的内存取决于它所执行的操作,即它分配了多少内存,以及其调用栈的深度。

更多关于Golang中goroutine的内存占用是多少?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你从哪里得到这个引用的?我在语言规范中找不到它。

而且这个引用读起来好像那几公斤只是针对栈的,而堆仍然在 goroutine 之间共享。

垃圾回收器不会修改栈。 但它的确会扫描栈以查找引用。

@ammon

不知为何我感到困惑,我以为垃圾回收器只清理堆,而不会触及栈。这在 goroutine 中是如何运作的呢?

所有的 Goroutine 共享同一个垃圾回收内存池。因此,当垃圾收集器运行时,它将回收无法访问的对象,无论该对象是由哪个 goroutine 分配的。

根据Go编程语言的官方文档:

第950行: // 栈的增长是倍增的,以实现恒定的摊销成本。

第1192行(在函数 shrinkstack 中):

newsize := oldsize / 2

我发现了这篇文章,它详细探讨了 Goroutine 的内存使用情况。文章指向了 stack.go 文件,该文件定义了最小栈大小为 2048 字节。(你可以随时查看主分支中的文件,以确认此值是否仍然相同。)

@amnon

显然,一个 goroutine 所需的内存取决于它的具体操作。但问题的核心在于其扩展性:

我的 goroutine 当前已分配了 X 内存,现在它需要更多,因此被分配了额外的 Y 内存。X 和 Y 之间的关系是什么?

我的 goroutine 当前已分配了 X 内存,现在它需要更少,因此被分配了更少的 Y 内存。X 和 Y 之间的关系是什么?

感谢 @amnon,我现在明白你的意思了,并且意识到我应该把我的问题表述得更清楚一些:

一个给定的 goroutine 消耗的栈内存将如何波动?

  • goroutine X 以 2048 字节的初始栈内存开始,如果该 goroutine 已经使用了 2048 字节,并且想要再使用一个额外的字节,接下来会发生什么?
  • 类似地,另一个 goroutine Y 现在使用了 2049 字节的栈内存,但突然它不再需要其中 50% 的字节,那么栈会变成什么样子?

在Go中,goroutine的初始栈大小在Go 1.4之后从8KB减少到了2KB。具体大小可以通过runtime/debug包的SetMaxStack函数查看和调整,但通常不建议修改。

栈的增长和缩小规则如下:

栈增长: 当goroutine栈空间不足时(例如深度递归或大量局部变量),运行时会分配一个新的更大的栈,并将旧栈内容复制到新栈。增长策略通常是当前大小的2倍,直到达到最大值(默认为1GB,32位系统为250MB)。

示例代码,展示栈增长:

package main

import (
    "fmt"
    "runtime/debug"
)

func recursiveCall(depth int) {
    var buf [1024]byte // 每个调用帧占用1KB
    if depth == 0 {
        return
    }
    recursiveCall(depth - 1)
    _ = buf // 防止优化
}

func main() {
    fmt.Println("初始栈大小:", debug.SetMaxStack(0)) // 获取当前最大栈大小
    recursiveCall(1000) // 触发栈增长
}

栈缩小: 在垃圾回收(GC)期间,如果发现goroutine的栈使用量小于当前大小的1/4,运行时会将其缩小为当前大小的1/2。缩小的栈不会立即释放内存,而是放回缓存池供后续重用。

性能开销: 频繁的栈调整会导致:

  1. 内存复制开销:增长时需要复制整个栈内容。
  2. CPU缓存失效:栈地址变化影响局部性。
  3. GC压力:频繁调整增加GC扫描工作量。

示例代码,展示栈缩小:

package main

import (
    "fmt"
    "runtime"
    "time"
)

func allocateLargeStack() {
    var arr [1024 * 1024]byte // 占用1MB栈空间
    _ = arr
}

func main() {
    go func() {
        allocateLargeStack()
        runtime.GC() // 触发GC,可能引起栈缩小
        time.Sleep(time.Second)
    }()
    time.Sleep(2 * time.Second)
}

可以通过runtime.ReadMemStats监控栈内存变化:

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    var ms runtime.MemStats
    for i := 0; i < 5; i++ {
        runtime.ReadMemStats(&ms)
        fmt.Printf("栈内存使用: %d MB\n", ms.StackSys/1024/1024)
        time.Sleep(time.Second)
    }
}

实际使用中,goroutine栈的自动管理在大多数场景下是高效的,仅在极端情况(如超深递归)下需要注意。

回到顶部