浅谈Golang内存逃逸

目录
  • 1.什么是内存逃逸
  • 2.什么是逃逸分析
  • 3.小结
  • 4.逃逸分析案例
    • 1.函数返回局部指针变量
    • 2.interface类型逃逸
      • 1.interface产生逃逸
      • 2.指向栈对象的指针不能在堆中
    • 3.闭包产生逃逸
    • 4. 变量大小不确定及栈空间不足引发逃逸
  • 5.总结

1.什么是内存逃逸

在一段程序中,每一个函数都会有自己的内存区域分配自己的局部变量,返回值,这些内存会由编译器在栈中进行分配,每一个函数会分配一个栈帧,在函数运行结束后销毁,但是有些变量我们想在函数运行结束后仍然使用,就需要把这个变量分配在堆上,这种从“栈”上逃逸到“堆”上的现象叫做内存逃逸

2.什么是逃逸分析

虽然Go语言引入的Gc,GC机制会对堆上的对象进行管理,当某个对象不可达(没有其他对象引用他),他将会被回收。虽然GC可以降低工作人员负担,但是GC也会给程序带来性能损耗,当堆内存上有大量的堆内存对象,就会给GC很大的压力,虽然Go语言使用的是标记清除算法,并且在此基础上使用了三色标记法和写屏障技术,但是我们在堆上分配大量内存,仍然会对GC造成很大压力,Go引入了逃逸分析,就是想减少堆内存的分配,可以在栈分配的内存尽量分配在栈上

3.小结

逃逸分析就是在程序编译阶段根据代码中的数据流,对代码中哪些变量需要在栈上分配,哪些需要在对象分配的静态分析方法,堆和栈相比,堆适合分配不可预知大小的内存,但是付出代价是分配速度慢,容易产生碎片,栈分配十分快,栈分配只需要两个指令“Push”和"Release"分配和释放,而且堆分配需要先找一块适合大小的内存块分配,需要垃圾回收释放,所以逃逸分析可以更好的做内存分配

Go语言的逃逸分析

src/cmd/compile/internal/gc/escape.go

  • pointers to stack objects cannot be stored in the heap: 指向栈对象的指针不能存储在堆中
  • pointers to a stack object cannot outlive that object:指向栈对象的指针不能超过该对象的存活期,指针不能在栈对象销毁之后依然存活(例子:声明的函数返回并销毁了对象的栈帧,或者它在循环迭代中被重复用于逻辑上不同的变量)

既然逃逸分析是在编译阶段进行的,那我们就可以通过go build -gcflga '-m -m l'查看逃逸分析结果

4.逃逸分析案例

1.函数返回局部指针变量

func Add(x,y int) *int {
 res := 0
 res = x + y
 return &res
}
func main()  {
 Add(1,2)
}

.\pointer.go:4:2: res escapes to heap:
.\pointer.go:4:2:   flow: ~r2 = &res:
.\pointer.go:4:2:     from &res (address-of) at .\pointer.go:6:9
.\pointer.go:4:2:     from return &res (return) at .\pointer.go:6:2
.\pointer.go:4:2: moved to heap: res

函数返回局部变量是一个指针变量,函数Add执行结束,对应栈帧就会销毁,但是引用返回到函数外部,如果我们外部解析地址,就会导致程序访问非法内存,所以经过编辑器分析过后将其在堆上分配

2.interface类型逃逸

1.interface产生逃逸

func main()  {
   str := "荔枝"
   fmt.Println(str)
}

E:\GoStudy\src\HighBase\Escape>go build -gcflags="-m -m -l" ./pointer.go
# command-line-arguments
.\pointer.go:20:13: str escapes to heap:
.\pointer.go:20:13:   flow: {storage for ... argument} = &{storage for str}:
.\pointer.go:20:13:     from str (spill) at .\pointer.go:20:13
.\pointer.go:20:13:     from ... argument (slice-literal-element) at .\pointer.go:20:13
.\pointer.go:20:13:   flow: {heap} = {storage for ... argument}:
.\pointer.go:20:13:     from ... argument (spill) at .\pointer.go:20:13
.\pointer.go:20:13:     from fmt.Println(... argument...) (call parameter) at .\pointer.go:20:13
.\pointer.go:20:13: ... argument does not escape
.\pointer.go:20:13: str escapes to heap

