IT袋

当前位置:主页 > 经验教程 > 硬件教程 >

内存管理:空间分配及逃逸分析

内存管理:空间分配及逃逸分析 内存管理: 分配与逃逸分析(4)

时间:2023-12-22 19:23:12 来源:IT袋 作者:马勇
导读:内存管理:空间分配及逃逸分析,User{...} escapes to heap 即表示对象逃逸到堆上了。 2. interface{} 动态类型逃逸 在 Go 语言中,空接口即 interface{} 可以表示任意的类型,如果函数参数为 interfa

内存管理:空间分配及逃逸分析

内存管理:空间分配及逃逸分析

&User{...} escapes to heap 即表示对象逃逸到堆上了。

2. interface{} 动态类型逃逸

在 Go 语言中,空接口即 interface{} 可以表示任意的类型,如果函数参数为 interface{},编译期间很难确定其参数的具体类型,也会发生逃逸。比如 Println 函数,入参是一个 interface{} 空类型:

func Println(a ...interface{}) (n int, err error)

这时,返回的是一个 User 对象,也会发生对象逃逸,但逃逸节点是 fmt.Println 函数使用时:

func GetUserInfo() User {
return User{
ID: 666666,
Name: "sim lou",
Avatar: "https://www.baidu.com/avatar/666666",
}
}
func main() {
u := GetUserInfo()
fmt.Println(u.Name) // 对象发生逃逸
}

3. 栈空间不足

操作系统对内核线程使用的栈空间是有大小限制的,64 位 Linux 系统上通常是 8 MB。

可以使用 ulimit -a 命令查看机器上栈允许占用的内存的大小。

root@cvm_172_16_10_34:~ # ulimit -a
-s: stack size (kbytes)             8192
-u: processes                       655360
-n: file descriptors                655360

因为栈空间通常比较小,因此递归函数实现不当时,容易导致栈溢出。

对于 Go 语言来说,运行时(runtime) 尝试在 goroutine 需要的时候动态地分配栈空间,goroutine 的初始栈大小为 2 KB。当 goroutine 被调度时,会绑定内核线程执行,栈空间大小也不会超过操作系统的限制。

对 Go 编译器而言,超过一定大小的局部变量将逃逸到堆上,不同的 Go 版本的大小限制可能不一样。我们来做一个实验(注意,分配 int[] 时,int 占 8 字节,所以 8192 个 int 就是 64 KB):

package main
import "math/rand"
func generate8191() {
nums := make([]int, 8192) // <= 64KB
for i := 0; i < 8192; i++ {
nums[i] = rand.Int()
}
}
func generate8192() {
nums := make([]int, 8193) // > 64KB
for i := 0; i < 8193; i++ {
nums[i] = rand.Int()
}
}
func generate(n int) {
nums := make([]int, n) // 不确定大小
for i := 0; i < n; i++ {
nums[i] = rand.Int()
}
}
func main() {
generate8191()
generate8192()
generate(1)
}

编译结果如下:

内存管理:空间分配及逃逸分析

可以发现,make([]int, 8192) 没有发生逃逸,make([]int, 8193)make([]int, n) 逃逸到堆上。

也就是说,当切片占用内存超过一定大小,或无法确定当前切片长度时,对象占用内存将在堆上分配。

4. 闭包

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

— 闭包

Go 语言中,当使用闭包函数时,也会发生内存逃逸。看一则示例代码:

package main
func Increase() func() int {
   n := 0
   return func() int {
       n++
       return n
  }
}
func main() {
   in := Increase()
   fmt.Println(in()) // 1
   fmt.Println(in()) // 2
}

Increase() 函数的返回值是一个闭包函数,该闭包函数访问了外部变量 n,那变量 n 将会一直存在,直到 in 被销毁。很显然,变量 n 占用的内存不能随着函数 Increase() 的退出而回收,因此将会逃逸到堆上。

4.3 利用逃逸分析提升性能

传值VS传指针

传值会拷贝整个对象,而传指针只会拷贝指针地址,指向的对象是同一个。传指针可以减少值的拷贝,但是会导致内存分配逃逸到堆中,增加垃圾回收(GC)的负担。在对象频繁创建和删除的场景下,传递指针导致的 GC 开销可能会严重影响性能。

一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能。

5. 小结

内存分配是程序运行时内存管理的核心逻辑,Go 程序运行时的内存分配器使用类似 TCMalloc 的分配策略将对象根据大小分类,并设计多层缓存的组件提高内存分配器的性能。

理解 Go 语言内存分配器的设计与实现原理,可以帮助我们理解不同编程语言在设计内存分配器时做出的不同选择。

栈内存是应用程序中重要的内存空间,它能够支持本地的局部变量和函数调用,栈空间中的变量会与栈一同创建和销毁,这部分内存空间不需要工程师过多的干预和管理,现代的编程语言通过逃逸分析减少了我们的工作量,理解栈空间的分配对于理解 Go 语言的运行时有很大的帮助

好了,以上就是本文的全部内容了,如果觉得文章有所启发或收获,不妨点赞、分享,这对不念是最大的鼓励!

上述的关于内存管理:空间分配及逃逸分析的详细方法介绍,大家作为一个参考建议。

相关阅读