首页 > 解决方案 > 垃圾收集如何与数据段一起工作?

问题描述

对于函数范围内的以下 Go 语法f()

var fruits [5]string
fruits[0] = "Apple"

下面是内存表示:

在此处输入图像描述

我的理解是,字符串Apple存储在数据段中,其余六个字符串头(ptr,length)被分配在堆栈段中。


对于函数范围内的以下代码f()

numbers := [4]int{10, 20, 30, 40}

内存{10, 20, 30, 40}在数据段中分配,但不在函数范围的堆栈段中f


在此处输入图像描述

Go 垃圾收集器清理进程的堆段。

从函数返回f()堆栈段指针清除函数的堆栈段f()


编辑:要理解分配字符串方面的值语义指针语义,

Apple从函数返回后如何清除数据段内存(用于字符串) f

标签: cgogarbage-collection

解决方案


Go 的语言定义没有根据段、堆栈、堆等来描述动作。所以所有这些都是实现细节,可能会从一个 Go 实现更改为另一个。

不过,一般来说,Go 编译器会对变量进行实时范围分析,并使用转义分析来确定是在可 GC 的内存(“堆”)还是自动释放的存储(“堆栈”)中分配一些东西。字符串文字可能,取决于太多的事情要计算,在编译时作为文本分配并直接从那里引用,或者复制到一些堆式或堆栈式的数据区域。

为了论证的缘故,我们假设您写道:

func f() {
    var fruits [5]string
    fruits[0] = "Apple"
}

这个函数根本不做任何事情,所以它只是从构建中删除。字符串常量"Apple"根本没有出现。让我们再添加一点,让它确实存在:

package main

import "fmt"

func f() {
    var fruits [5]string
    fruits[0] = "Apple"
    fmt.Println(fruits[0])
}

func main() {
    f()
    fmt.Println("foo")
}

main.f这是生成的二进制文件中的 一些(手工修剪/清理)反汇编。请注意,在其他版本的 Go 中实现几乎肯定会有所不同。 这是使用 Go 1.13.5(用于 amd64)构建的。

main.f:
     mov    %fs:0xfffffffffffffff8,%rcx
     cmp    0x10(%rcx),%rsp
     jbe    2f

到这里为止的一切都是样板文件:函数的入口点检查它是否需要调用运行时来分配更多的堆栈空间,因为它在这里即将使用 0x58 字节的堆栈空间:

1:   sub    $0x58,%rsp
     mov    %rbp,0x50(%rsp)

这是样板的结尾:在接下来的几条指令之后,我们将能够从f一个简单的retq. 现在我们在堆栈上为数组fruits腾出空间,加上编译器认为出于任何原因合适的其他空间,然后更新%rbp%(rsp)然后我们在和中存储一个字符串头,%8%(rsp)以便convTstring在包中调用runtime

     lea    0x50(%rsp),%rbp
     lea    0x35305(%rip),%rax        # <go.string.*+0x24d> - the string is here
     mov    %rax,(%rsp)
     movq   $0x5,0x8(%rsp)            # this is the length of the string
     callq  408da0 <runtime.convTstring>
     mov    0x10(%rsp),%rax

该函数runtime.convTstring实际上在“堆”上为字符串头的另一个副本分配空间(这台机器上的 16 个字节),然后将头复制到位。该副本现在可以存储到fruits[0]其他地方或其他地方。 x86_64 上 Go 的调用约定有点奇怪,所以返回值是 at 0x10(%rsp),我们现在已经将其复制到%rax. 我们稍后会看到它在哪里使用:

     xorps  %xmm0,%xmm0
     movups %xmm0,0x40(%rsp)

这些指令将 16 个字节清零,从0x40(%rsp). 我不清楚这是为了什么,特别是因为我们立即覆盖了它们。

     lea    0x11a92(%rip),%rcx        # <type.*+0x11140>
     mov    %rcx,0x40(%rsp)
     mov    %rax,0x48(%rsp)
     mov    0xd04a1(%rip),%rax        # <os.Stdout>
     lea    0x4defa(%rip),%rcx        # <go.itab.*os.File,io.Writer>
     mov    %rcx,(%rsp)
     mov    %rax,0x8(%rsp)
     lea    0x40(%rsp),%rax
     mov    %rax,0x10(%rsp)
     movq   $0x1,0x18(%rsp)
     movq   $0x1,0x20(%rsp)
     callq  <fmt.Fprintln>

这似乎是对 的调用fmt.Println:因为我们传递了一个接口值,所以我们必须将它打包为一个类型和指向值的指针(也许这就是为什么首先要调用的原因runtime.convTstring)。我们还os.stdout通过一些内联将其接口描述符直接插入到此处的调用中(请注意,此调用直接转到fmt.Fprintln)。

无论如何,我们将runtime.convTstring这里分配的字符串头传递给 function fmt.Println

     mov    0x50(%rsp),%rbp
     add    $0x58,%rsp
     retq
2:   callq  <runtime.morestack_noctxt>
     jmpq   1b

这就是我们从函数返回的方式——常量 0x50 和 0x58 取决于我们分配了多少堆栈空间——以及在函数开头可以跳转到的标签之后,函数入口样板的其余部分。

无论如何,以上所有内容的重点是表明:

  • 五字节序列Apple在运行时根本不分配。相反,它存在于rodata称为go.string.*. 该rodata段实际上是程序文本:如果可能,操作系统会将其放入只读存储器中。出于组织目的,它只是与可执行指令分开。

  • fruits数组实际上根本没有被使用过。编译器可以看到,当我们写入它时,除​​了一次调用之外我们没有使用它,所以我们根本不需要它。

  • 但是一个字符串头,通过它可以找到字符串的长度和数据(在那个rodata段中),确实得到了堆分配。

不需要,因为fmt.Println不会保存这个指针,但编译器没有发现它。最终,运行时 gc 将释放堆分配的字符串头数据,除非程序首先完全退出。


推荐阅读