c - 垃圾收集如何与数据段一起工作?
问题描述
对于函数范围内的以下 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
?
解决方案
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 将释放堆分配的字符串头数据,除非程序首先完全退出。
推荐阅读
- sails.js - 通过模型设置创建唯一的多键索引
- javascript - 如何在 testcafe 中调用外部异步等待函数
- javascript - url-createobjecturl-no-longer-accepts-mediastream - 在 2018 年 12 月 14 日 Chrome 更新之后
- c - struct 中的枚举 - C 中的枚举范围
- python - ValueError:无法获取具有未知等级的形状的长度
- mysql - 为什么 <> 'null' 在 MySQL 中有效?
- python - Python-正确添加分数
- c# - 如何使用 MVVM 在 C# UWP 中过滤异步列表?
- css - 法语中一些标点符号前的空格:有没有 CSS 方法来避免断行?
- javascript - 应用程序更新与先前单击的信息反应和 redux