原文地址:我要在堆上。不,你应该把它放在堆上。我们在写代码的时候,有时候会想这个变量分配到哪里去了?这时候可能有人会说,在栈上,在堆上。相信我,你是对的……但就结果而言,你还是有点知识,这不好,以防有人一头雾水。今天,就让我们在这方面来深挖一下Go的奥秘,自己动手吧。typeUserstruct{IDint64NamestringAvatarstring}funcGetUserInfo()*User{return&User{ID:13746731,Name:"EDDYCJY",Avatar:"https://avatars0.githubusercontent.com/u/13746731"}}funcmain(){_=GetUserInfo()}开头是问号,带着疑问学习。调用GetUserInfo后询问main返回的&User{...}。这个变量是分配在栈上还是堆上?什么是堆/栈这里我不打算详细介绍栈,只是简单介绍一下本文所需要的基础知识。如下:Heap:一般来说是手动管理的,手动申请,分配,释放。一般涉及的内存大小不固定,一般存放较大的对象。另外,它的分配比较慢,涉及的指令动作也比较多栈(Stack):由编译器管理,自动申请、分配、释放。一般不会太大,我们常用的函数参数(不同平台允许存放的个数不同)、局部变量等都会存放在栈中。在我们今天介绍的Go语言中,它的栈分配是由Compiler来分析,由GC来管理的。是的,针对它的分析和动作选择是今天讨论的重点。什么是逃逸分析?在编译器优化理论中,逃逸分析是一种确定指针动态范围的方法。简单的说就是分析程序中哪里可以访问到。通俗地说,逃逸分析就是判断一个变量应该放在堆上还是栈上。规则如下:是否在别处(非本地)引用。只要有可能被引用,就一定分配在堆上。否则,即使分配在栈上,不被外部引用,对象也太大,无法入栈。仍然可以分配到堆上。你可以理解为逃逸分析是编译器用来判断变量分配到堆还是栈的一种行为。逃生在什么阶段建立?转义是在编译阶段建立的。注意不是在运行时为什么要转义这个问题?我们可以反过来想,如果变量都分配到堆上会怎样?例如:垃圾回收(GC)的压力不断增加,申请、分配、回收内存的系统开销增加(相对于栈)。动态分配会产生一定量的内存碎片。其实一般来说,堆内存的频繁申请和分配是有一定“代价”的。它会影响应用程序的运行效率,间接影响整个系统。因此,“按需分配”,最大限度地灵活使用资源,才是正确的治理之道。这就是为什么需要逃逸分析,你不觉得吗?如何判断是否逃逸首先,通过编译命令,可以看到详细的逃逸分析过程。指令集-gcflags用于将识别参数传递给Go编译器,其中涉及到:-m会打印出逃逸分析的优化策略。其实总共最多可以用4个-m,但是信息量大,一般用1个。一个-l就会关闭函数内联,这里关闭内联可以更好的观察逃逸情况,减少干扰.go注意:可以使用gotoolcompile-help查看所有允许传递给编译器的标识参数。第一种情况:指针第一种情况就是一开始抛出的问题。现在你可以再看一遍,这样想:typeUserstruct{IDint64NamestringAvatarstring}funcGetUserInfo()*User{return&User{ID:13746731,Name:"EDDYCJY",Avatar:"https://avatars0.githubusercontent.com/u/13746731"}}funcmain(){_=GetUserInfo()}执行命令观察,如下:$gobuild-gcflags'-m-l'main.go#command-line-arguments./main.go:10:54:&Userliteralescapestoheap通过查看分析结果,我们可以知道&Userescapestotheheap,也就是分配在堆上。这个是不是有问题...看汇编代码确定一下,如下:$gotoolcompile-Smain.go"".GetUserInfoSTEXTsize=190args=0x8locals=0x180x000000000(main.go:9)TEXT"".GetUserInfo(SB),$24-8...0x002800040(main.go:10)MOVQAX,(SP)0x002c00044(main.go:10)CALLruntime.newobject(SB)0x003100049(main.go:10)PCDATA$2,$10x003100049(main.go:10)MOVQ8(SP),AX0x003600054(main.go:10)MOVQ$13746731,(AX)0x003d00061(main.go:10)MOVQ$7,16(AX)0x004500069(main.go:10)PCDATA$2,$-20x004500069(main.go:10)PCDATA$0,$-20x004500069(main.go:10)CMPLruntime.writeBarrier(SB),$00x004c00076(main.go:10)JNE1560x004e00078(main.go:10)LEAQgo.string."EDDYCJY"(SB),CX...我们关注CALL指令并找到它执行了runtime.newobject方法,也就是确实分配在了堆上。为什么是这样?分析结果这是因为GetUserInfo()返回的是指针对象,在方法外返回了引用。所以编译器会在堆上分配对象,而不是在栈上。不然方法结束后,局部变量会被回收,那岂不是崩溃了。所以最后分配到堆上是水到渠成的事情。那么你可能会想,也就是说,所有的指针对象都应该在堆上吧?不是。如下:funcmain(){str:=new(string)*str="EDDYCJY"}你认为这个对象会分配到哪里?如下:$gobuild-gcflags'-m-l'main.go#command-line-arguments./main.go:4:12:mainnew(string)doesnotescape很明显,对象分配在栈上.核心点是是否在作用域外被引用,这里的作用域仍然保留在main中,所以不逃逸Case2:undeterminedtypefuncmain(){str:=new(string)*str="EDDYCJY"fmt.Println(str)}执行命令观察,如下:$gobuild-gcflags'-m-l'main.go#command-line-arguments./main.go:9:13:strescapestoheap./main.go:6:12:new(string)escapetoheap./main.go:9:13:main...argumentdoesescape通过查看分析结果可以知道str变量有escapedto堆上,即对象分配在堆上。但是上次的时候还是在栈上的,所以我们直接从fmt中输出就可以了。这到底是怎么回事……?与case1相比,case2只增加了一行代码fmt.Println(str),问题一定出在里面。其原型:funcPrintln(a...interface{})(nint,errerror)通过对其分析可知,当形参为接口类型时,编译器在编译阶段无法确定其具体类型。因此会发生逃逸,会分配到堆上。有兴趣追溯源码的可以看看内部的reflect.TypeOf(arg).Kind()语句,会导致堆逃逸,外观是接口类型会导致对象分配到堆Case3.泄露参数typeUserstruct{IDint64NamestringAvatarstring}funcGetUserInfo(u*User)*User{returnu}funcmain(){_=GetUserInfo(&User{ID:13746731,Name:"EDDYCJY",Avatar:"https://avatars0.githubusercontent.com/u/13746731"})}执行命令观察,如下:$gobuild-gcflags'-m-l'main.go#command-line-arguments./main.go:9:18:leakingparam:utoresult~r1level=0./main.go:14:63:main&Userliteraldoesnotescape我们注意到泄漏参数的表达式,这说明变量u是泄漏的参数。结合代码可知,传递给GetUserInfo方法后,变量直接返回,没有任何涉及变量的引用等动作。所以,这个变量其实并没有逃逸,它的作用域还在main()中,所以分配在栈上,想想看。你怎么能把它分配到堆上?结合案例一,举一反三。修改如下:typeUserstruct{IDint64NamestringAvatarstring}funcGetUserInfo(uUser)*User{return&u}funcmain(){_=GetUserInfo(User{ID:13746731,Name:"EDDYCJY",Avatar:"https://avatars0.githubusercontent.com/u/13746731"})}执行命令观察,如下:$gobuild-gcflags'-m-l'main.go#command-line-arguments./main.go:10:9:&uescapestoheap./main.go:9:18:movedtoheap:只要稍微修改一下u,就会认为是被外部引用了,所以适当分配给为大家介绍了堆逃逸分析的概念和规则,并给出了一些例子加深理解。但现实肯定远不止这些案例。你需要做的是掌握方法,遇到了就看看。另外需要注意:静态分配到栈上,性能肯定比动态分配到堆上好。底层分配给堆或栈。其实对你来说是透明的,你不需要过分担心每个Go版本的逃逸分析会不一样(会改变和优化)。直接通过gobuild-gcflags'-m-l'就可以看到逃逸分析了,用指针到处传递过程和结果不一定是最好的。在正确使用之前,我想过要不要写“逃逸分析”相关的文章。直到最近夜读里看到有人问,我还是写了necessary。对于这块知识。我的建议是适当认识,但没必要死记硬背。就靠基础知识点加命令调试观察。就像曹达之前说的,“你们想了很久逃逸分析,压测之后,瓶颈就被锁住了”。不用太在意。。。参考Golang逃逸分析FAQ逃逸分析