str是main的一个局部变量,传给 fmt.Printl()之后逃逸,因为fmt.Println()的入参是interface{}类型,如果参数为interface{},那么编译期间就很难确定参数类型

2.指向栈对象的指针不能在堆中

我们把代码改成这样

func main()  {
   str := "苏珊"
   fmt.Println(&str)
}

# command-line-arguments
.\pointer.go:19:2: str escapes to heap:
.\pointer.go:19:2:   flow: {storage for ... argument} = &str:
.\pointer.go:19:2:     from &str (address-of) at .\pointer.go:20:14
.\pointer.go:19:2:     from &str (interface-converted) at .\pointer.go:20:14
.\pointer.go:19:2:     from ... argument (slice-literal-element) at .\pointer.go:20:13
.\pointer.go:19:2:   flow: {heap} = {storage for ... argument}:
.\pointer.go:19:2:     from ... argument (spill) at .\pointer.go:20:13
.\pointer.go:19:2:     from fmt.Println(... argument...) (call parameter) at .\pointer.go:20:13
.\pointer.go:19:2: moved to heap: str
.\pointer.go:20:13: ... argument does not escape

这次str也逃逸到堆上面了,在堆上面进行分配,因为入参是interface,变量str的地址被以实参的方式传入fmt.Println被装箱到一个interface{}

装箱的形参变量要在堆上分配,但是还需要存储一个栈上的地址,这和之前说的第一条不符,所以str也会分配到堆上

3.闭包产生逃逸

func Increase() func() int {
 n := 0
 return func() int {
  n++
  return n
 }
}

func main() {
 in := Increase()
 fmt.Println(in()) // 1
}

E:\GoStudy\src\HighBase\Escape>go build -gcflags "-m -m -l" ./pointer.go
# command-line-arguments
.\pointer.go:27:2: Increase capturing by ref: n (addr=false assign=true width=8)
.\pointer.go:28:9: func literal escapes to heap:
.\pointer.go:28:9:   flow: ~r0 = &{storage for func literal}:
.\pointer.go:28:9:     from func literal (spill) at .\pointer.go:28:9
.\pointer.go:28:9:     from return func literal (return) at .\pointer.go:28:2
.\pointer.go:27:2: n escapes to heap:
.\pointer.go:27:2:   flow: {storage for func literal} = &n:
.\pointer.go:27:2:     from n (captured by a closure) at .\pointer.go:29:3
.\pointer.go:27:2:     from n (reference) at .\pointer.go:29:3
.\pointer.go:27:2: moved to heap: n
.\pointer.go:28:9: func literal escapes to heap
.\pointer.go:36:16: in() escapes to heap:
.\pointer.go:36:16:   flow: {storage for ... argument} = &{storage for in()}:
.\pointer.go:36:16:     from in() (spill) at .\pointer.go:36:16
.\pointer.go:36:16:     from ... argument (slice-literal-element) at .\pointer.go:36:13
.\pointer.go:36:16:   flow: {heap} = {storage for ... argument}:
.\pointer.go:36:16:     from ... argument (spill) at .\pointer.go:36:13
.\pointer.go:36:16:     from fmt.Println(... argument...) (call parameter) at .\pointer.go:36:13
.\pointer.go:36:13: ... argument does not escape
.\pointer.go:36:16: in() escapes to heap

因为函数是指针类型,所以匿名函数当做返回值产生逃逸,匿名函数使用外部变量n,这个n会一直存在知道in被销毁

4. 变量大小不确定及栈空间不足引发逃逸

import (
    "math/rand"
)

func LessThan8192()  {
    nums := make([]int, 100) // = 64KB
    for i := 0; i < len(nums); i++ {
        nums[i] = rand.Int()
    }
}

