unsafe.Pointer 和 uintptr 隐藏的坑
- new的对象,内存在哪里开辟
- unsafe.Pointer 和 uintptr 是什么
- 正确地使用非类型安全指针
-
- 一些事实
- 正确使用非类型安全的指针的一些模式
-
- 模式一:将类型*T1的一个值转换为非类型安全指针值,然后将此非类型安全指针值转换为类型*T2。
- 模式二:将一个非类型安全指针值转换为一个uintptr值,然后使用此uintptr值。
- 模式三:将一个非类型安全指针转换为一个uintptr值,然后此uintptr值参与各种算术运算,再将算术运算的结果uintptr值转回非类型安全指针。
- 模式四:将非类型安全指针值转换为uintptr值并传递给syscall.Syscall函数调用。
- 模式五:将reflect.Value.Pointer或者reflect.Value.UnsafeAddr方法的uintptr返回值转换为非类型安全指针。
- 模式六:将一个reflect.SliceHeader或者reflect.StringHeader值的Data字段转换为非类型安全指针,以及其逆转换。
- reflect.SliceHeader为啥不要使用于获取底层数组指针
- 真正安全的string零拷贝
- 总结
- 参考文献
new的对象,内存在哪里开辟
使用go标准编译器编译的代码,每个协程都会有自己的协程栈,一个协程栈是一个预申请的内存块。每个协程的初始栈大小比较小(在64位系统上2KB)。 每个栈的大小在协程运行的时候将按照需要增长和收缩( stack grow )。
当我们创建对象申请内存块的时候,可以从协程栈上申请,也可以从堆上申请。
从协程栈上申请的对象,只能在此协程内部被使用(引用),比如指针指向的对象不能逃逸到协程外,其它协程是无法访问到这些内存块的。一个协程不需要使用任何数据同步技术而使用开辟在它的栈上的内存块上的值。
也可以从堆上申请对象, 开辟在堆上的内存块可以被多个协程并发地访问。 在需要的时候需要做并发安全控制。
编译器在编译代码时候会做逃逸分析,关于逃逸分析细节可以参考:golang 逃逸分析与栈、堆分配分析
逃逸分析在编译阶段可以确定一个对象是分配在堆上还是协程栈上。如果编译器觉察到一个内存块在运行时将会被多个协程访问,或者不能轻松地断定此内存块是否只会被一个协程访问,则此内存块将会被开辟在堆上。 也就是说,编译器将采取保守但安全的策略,使得某些可以安全地被开辟在栈上的内存块也有可能会被开辟在堆上。
go支持协程栈是为了提升性能。
- 从栈上开辟内存块比在堆上快得多;
- 开辟在栈上的内存块不需要被垃圾回收;
- 开辟在栈上的内存块对CPU缓存更加友好。
如上所述,目前官方Go编译器中的逃逸分析器并不十分完美,因此某些可以安全地开辟在栈上的值也可能会逃逸到了堆上。
不过我们可以认为每个包级变量(常称全局变量)都被开辟在了堆上,并且它被一个开辟在一个全局内存区上的隐式指针所引用着。 一个开辟在堆上的内存块可能同时被开辟在若干不同栈上的值部所引用着。
一些事实:
- 如果一个结构体值的一个字段逃逸到了堆上,则此整个结构体值也逃逸到了堆上。
- 如果一个数组的某个元素逃逸到了堆上,则此整个数组也逃逸到了堆上。
- 如果一个切片的某个元素逃逸到了堆上,则此切片中的所有元素都将逃逸到堆上,但此切片值的直接部分(SliceHeader)可能开辟在栈上。
- 如果一个值部v被一个逃逸到了堆上的值部所引用,则此值部v也将逃逸到堆上。
使用内置new函数开辟的内存可能开辟在堆上,也可能开辟在栈上,也就是不是所有的指针指向的对象都保存在堆上。这是与C++不同的一点。
当一个协程的栈的大小改变(grow)时,一个新的内存段将申请给此栈使用。原先已经开辟在老的内存段上的内存块将很有可能被转移到新的内存段上,或者说这些内存块的地址将改变。 相应地,引用着这些开辟在此栈上的内存块的指针(它们同样开辟在此栈上)中存储的地址也将得到刷新。 这里很重要,这也是 uintptr 变量不要轻易使用的原因。
unsafe.Pointer 和 uintptr 是什么
关于这一块可以参考:golang unsafe实践与原理
这里需要再次强调的是: uintptr 就是一个16进制的整数,这个数字表示对象的地址,但是uintptr没有指针的语义。所以有一些情况:一,如果一个对象只有一个 uintptr 表示的地址表示"引用"关系,那么这个对象会在GC时被无情的回收掉,那么uintptr表示一个野地址。二,如果uintptr表示的地址指向的对象发生了copy移动(比如协程栈增长,slice的扩容等),那么uintptr也表示一个野地址。 但是unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收,并且在发生移动时候更新地址值。
正确地使用非类型安全指针
这部分主要是参考unsafe.Pointer的官方文档和:go101的非类型安全指针一文
一些事实
一:非类型安全指针值(unsafe.Pointer)是指针但uintptr值是整数
每一个非零安全或者不安全指针值均引用着另一个值。但是一个uintptr值并不引用任何值,它被看作是一个整数,尽管常常它存储的是一个地址的数字表示。
Go的GC会检查对象引用关系并回收不再被程序中的任何仍在使用中的值所引用的对象。指针在这一过程中扮演着重要的角色。值与值之间和内存块与值之间的引用关系是通过指针来表征的。
既然一个uintptr值是一个整数,那么它可以参与算术运算。
二:不再被使用的内存块的回收时间点是不确定的
也就是GC的开始时间是不确定的。
下面有个例子:
import "unsafe"
// 假设此函数不会被内联(inline)。
func createInt() *int {
return new(int)
}
func foo() {
p0, y, z := createInt(), createInt(), createInt()
var p1 = unsafe.Pointer(y) // 和y一样引用着同一个值
var p2 = uintptr(unsafe.Pointer(z))
// 此时,即使z指针值所引用的int值的地址仍旧存储
// 在p2值中,但是此int值已经不再被使用了,所以垃圾
// 回收器认为可以回收它所占据的内存块了。另一方面,
// p0和p1各自所引用的int值仍旧将在下面被使用。
// uintptr值可以参与算术运算。
p2 += 2; p2--; p2--
*p0 = 1 // okay
*(*int)(p1) = 2 // okay
*(*int)(unsafe.Pointer(p2)) = 3 // 危险操作!
}
值p2是一个 uintptr, 不具有指针含义而是一个整数,所以不能保证z指针值所引用的int值所占的内存块一定还没有被回收。 换句话说,当*(*T)(unsafe.Pointer(p2))) = 3被执行的时候,此内存块有可能已经被回收了。 所以,接引p2中存储的地址可能是接引野指针。
三:一个值的地址在程序运行中可能改变
参考 unsafe.Pointer 和 uintptr 是什么
这里我们只需要知道当一个协程的栈的大小改变时,开辟在此栈上的内存块需要移动,从而相应的值的地址将改变。
四:我们可以将一个值的指针传递给runtime.KeepAlive函数调用来确保此值在此调用之前仍然处于被使用中
为了确保一个值部和它所引用着的值部仍然被认为在使用中,我们应该将引用着此值的另一个值传给一个runtime.KeepAlive函数调用。 在实践中,我们常常将此值的指针传递给一个runtime.KeepAlive函数调用。
还是上面 事实二 的例子:
func foo() {
p0, y, z := createInt(