1、堆内存和栈内存是什么
栈内存上的对象的存储空间是自动分配和销毁的,无需开发人员或编程语言运行时过多参与(作用域函数内);
内存对象,可以在全局(跨函数间)合法使用,这就是堆内存对象,堆内存对象需要通过专用API手工分配和释放,在C中对应的分配和释放方法就是malloc和free;
c语言程序解释
#include <stdio.h> #include <stdlib.h> int *foo() { int *c = malloc(sizeof(int)); *c = 12; return c; } int main() { int *p = foo(); printf("the return value of foo = %d\n", *p); free(p); }
堆内存对象的生命周期管理将会给开发人员带来很大的心智负担。为了降低这方面的心智负担,带有GC(垃圾回收)的编程语言出现了,比如Java、Go等。这些带有GC的编程语言会对位于堆上的对象进行自动管理。当某个对象不可达时(即没有其对象引用它时),它将会被回收并被重用。
但GC的出现虽然降低了开发人员在内存管理方面的心智负担,但GC不是免费的,它给程序带来的性能损耗是不可忽视的,尤其是当堆内存上有大量待扫描的堆内存对象时,将会给GC带来过大的压力,从而使得GC占用更多本应用于处理业务逻辑的计算和存储资源。于是人们开始想方法尽量减少在堆上的内存分配,可以在栈上分配的变量尽量留在栈上。
2、逃逸分析是什么
逃逸分析(escape analysis)就是在程序编译阶段根据程序代码中的数据流,对代码中哪些变量需要在栈上分配,哪些变量需要在堆上分配进行静态分析的方法。
作用是:
帮助程序员将那些人们认为需要分配在栈上的变量尽可能保留在栈上,尽可能少的“逃逸”到堆上的算法
3、对int和slice做逃逸分析 - 如何减少变量逃逸提升程序性能
a、int 实例:
package main import "testing" // 逃逸分析(escape analysis)就是在程序编译阶段根据程序代码中的数据流,对代码中哪些变量需要在栈上分配, // 哪些变量需要在堆上分配进行静态分析的方法 // 位于栈上的内存对象由程序自行创建销毁不同,堆内存对象需要通过专用API手工分配和释放,在C中对应的分配和释放方法就是malloc和free // 尽量减少在堆上的内存分配,可以在栈上分配的变量尽量留在栈上 // 而函数foo中的a以及指针p指向的内存块都在栈上分配(即便我们是调用的new创建的int对象, // Go中new出来的对象可不一定分配在堆上,逃逸分析的输出日志中还专门提及new(int)没有逃逸) // 未逃逸的a和p指向的内存块的地址区域在0xc000074860~0xc000074868 func foo() { // 整型 a := 11 // 指针 p := new(int) *p = 12 // 未逃逸 println("addr of a is", &a) // 未逃逸 println("addr that p point to is", p) } // 逃逸的m和n被分配到了堆内存空间,从输出的结果来看在0xc0000160e0~0xc0000160e8 // 函数bar中执行了两次堆内存分配动作 // 源码的14和15行,汇编调用了runtime.newobject在堆上执行了内存分配动作,这恰是逃逸的m和n声明的位置。 // 实际上在gc管理的内存上执行了malloc动作 func bar() (*int, *int) { m := 21 n := 22 println("addr of m is", &m) println("addr of n is", &n) // 返回栈内存变量的指针 // bar中的m、n逃逸到heap // 这两个变量将在heap上被分配存储空间 return &m, &n } func main() { // go build -gcflags "-m -l" int.go // go run -gcflags "-l" int.go // 逃逸分析 - 栈内存变量逃逸堆内存变量 - 叫做逃逸 println(int(testing.AllocsPerRun(1, foo))) println(int(testing.AllocsPerRun(1, func() { bar() }))) }
b、slice 示例
package main import ( "reflect" "unsafe" ) // slice的原理:切片实现原理 - 三元组 //type slice struct { // array unsafe.Pointer // len int // cap int //} // 声明了一个空slice // slice自身是分配在栈上的,但是运行时在动态扩展切片时,选择了将其元素存储在heap上 func noEscapeSliceWithDataInHeap() { var sl []int println("addr of local(noescape, data in heap) slice = ", &sl) printSliceHeader(&sl) sl = append(sl, 1) println("append 1") printSliceHeader(&sl) println("append 2") sl = append(sl, 2) printSliceHeader(&sl) println("append 3") sl = append(sl, 3) printSliceHeader(&sl) println("append 4") sl = append(sl, 4) printSliceHeader(&sl) } // noEscapeWithDataInStack直接初始化了一个包含8个元素存储空间的切片,切片自身没有逃逸, //并且在附加(append)的元素个数小于等于8个的时候,元素直接使用了为其分配的栈空间 // 如果附加的元素超过8个,那么运行时会在堆上分配一个更大的空间并将原栈上的8个元素复制过去,后续该切片的元素就都存储在了堆上 // 为什么强烈建议在创建 slice 时带上预估的cap参数的原因: 减少了堆内存的频繁分配、cap容量之下,所有元素都分配在栈上 func noEscapeSliceWithDataInStack() { var sl = make([]int, 0, 8) println("addr of local(noescape, data in stack) slice = ", &sl) printSliceHeader(&sl) sl = append(sl, 1) println("append 1") printSliceHeader(&sl) sl = append(sl, 2) println("append 2") printSliceHeader(&sl) } // escapeSlice则是切片变量自身以及其元素的存储都在堆上 func escapeSlice() *[]int { var sl = make([]int, 0, 8) println("addr of local(escape) slice = ", &sl) printSliceHeader(&sl) sl = append(sl, 1) println("append 1") printSliceHeader(&sl) sl = append(sl, 2) println("append 2") printSliceHeader(&sl) return &sl } func printSliceHeader(p *[]int) { ph := (*reflect.SliceHeader)(unsafe.Pointer(p)) println("slice data =", unsafe.Pointer(ph.Data)) } func main() { noEscapeSliceWithDataInHeap() noEscapeSliceWithDataInStack() escapeSlice() }
所以以上结论就是 - 最好栈内存对象不要指针向外抛出导致内存逃逸,slice 尽量创建时候分配上 cap 参数(容量) - 还有就是 make slice 的时候指定初始长度 0 的意义
转载自:
https://mp.weixin.qq.com/s/ALzzT1kQ3VfYQly1LT7mQw