func MoreThan8192(){
    nums := make([]int, 1000000) // = 64KB
    for i := 0; i < len(nums); i++ {
        nums[i] = rand.Int()
    }
}

func NonConstant() {
    number := 10
    s := make([]int, number)
    for i := 0; i < len(s); i++ {
        s[i] = i
    }
}

func main() {
    NonConstant()
    MoreThan8192()
    LessThan8192()
}

# command-line-arguments
.\pointer.go:43:14: make([]int, 100) does not escape
.\pointer.go:51:14: make([]int, 1000000) escapes to heap:
.\pointer.go:51:14:   flow: {heap} = &{storage for make([]int, 1000000)}:
.\pointer.go:51:14:     from make([]int, 1000000) (too large for stack) at .\pointer.go:51:14
.\pointer.go:51:14: make([]int, 1000000) escapes to heap
.\pointer.go:60:11: make([]int, number) escapes to heap:
.\pointer.go:60:11:   flow: {heap} = &{storage for make([]int, number)}:
.\pointer.go:60:11:     from make([]int, number) (non-constant size) at .\pointer.go:60:11
.\pointer.go:60:11: make([]int, number) escapes to heap

栈空间足够不会发生逃逸,但是变量过大,已经超过栈空间,会逃逸到堆上

5.总结

  • 逃逸分析在编译阶段确定哪些变量可以分配在栈中,哪些变量分配在堆上
  • 逃逸分析减轻了GC压力,提高程序的运行速度
  • 栈上内存使用完毕不需要GC处理,堆上内存使用完毕会交给GC处理
  • 函数传参时对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能
  • 根据代码具体分析,尽量减少逃逸代码,减轻GC压力,提高性能

