使用Golang来作为日常的cmdline程序开发也有一两年了,之前作为一名Ops来说,会使用Golang去开发一些常用的工具来实现生产环境的各种常规操作以及日常运维管理,而对于整个Golang语言内部的一些细节都不甚了解。但随着对Ops要求的提高,以及向SRE理念转型的需要,我们越来越需要深入理解一些内部底层的原理,这样在我们去管理的我们的Kubernetes集群,或者其他的一些内部系统时才能真正做到游刃有余。
在Golang中,一个对象最终是分配到
概念介绍
逃逸分析
所以,更通俗一点讲,逃逸分析就是确定一个对象是要放在
- 1.是否有非局部调用(对象定义之外的调用).即:如果有可能被
引用 ,那通常会被分配到堆上,否则就在栈上 - 2.如果对象太大(即使没有被引用),无法放在栈区也是可能放到
堆 上的
总结起来就是:
- 1.减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除
- 2.逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(系统开销少)
- 3.减少动态分配所造成的内存碎片
如何进行逃逸分析
分析工具:
- 1.通过编译工具查看详细的逃逸分析过程(
go build -gcflags '-m -l' main.go ) - 2.通过反编译命令查看
go tool compile -S main.go
-
-N : 禁止编译优化 -
-l : 禁止内联(可以有效减少程序大小) -
-m : 逃逸分析(最多可重复四次) -
-benchmem : 压测时打印内存分配统计
堆
堆是除栈之外的第二个内存区域,用于存储值,
首先,成本与垃圾收集器(GC)有关,垃圾收集器必须参与进来以保持该区域的清洁。当GC运行时,它将使用25%的可用CPU资源。此外,它可能会产生微秒级的“stop the world”延迟。拥有GC的好处是你不需要担心内存的管理问题,因为内存管理是相当复杂、也容易出错的。
堆上的值构成Go中的内存分配。这些分配对GC造成压力,因为堆中不再被指针引用的每个值都需要删除。需要检查和删除的值越多,GC每次运行时必须执行的工作就越多。因此,GC算法一直在努力在堆的大小分配和运行速度之间寻求平衡。
栈
在程序中,每个函数块都会有自己的
这块内存地址称为
逃逸分析示例
1.示例-参数泄露
使用
由上述输出的
2.示例-未知类型
这个时候,我们把上面的代码稍微改动一下:
再次进行逃逸分析:
由上可以发现我们的指针对象
这是为什么呢?怎么加了个
其实主要原因为
我们可以看到
3.示例-指针
此时,我们再小改点代码:
逃逸分析:
由以上输出可以看到在
4.示例-综合案例
在上面第三个示例中我们提到,当返回对象是
但是又如第二个示例中所说,如果我们在上面的示例中增加
由上述输出可看到,当使用引用类型来获取底层的值时,在
而这次我们使用
总结
通过上面的概念和实例分析,我们基本知道了逃逸分析的概念和规则,并且大概知道何时,那种对象会被分配到堆或栈内存中,在实际情况中可能情况会更加复杂,需要具体分析。
不过,有如下几点可能在我们实际使用过程中要注意下:
- 静态分配到栈上,性能一定比动态分配到堆上好
- 底层分配到堆,还是栈。实际上对你来说是透明的,不需要过度关心
- 每个 Go 版本的逃逸分析都会有所不同(会改变,会优化)
- 直接通过
go build -gcflags '-m -l' 就可以看到逃逸分析的过程和结果 - 到处都用
指针传递并不一定是最好的 ,要用对 - map & slice 初始化时,预估容量,避免由扩展导致的内存分配。但是如果太大(10000)也会逃逸,因为栈的空间是有限的
思考
函数传递指针真的比传值效率高吗?
我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。
内存碎片化问题
实际项目基本都是通过
Golang使用的垃圾回收算法是『标记——清除』.
简单得说,就是程序要从操作系统申请一块比较大的内存,内存分成小块,通过链表链接。
每次程序申请内存,就从链表上面遍历每一小块,找到符合的就返回其地址,没有合适的就从操作系统再申请。如果申请内存次数较多,而且申请的大小不固定,就会引起内存碎片化的问题。
申请的堆内存并没有用完,但是用户申请的内存的时候却没有合适的空间提供。这样会遍历整个链表,还会继续向操作系统申请内存。
知识星球
公众号
欢迎关注我的公众号: BGBiao,一起进步~