Java架构师-十项全能:打造高度深度广度兼备的全面技术人才
Golang记忆调谐 逃逸分析(逃逸分析) 在程序编译阶段,根据程序代码中的数据流,静态分析代码中哪些变量需要分配在堆栈空间上,哪些变量需要分配在堆栈空间上。一个理想的逸出分析算法,能够将开发者认为需要分配的变量尽可能多地保留在堆栈空间上,尽可能少地“逸出”到堆栈空间。理想太丰满,现实却很骨感。不同的语言情况,不同的语言版本,转义算法的准确性和实际优化是不一样的。对于开发者来说,只需要掌握逃逸分析工具和逃逸分析目标即可。 Golang语言的转义分析算法有两个版本。go1.13.6 darwin/amd64之后的版本由马修·登普斯基(Matthew Dempsky)大哥改写。它在源代码的src/cmd/compile/internal/GC/escape . go文件中,有详细的注释。有兴趣可以看看。虽然只有1000多行代码,而且有完整的算法描述和代码注释,但还是很“硬头”。 源文件注释了Golang语言转义分析算法的原理,是内存转义的指导思想。有两个基本不变量:
我们必须确保的两个关键不变量是: (1)指向堆栈对象的指针不能存储在堆中 (2)指向堆栈对象的指针不能比该对象更长寿(例如,因为声明函数返回并破坏了对象的堆栈帧,或者其空间在逻辑上不同的变量的循环迭代中被重用)。
指向堆栈对象的指针不能存储在堆中。 指向stack对象的指针不能超过stack对象的生存期(也就是说,在stack对象被销毁后,指针不能存活)。
逃逸分析算法的一般原理和过程在注释中也有说明。一般步骤是:
Golang编译器解析Golang源文件并获得抽象语法树(AST)。 构造一个有向加权图,遍历有向加权图,寻找可能违反上述两个不变量的赋值路径。如果变量的地址存储在堆中或其他可能超过其生存期的地方,该变量将被标记为需要在堆上分配。 在分析函数间的数据时,转义分析算法记录了每个函数的数据流,具体算法可以移到源代码中。
虽然不能通过new、make或literal的方式显式指定变量的分配位置,但是通过转义分析可以知道变量是分配到堆栈空间还是堆空间,这样可以提高热接口的响应,优化内存,提高GC效率,避免OOM。日常开发中,不需要详细了解逃逸分析算法的工作原理。只有使用Golang的工具链,才能使用逃逸分析算法进行逃逸分析。 诊断器具 使用编译工具 通过编译工具查看详细的转义分析过程,命令:gobuild-gcflags'-m-l 'xxxx.go。 编译参数(-gcflags):
-N:禁止编译优化。 -l:禁止内联。 -m:逃逸分析 -benchmem:在压力测量期间打印内存分配统计数据。
比如局部变量badBoy是在main函数中声明的,但是局部变量不是从外部捕获的,理论上会在函数栈上分配。 主包装
类型人员结构{ 名称字符串 EnName字符串 }
func main() { 坏男孩:= &Person{ 姓名:“法外狂徒张三”, EnName:“张三”, } _ =坏男孩 } 复制代码 输入命令:go build -gcflags '-m -l' main.go,传入-l关闭inline,从而屏蔽inline对最终生成代码的影响。
逃逸分析总结
堆栈空间分配内存比堆空间分配内存更高效,不同版本的Golang优化不同。本文基于go1.13.6 darwin/amd64。 逸出分析的目的是确定内存分配地址是堆栈空间还是堆空间。对于开发人员来说,无法通过new、make或literal来指定分配空间。 转义分析是在编译阶段完成的,可以通过编译工具分析,也可以反编译生成汇编代码,前者方便快捷,后者准确。 对于大型局部变量和大型内存,使用容器类型:切片、映射、数组。为了获得最佳性能,最好指明长度并将其分配给堆栈。对于肯定会转义到堆的变量,要知道是否会被捕获,循环引用会导致GC失败。 Golang系统提供的函数,底层方法在运行时反射类型、生成对象时,会有内存转义,所以在业务代码中尽量少用反射,一方面提高了代码的可读性,另一方面也给底层方法留下了“转义”的机会。 在日常开发中,无论是分配到栈空间还是堆空间,都不需要太在意。你只需要知道常见的逃逸场景,遇到OOM的时候有思路去查,去优化。 download:Java架构师-十项全能:打造高度深度广度兼备的全面技术人才