堆、栈与指针
前言
堆、栈在计算机领域是亘古不变的热门话题,归根结底它们和编程语言无关,都是操作系统层面的内存划分,后面尝试简单地拆开这几个概念,谈谈我对它们的理解。
栈
每个函数中每个值在栈中都是独占的,不能在其他栈中被访问。每个方法片(function frame)都有一个自己的独享栈,这个栈的生命周期随着方法开始结束诞生与消逝,在方法结束时候会被释放掉,较之于堆,栈的优势是比较轻量级,随用随弃,存活期跟随着函数。
堆
通俗的讲,假如说栈是各个函数的一栋私人住宅,堆就是一个大型的人民广场,它可以被共享。堆作为一个全局访问块,它的空间由GC(拆迁大队)管理作。
The heap is not self cleaning like stacks, so there is a bigger cost to using this memory. Primarily, the costs are associated with the garbage collector (GC), which must get involved to keep this area clean.
翻译过来,区别于栈在函数调用结束时候就释放掉,堆不会自动释放,堆空间的释放主要来自于垃圾回收操作。
GC(Garbage collection)
垃圾回收, 垃圾回收具有多种策略,一般来说,每一个存在于堆中,但不再被指针所引用的变量,都会被回收掉。“These allocations put pressure on the GC because every value on the heap that is no longer referenced by a pointer, needs to be removed.”由于垃圾回收涉及内存操作,往往需要考虑许多因素,所幸经过漫长演变,前人种树,有些编程语言垃圾回收策略已经足够强大(Java,Go),我们大部分时候不需要去干涉内存清理,把这部分工作交给底层调度器。
分配到堆内存有个弊端是它会为下一次GC增加压力,好处是可以被其他栈所共享,下列情景编译器会倾向于将它放在堆中存储:
- 尝试申请一个较大的结构体/数组
- 变量在一定的时间内还会被使用
- 编译期间不能确认大小的变量申请
指针
指针,本质上和其他类型一样,只不过它的值是内存地址(引用),我的理解是内存块的门牌号。有句经常被提及的话:
什么时候该使用指针取决于什么时候要分享它。
Pointers serve one purpose, to share a value with a function so the function can read and write to that value even though the value does not exist directly inside its own frame.
指针是为了让变量在不同函数方法块(栈区间)之间分享,并且提供变量读写操作。
结合上述的理解,指针指的是内存地址,堆是共享模块,指针是为了共享同一块内存片。在Go语言中,所有传参都是值传递,指针也是通过传递指针的值。
程序实例 指针共享
主函数
func main() {
var v int = 1
fmt.Printf("# Main frame: Value of v:\t\t %v, address: %p\n", v, &v)
PassValue(v, &v)
fmt.Printf("# Main frame: Value of v:\t\t %v, address: %p\n", v, &v)
}
复制代码
子函数
func PassValue(fv int, addV *int) {
// fv 的地址只属于该函数, 由该函数栈分配
fmt.Printf("# Func frame: Value of fv:\t\t %v, address: %p\n", fv, &fv)
//本次修改只在该函数生效
fv = 0
fmt.Printf("# Func frame: Value of fv:\t\t %v, address: %p\n", fv, &fv)
/*
* 根据main函数传入的全局地址, 对指针执行操作外部是可见的,
* 因为改指针操作的都是同一个内存块的内容
*/
*addV++
fmt.Printf("# Func frame: Value of addV:\t %v, address: %p\n", *addV, addV)
}
复制代码
输出:
# Main frame: Value of v: 1, address: 0xc000054080
# Func frame: Value of fv: 1, address: 0xc0000540a0
# Func frame: Value of fv: 0, address: 0xc0000540a0
# Func frame: Value of addV: 2, address: 0xc000054080
# Main frame: Value of v: 2, address: 0xc000054080
复制代码
可以看到传递指针在子函数里面操作的都是同一个地址(0xc000054080),所以在子函数退出时候对v的改变在主函数是可见的。
而传进子函数的fv所在地址已经处于子函数的管辖栈,随着函数结束,该栈会被释放。
栈逃逸
-
访问外部栈
指的是变量在函数执行结束时候,没有随着函数栈结束生命,值超出函数(栈)的调用周期,逃到堆去了(通常是一个全局指针),能被外部所共享,可以通过go自带的工具来分析。下面是栈逃逸的一个栗子。//go:noinline func CreatePointer() *int { return new(int) } 复制代码
分析
$ go build -gcflags "-m -m -l" escape.go # command-line-arguments .\escape.go:9:12: new(int) escapes to heap .\escape.go:9:12: from ~r0 (return) at .\escape.go:9:2 复制代码
可以看到提示,
return new(int)
这个语句把new指针返回给调用方,这个随着CreatePointer函数结束的时候仍然有外部引用到这个指针,已经超出了该函数栈的范围,所以编译器提示它将分配到堆中去。 -
编译未确定
关于栈逃逸,还有另外一种情景会发生,再看一个栗子:func SpecifySizeAllocate() { buf := make([]byte, 5) println(buf) } func UnSpecifySizeAllocate(size int) { buf := make([]byte, size) println(buf) } 复制代码
分析
$ go build -gcflags "-m -m" escape.go # command-line-arguments .\escape.go:5:6: can inline SpecifySizeAllocate as: func() { buf := make([]byte, 5); println(buf) } .\escape.go:10:6: can inline UnSpecifySizeAllocate as: func(int) { buf := make([]byte, size); println(buf) } .\escape.go:6:13: SpecifySizeAllocate make([]byte, 5) does not escape .\escape.go:11:13: make([]byte, size) escapes to heap .\escape.go:11:13: from make([]byte, size) (non-constant size) at .\escape.go:11:13 复制代码
观察这两个函数,两个buf的生存期都只在函数里面,好像都不会逃逸,然而根据分析结果,
UnSpecifySizeAllocate()
这个函数却产生了栈逃逸,这是为什么呢?
可以看到,分析提示“non-constant size”/“没有具体大小”,这是因为,编译器并不能在编译阶段知道size的值,所以没法在函数栈里面分配确切大小的区间给buf,如果编译器遇到这种未确定的大小分配,会把他分配到堆中去。 这也解释了为什么SpecifySizeAllocate()
函数没有产生逃逸。
后记:
经过时间的演变编译器已经足够智能,堆栈的申请分配可以放心交给它们去做,大部分业务代码并不需要过度考虑变量的分配,这里仅仅是尝试刨析程序变量在内存中的划分,理解一些概念,要知道堆栈分析只是性能调优其中的一种方式。
当然,这里不仅仅是性能问题,在参数传递中,使用值拷贝还是使用指针,最好要结合这个变量未来的作用域而决定。
Go自带一些工具方便我们分析底层的实现,在遵循前人的建议下,堆栈的分析可能更适合处于业务代码完成之后的优化阶段中,前期为了保证代码的功能和可读性,程序猿应该首选专注于实现,当后面遇到性能瓶颈了,尝试从堆栈分配处优化可能才是要考虑的,毕竟有个原则叫做不要过早优化。
参考链接:
官档:How do I know whether a variable is allocated on the heap or the stack
golang.org/doc/faq#sta…
Ardan labs 四连干货(推荐):
www.ardanlabs.com/blog/2017/0…
Go: Should I Use a Pointer instead of a Copy of my Struct?
medium.com/a-journey-w…
Memory : Stack vs Heap
www.gribblelab.org/CBootCamp/7…
有疑问加站长微信联系(非本文作者)