上半部分参考 https://www.jianshu.com/p/63404461e520
golang 在栈或者堆中分配内存,更倾向在栈中分配因为代价低,
内存逃逸上指,编译器编译时检查变量,发现整个生命周期是否在运行时完全可知。
如果可知,它就可以在栈上分配。否则就说它 逃逸 了,必须在堆上分配。
可以使用命令
go build -gcflags=-m
进行逃逸分析
通常栈逃逸到堆的情况
-
发送指针或带有指针的值到 channel 中。在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
-
在一个切片上存储指针或带指针的值。一个典型的例子就是
[]*string
。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。 -
slice 的背后数组被重新分配了,因为
append
时可能会超出其容量(cap
)。slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。 -
在 interface 类型上调用方法。在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个
io.Reader
类型的变量r
, 调用r.Read(b)
会使得r
的值和切片b
的背后存储都逃逸掉,所以会在堆上分配。
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0)
}
-
noescape()
函数的作用是遮蔽输入和输出的依赖关系。使编译器不认为p
会通过x
逃逸, 因为uintptr()
产生的引用是编译器无法理解的。 -
内置的
uintptr
类型是一个真正的指针类型,但是在编译器层面,它只是一个存储一个指针地址
的int
类型。代码的最后一行返回unsafe.Pointer
也是一个int
。 -
noescape()
在runtime
包中使用unsafe.Pointer
的地方被大量使用。如果作者清楚被unsafe.Pointer
引用的数据肯定不会被逃逸,但编译器却不知道的情况下,这是很有用的。
以上原文都有,然后主要说说 uintptr 和 unsafe.Pointer
- unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;
- 而uintptr是用于指针运算的,GC 不把 uintptr 当指针,它只是一个存储一个
指针地址
的int
类型,也就是说 uintptr 无法持有对象, uintptr 类型的目标会被回收; - unsafe.Pointer 可以和 普通指针 进行相互转换;
- unsafe.Pointer 可以和 uintptr 进行相互转换。
可以使用此指针方式给结构体赋值
type Test struct {
a int
b int
}
func main() {
var test *Test = new(Test)
fmt.Println(test)
a := unsafe.Pointer(uintptr(unsafe.Pointer(test))+unsafe.Offsetof(test.a))
*((*int)(a)) = 21
fmt.Println(test)
}
运行结果
&{0 0}
&{21 0}
uintptr(unsafe.Pointer(test))
获取了 test 的指针起始值,然后offsetof获取偏移量,a为结构体内a的指针,是一个通用指针
转换为int指针,再使用* 解引用,赋值