到此这篇关于浅谈Golang内存逃逸 的文章就介绍到这了,更多相关Golang内存逃逸 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Go语言共享内存读写实例分析

    本文实例分析了Go语言共享内存读写的方法.分享给大家供大家参考.具体分析如下: 前面分析了Go语言指针运算和内嵌C代码的方法,做了一个Go语言共享内存读写的实验. 先大概说下什么是共享内存.我们知道不同进程见的内存是互相独立的,没办法直接互相操作对方内的数据,而共享内存则是靠操作系统提供的内存映射机制,让不同进程的一块地址空间映射到同一个虚拟内存区域上,使不同的进程可以操作到一块共用的内存块.共享内存是效率最高的进程间通讯机制,因为数据不需要在内核和程序之间复制. 共享内存用到的是系统提供的mm

  • Go pprof内存指标含义备忘录及案例分析

    最近组内一些Go服务碰到内存相关的问题,所以今天抽时间看了下Go pprof内存指标的含义,为后续查问题做准备. 内容主要来自于Go代码中对这些字段的注释,加自己的理解.理解不对的地方欢迎指正. // https://github.com/golang/go/blob/master/src/runtime/mstats.go#L150 // 总共从OS申请的字节数 // 是下面各种XxxSys指标的总和.包含运行时的heap.stack和其他内部数据结构的总和. // 它是虚拟内存空间.不一定全

  • Golang 内存模型详解(一)

    开始之前 首先,这是一篇菜B写的文章,可能会有理解错误的地方,发现错误请斧正,谢谢. 为了治疗我的懒癌早期,我一次就不写得太多了,这个系列想写很久了,每次都是开了个头就没有再写.这次争取把写完,弄成一个系列. 此 nil 不等彼 nil 先声明,这个标题有标题党的嫌疑. Go 的类型系统是比较奇葩的,nil 的含义跟其它语言有些差别,这里举个例子(可以直接进入 http://play.golang.org/p/ezFhXX0dnB 运行查看结果): 复制代码 代码如下: package main

  • golang切片内存应用技巧详解

    在 Go 语言中切片是使用非常频繁的一种聚合类型,它代表变长的序列,底层引用一个数组对象.一个切片由三个部分构成:指针.长度和容量.指针指向该切片自己第一个元素对应的底层数组元素的内存地址. 切片的类型声明如下: type slice struct { array unsafe.Pointer len int cap int } 多个切片之间可以共享底层数组的数据,并且引用的数组区间可能重叠.利用切片 的这个特性我们可以在原有内存空间中对切片进行反转.筛选和去重等操作,这样就不用声明一个指向新内

  • 解决golang内存溢出的方法

    最近在项目中出现golang内存溢出的问题,master刚开始运行时只有10多M,运行几天后,竟然达到了10多个G.而且到凌晨流量变少内存也没有明显降低,内存状态呈现一种很不健康的曲线. 像这种情况肯定是golang内存溢出了,为此我持续排查了两天,终于找到问题所在,特此记录下. 准备工作 一台较好的环境测试机,单台运行无污染. 压测工具,无论服务是http还是websocket服务,都必须准备好压测工具模拟最真实的用户场景. 将master引入net/http/pprof包,通过http访问获

  • Go语言中的内存布局详解

    一.go语言内存布局 想象一下,你有一个如下的结构体. 复制代码 代码如下: type MyData struct {         aByte   byte         aShort  int16         anInt32 int32         aSlice  []byte } 那么这个结构体究竟是什么呢? 从根本上说,它描述了如何在内存中布局数据. 这是什么意思?编译器又是如何展现出来呢? 我们来看一下. 首先让我们使用反射来检查结构中的字段. 二.反射之上 下面是一些使用

  • go语言中切片与内存复制 memcpy 的实现操作

    Go 语言原则上不支持内存的直接操作访问,但是提供了切片功能. 最初我以为切片就是动态数组,实际程序设计过程中发现,切片是提供数组一个内存片段的一个合法的手段,利用切片功能,实际上我们可以自由访问数组的任何一个片段,因而可以借助 copy 函数,实现内存复制. 不同类型之间的数据复制,可以借助 unsafe 取出变量地址,类型转换为数组后,利用数组切片,实现内存复制. 不罗嗦了,示例代码如下: package main import ( "fmt" "unsafe"

  • 详解Go内存模型

    介绍 Go 内存模型规定了一些条件,在这些条件下,在一个 goroutine 中读取变量返回的值能够确保是另一个 goroutine 中对该变量写入的值.[翻译这篇文章花费了我 3 个半小时 ] Happens Before(在-之前发生) 在一个 goroutine 中,读操作和写操作必须表现地就好像它们是按照程序中指定的顺序执行的.这是因为,在一个 goroutine 中编译器和处理器可能重新安排读和写操作的执行顺序(只要这种乱序执行不改变这个 goroutine 中在语言规范中定义的行为)

  • 浅谈Golang内存逃逸

    目录 1.什么是内存逃逸 2.什么是逃逸分析 3.小结 4.逃逸分析案例 1.函数返回局部指针变量 2.interface类型逃逸 1.interface产生逃逸 2.指向栈对象的指针不能在堆中 3.闭包产生逃逸 4. 变量大小不确定及栈空间不足引发逃逸 5.总结 1.什么是内存逃逸 在一段程序中,每一个函数都会有自己的内存区域分配自己的局部变量,返回值,这些内存会由编译器在栈中进行分配,每一个函数会分配一个栈帧,在函数运行结束后销毁,但是有些变量我们想在函数运行结束后仍然使用,就需要把这个变量

  • 浅谈Golang是如何读取文件内容的(7种)

    本文旨在快速介绍Go标准库中读取文件的许多选项. 在Go中(就此而言,大多数底层语言和某些动态语言(如Node))返回字节流. 不将所有内容自动转换为字符串的好处是,其中之一是避免昂贵的字符串分配,这会增加GC压力. 为了使本文更加简单,我将使用string(arrayOfBytes)将bytes数组转换为字符串. 但是,在发布生产代码时,不应将其作为一般建议. 1.读取整个文件到内存中 首先,标准库提供了多种功能和实用程序来读取文件数据.我们将从os软件包中提供的基本情况开始.这意味着两个先决

  • 浅谈Golang Slice切片如何扩容的实现

    目录 一.Slice数据结构是什么? 二.详细代码 1.数据结构 2.扩容原则 3.如何理解扩容规则一 1.当小于1024个元素时 2.当大于1024个元素时 4.如何理解扩容规则二 1.简单理解内存地址更换 总结 一.Slice数据结构是什么? 切片(slice)是 Golang 中一种比较特殊的数据结构,这种数据结构更便于使用和管理数据集合.切片是围绕动态数组的概念构建的,可以按需自动增长和缩小.切片(slice)是可以看做是一个长度可变的数组.切片(slice)自身并不是动态数组或者数组指

  • 浅谈Golang的new与make区别是什么

    目录 new make 小结: 区别:在go语言中,make和new都是内存的分配(堆上),但是make只用于slice.map以及channel的初始化(非零值):而new用于类型的内存分配,并且内存置为零.make返回的是引用类型本身:而new返回的是指向类型的指针. 本文操作环境:windows10系统.GO 1.11.2.thinkpad t480电脑. Go语言中new和make都是用来内存分配的原语(allocation primitives).简单的说,new只分配内存,make用

  • 浅谈Golang数据竞态

    目录 一个数据竞态的case 检查数据竞态 解决方案 1.WaitGroup等待 2.Channel阻塞等待 3.Channel通道 4.互斥锁 典型数据竞态 1.循环计数上的竞态 2.意外共享变量 3.无保护的全局变量 4.原始无保护变量 5.未同步的发送和关闭操作 本文以一个简单事例的多种解决方案作为引子,用结构体Demo来总结各种并发读写的情况 一个数据竞态的case package main import ( "fmt" "testing" "ti

  • 浅谈java+内存分配及变量存储位置的区别

    Java内存分配与管理是Java的核心技术之一,之前我们曾介绍过Java的内存管理与内存泄露以及Java垃圾回收方面的知识,今天我们再次深入Java核心,详细介绍一下Java在内存分配方面的知识.一般Java在内存分配时会涉及到以下区域: ◆寄存器:我们在程序中无法控制 ◆栈:存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中(new 出来的对象) ◆堆:存放用new产生的数据 ◆静态域:存放在对象中用static定义的静态成员 ◆常量池:存放常量 ◆非RAM存储:硬盘等永久

  • 浅谈Java内存区域与对象创建过程

    一.java内存区域 Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁.根据<Java虚拟机规范(JavaSE7版)>的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域. 1.程序计数器(线程私有) 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码

  • 浅谈java内存管理与内存溢出异常

    说到内存管理,笔者这里想先比较一下java与C.C++之间的区别: 在C.C++中,内存管理是由程序员负责的,也就是说程序员既要完成繁重的代码编写工作又要时常考虑到系统内存的维护 在java中,程序员无需考虑内存的控制和维护,而是交由JVM自动管理,这样就不容易出现内存泄漏和溢出的问题.然而,一旦出现内存泄漏和溢出方面的问题,如果不了解JVM的内存管理机制就很难找到错误所在. 1.JVM运行时数据区 JVM在运行java程序的时候会将它所管理的内存划分为若干个不同的区域,这些区域不仅有自己的用途

  • 浅谈C++内存分配及变长数组的动态分配

    第一部分 C++内存分配 一.关于内存 1.内存分配方式 内存分配方式有三种: (1)从静态存储区域分配.内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在 例如全局变量,static变量. (2)在栈上创建.在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存 储单元自动被释放.栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限. (3) 从堆上分配,亦称动态内存分配.程序在运行的时候用malloc或new申请任意多少的内存,程序员

  • 浅谈Java内存区域划分和内存分配策略

    如果不知道,类的静态变量存储在那? 方法的局部变量存储在那? 赶快收藏 Java内存区域主要可以分为共享内存,堆.方法区和线程私有内存,虚拟机栈.本地方法栈和程序计数器.如下图所示,本文将详细讲述各个区域,同时也会讲述创建对象过程,内存分配策略, 和对象访问定位原理.觉得写得好的,可以点个收藏,绝对不亏. Java内存区域 程序计数器 程序计数器,可以看作程序当前线程所执行的字节码行号指示器.字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支.循环.跳转.异常处理都需

随机推荐