聊一聊堆、栈与Go语言的指针

堆、栈与指针

前言

堆、栈在计算机领域是亘古不变的热门话题,归根结底它们和编程语言无关,都是操做系统层面的内存划分,后面尝试简单地拆开这几个概念,谈谈我对它们的理解。html

每一个函数中每一个值在栈中都是独占的,不能在其余栈中被访问。每一个方法片(function frame)都有一个本身的独享栈,这个栈的生命周期随着方法开始结束诞生与消逝,在方法结束时候会被释放掉,较之于堆,栈的优点是比较轻量级,随用随弃,存活期跟随着函数。golang

通俗的讲,假如说栈是各个函数的一栋私人住宅,堆就是一个大型的人民广场,它能够被共享。堆做为一个全局访问块,它的空间由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),咱们大部分时候不须要去干涉内存清理,把这部分工做交给底层调度器。ide

分配到堆内存有个弊端是它会为下一次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所在地址已经处于子函数的管辖栈,随着函数结束,该栈会被释放。


栈逃逸

  1. 访问外部栈
    指的是变量在函数执行结束时候,没有随着函数栈结束生命,值超出函数(栈)的调用周期,逃到堆去了(一般是一个全局指针),能被外部所共享,能够经过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函数结束的时候仍然有外部引用到这个指针,已经超出了该函数栈的范围,因此编译器提示它将分配到堆中去。

  2. 编译未肯定
    关于栈逃逸,还有另一种情景会发生,再看一个栗子:

    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…

相关文章
相关标签/搜索