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

栏目: IT技术 · 发布时间: 4年前

内容简介:堆、栈在计算机领域是亘古不变的热门话题,归根结底它们和编程语言无关,都是操作系统层面的内存划分,后面尝试简单地拆开这几个概念,谈谈我对它们的理解。每个函数中每个值在栈中都是独占的,不能在其他栈中被访问。每个方法片(function frame)都有一个自己的独享栈,这个栈的生命周期随着方法开始结束诞生与消逝,在方法结束时候会被释放掉,较之于堆,栈的优势是比较轻量级,随用随弃,存活期跟随着函数。通俗的讲,假如说栈是各个函数的一栋私人住宅,堆就是一个大型的人民广场,它可以被共享。堆作为一个全局访问块,它的空间由

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

每个函数中每个值在栈中都是独占的,不能在其他栈中被访问。每个方法片(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所在地址已经处于子函数的管辖栈,随着函数结束,该栈会被释放。

栈逃逸

  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…

以上所述就是小编给大家介绍的《聊一聊堆、栈与Go语言的指针》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First Design Patterns—深入淺出設計模式

Head First Design Patterns—深入淺出設計模式

天瓏

寫應用程式時需要依照需求預先規劃、設計,而設計模式累積了前人的經歷,經由四人幫彙整出一系列的設計模式,以利後人可以套用。本書集合四人幫的23個模式(十幾年前的事)外加這十幾年來新增的一些模式,作者群以詼諧、幽默、圖文並茂、打破傳統著書的方式,由淺入深地詳解了設計模式的精神及重點。全書全部以當紅的 Java 程式語言為範例。 本書特點: * 全世界第二本書......一起来看看 《Head First Design Patterns—深入淺出設計模式》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器