使用Golang来作为日常的cmdline程序开发也有一两年了,之前作为一名Ops来说,会使用Golang去开发一些常用的工具来实现生产环境的各种常规操作以及日常运维管理,而对于整个Golang语言内部的一些细节都不甚了解。但随着对Ops要求的提高,以及向SRE理念转型的需要,我们越来越需要深入理解一些内部底层的原理,这样在我们去管理的我们的Kubernetes集群,或者其他的一些内部系统时才能真正做到游刃有余。

堆栈逃逸分析

概念介绍

逃逸分析

逃逸分析编译器执行静态代码分析栈(帧)逃逸堆
堆栈
引用栈上堆
如果在函数外部引用,必定在堆中分配;如果没有外部引用,优先在栈中分配;如果一个函数返回的是一个(局部)变量的地址,那么这个变量就发生逃逸
避免逃逸的好处:
  • 1.减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除
  • 2.逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(系统开销少)
  • 3.减少动态分配所造成的内存碎片

如何进行逃逸分析

注意:go build

分析工具:

go build -gcflags '-m -l' main.gogo tool compile -S main.go
编译参数介绍(-gcflags):
-N-l-m-benchmem

全局变量、内存占用大的局部变量、发生了逃逸的局部变量存在的地方就是堆三色标记法

首先,成本与垃圾收集器(GC)有关,垃圾收集器必须参与进来以保持该区域的清洁。当GC运行时,它将使用25%的可用CPU资源。此外,它可能会产生微秒级的“stop the world”延迟。拥有GC的好处是你不需要担心内存的管理问题,因为内存管理是相当复杂、也容易出错的。

堆上的值构成Go中的内存分配。这些分配对GC造成压力,因为堆中不再被指针引用的每个值都需要删除。需要检查和删除的值越多,GC每次运行时必须执行的工作就越多。因此,GC算法一直在努力在堆的大小分配和运行速度之间寻求平衡。

注意:

内存区域局部变量地址、返回值大小在编译时已经确定
栈栈是线程级别大小在创建的时候已经确定
注意:栈栈上的空间

逃逸分析示例

1.示例-参数泄露

逃逸分析
leaking paramGetUserInfoGetName指针变量uuuser&usermain()

2.示例-未知类型

这个时候,我们把上面的代码稍微改动一下:

再次进行逃逸分析:

&userGetUserInfo(user)GetName(user)
fmt.Println
fmt.Println
fmt.Println(a)interface{}reflectreflect.TypeOf(arg).Kind()

3.示例-指针

此时,我们再小改点代码:

逃逸分析:

GetUserInfo(u user)堆指针对象

4.示例-综合案例

指针类型*name该引用类型未被外部使用
fmt.Println(name)
注意
fmt.Println*name
fmt.Println(name)namenew(string)

总结

通过上面的概念和实例分析,我们基本知道了逃逸分析的概念和规则,并且大概知道何时,那种对象会被分配到堆或栈内存中,在实际情况中可能情况会更加复杂,需要具体分析。

不过,有如下几点可能在我们实际使用过程中要注意下:

go build -gcflags '-m -l'指针传递并不一定是最好的

思考

函数传递指针真的比传值效率高吗?

我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。

内存碎片化问题

c := make([]int, 0, l)

Golang使用的垃圾回收算法是『标记——清除』.

简单得说,就是程序要从操作系统申请一块比较大的内存,内存分成小块,通过链表链接。

每次程序申请内存,就从链表上面遍历每一小块,找到符合的就返回其地址,没有合适的就从操作系统再申请。如果申请内存次数较多,而且申请的大小不固定,就会引起内存碎片化的问题。

申请的堆内存并没有用完,但是用户申请的内存的时候却没有合适的空间提供。这样会遍历整个链表,还会继续向操作系统申请内存。

wx公号: BGBiao,一起进步~