【go基础】9.内存分配, 内存逃逸, 栈空间

概括


1、基于多级内存管理策略

首先把对象按大小分为微对象(<16B)、小对象(<32KB)和大对象(>32KB),并分别处理。 因为不同大小的对象有不同的分配和回收成本, 这种策略有助于提高内存分配器的性能。

2、内存分配器

分为线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)来分级管理内存。
线程缓存属于每个独立的线程,所以不需要锁,减少了锁竞争带来的性能损耗。
微对象和小对象都是由线程缓存分配的,不够时会向中心缓存拿一块新的。
大对象直接从页堆上分配。

3、span和元素

在g o 中,内存被分为多个 span ,是最小内存分配单位,每个 span 一块 连续内存区域。 span 又进一步被切分为元素,用于分配不同大小的对象。分配对象时,并不是直接分配 span ,而是分配 span 中的元素。
当程序申请内存时会取一块新的span到线程缓存中。
go的所有内存空间都由二维矩阵heapArena管理。一个heapArena下有多个mspan。

4、内存预分配与切分

go 语言每次从操作系统直接申请一大块内存(如 1MB ),以减少系统调用的次数。这样做可以显著提高性能,因为系统调用相对成本较高。
申请到的大块内存会按照特定大小预先切分成小块,构成链表。当需要为对象分配内存时,只需从大小合适的链表中提取一小块即可。这种策略减少了频繁申请小块内存的开销。

5、内存回收与复用

当对象不再需要时,其占用的内存块会被重新归还原链表,以便复用。这种设计减少了内存碎片,提高了内存的利用率。
当闲置内存过多时, GC 尝试归还多于内存给操作系统,以降低整体开销。

6、主旨

总的来说, Go 的内存分配主旨是尽量多的内存复用,通过减少系统调用,用多级内存管理、线程局部缓存的方式,提高性能和内存利用率。

内存的几个部分


  • Mutator: 用户程序
  • Allocator: 内存分配器
  • Collector: 垃圾回收器
  • Heap: 堆

1、栈区(stack sagment)

  •  存放函数的参数,返回值,局部变量值
  • 在编译期确定
  • 由高向低扩展,内存连续(栈顶地址和最大容量是预先规定好的,windows中栈的大小是2M)
  • 申请时若栈的剩余空间不足,会提示overflow
  • 能从栈获得的空间较小
  • goroutine的初始栈大小2KB

2、堆区(heap sagment) 

  • 存放被动态分配的内存
  • 在运行时分配,C++由工程师主动申请和释放内存,Go是由内存分配器分配并由垃圾收集器回收
  • 由低向高扩展,不连续的内存(链表存储,链表是由低向高的)
  • 堆获得的空间比较灵活,也比较大,受限于计算机的虚拟内存

3、全局区(静态区)(data sagment)

  • 全局变量和静态变量的存储在一起
  • 程序结束后由系统释放
  • 大小由系统限定,一般很大

4、文字常量区

  • 存放常量字符串
  • 程序结束后由系统释放

5、程序代码区

  • 存放函数体的二进制代码

内存的分配方法


1、线性分配器

在内存中维护一个指向内存位置的指针,申请内存时,分配器只需要检查剩余的空闲内存,返回分配的内存区域并移动指针。复杂度低,执行效率高。

但内存被回收后无法重用,要配合垃圾回收机制使用。

2、空闲链表分配器

用链表维护内存空间。

可以方便的进行内存回收重用。

由于分配内存时需要遍历链表,所以时间复杂度是 𝑂(𝑛)。内存块的选择方式可以使用不同的策略。

go的分级内存分配


go的内存分配方式是基于线程缓存分配(Thread-Caching Malloc,TCMalloc),比 glibc 中的 malloc 还要快很多。主要是使用多级缓存将对象根据大小分类,并按照类别实施不同的分配策略。

0、为什么分配速度快

  • 分级分配和缓存策略
  • 内存对齐机制
  • 逃逸分析
  • 栈上分配的优势

1、对象大小

  • tiny 微对象  <16B
  • 小对象          <32KB
  • 大对象          >32KB

2、多级缓存

  • 线程缓存 mcache
  • 中心缓存 mcentral
  • 页堆        mheap

   mcache属于每个独立的线程,所以不需要锁,减少了锁竞争带来的性能损耗。

   tiny对象和小对象都由mcache分配,不够时会向mcentral拿一块新的。

   大对象直接从mheap分配。

3、地址空间的4种状态

  • none:初始状态
  • reserved:地址已经被runtime拥有,但并未真正分配,访问会异常
  • prepared:地址已经是reserved,但并未真正分配,内存被保留,一般没有对应的物理内存访问该片内存的行为是未定义的可以快速转换到ready状态
  • ready:地址空间已被分配, 可以被安全访问

4、主要结构

(1)mspan

go内存的基本单元,管理一串地址连续的空间。

双向链表存储结构。

(2)mcache

每个P都有一个mcache,每一个mcache持有 68*2 个mspan。

mcache在刚被初始化时是不包含mspan的,只有当用户申请内存时才会从上一级组件获取新的mspan。

(3)mcentral

全局的,访问需要锁。

存储包含空闲对象和不包含空闲对象的内存管理单元。

(4)mheap

全局的,用于管理golang的整个堆。

真实拥有虚拟地址,当mcentral不够时候,会向mheap申请

mheap包含一个长度为 136 的mcentral数组,其中 68 个是scan的mcentral,另外的 68 个是noscan的mcentral

(5)heapArena 

Go的所有内存空间都由二维矩阵heapArena管理。

一个heapArena下有多个mspan。

5、整体结构

所有的go程序都会在启动时初始化如下内存布局,每一个线程P都会分配一个mcache用于处理tiny对象和小对象的分配,它们会持有内存管理单元mspan。

每个类型的mspan都会管理特定大小的对象,当mspan中不存在空闲对象时,它们会从mheap持有的134个中心缓存mcentral中获取新的内存单元,mcentral属于全局的堆结构体mheap,它会从操作系统中申请内存。

在amd64的linux操作系统上,mheap会持有4194304个heapArena,每个heapArena都会管理 64MB 的内存,单个go程序的内存上限也就是256TB。

内存逃逸


1、含义

函数的局部变量是分配在栈上的,栈的空间是确定的,当变量太大时,会逃逸到堆上。
简单说内存逃逸就是局部变量通过堆分配和回收。

2、危害

  • 函数结束后不需要的变量会留在堆里
  • 堆的分配和回收开销比栈大得多
  • 会造成gc压力,增加内存碎片

3、逃逸原则

  • 编译期不确定变量的具体类型
  • 变量生命周期不确定
  • 变量占用内存较大(>32KB)
  • 变量大小不确定

4、导致内存逃逸的操作

(1)向 channel 发送指针

编译时不知道channel中的数据会被哪个goroutine接收,所以编译器不知道变量什么时候才会被释放,只能放入堆中。

(2)局部变量在函数结束后还被使用

如:函数返回局部变量指针、闭包中引用包外的值。因为变量的生命周期可能会超过函数周期,所以会放入堆中。

(3)slice 或 map 中存储指针

比如 []*string,后面的数组可能是在栈上分配的,但其引用的值还是在堆上。

(4)切片扩容

扩容后长度太大,导致栈空间不足,逃逸到堆上。

(5)在 interface 类型上调用方法

会把interface变量使用堆分配, 因为方法的真正实现只能在运行时知道。

5、如何避免

  • 对小数据,使用传值而不是传指针
  • 避免使用长度不固定的slice
  • interface调用方法会发生内存逃逸,谨慎使用

栈空间


1、栈扩容

(1)原因

golang使用连续栈,goroutine初始时只给栈分配很小的空间(2KB),随着函数的调用层级加深,Go 的初始栈空间就可能不够用,就会触发栈空间的扩容

(2)时机

编译器的运行时检查runtime.morestack,在几乎所有函数调用之前

(3)操作

调用runtime.newstack 创建新的栈,调用runtime.copystack进行栈拷贝,并调整原来指针的指向新栈空间翻倍,最大限制1G。 

2、栈缩容

(1)原因

在函数返回后,对应的栈空间会回收,如果调用栈比较深,随着函数一个一个返回,回收的栈空间会越来越多。假设在调用栈最深的时候,整体的栈空间扩容到了 100M,那么随着函数的返回,到某一个函数的时候,100M 的栈空间只有 1M 是实际占用的,内存利用率只有 1%。

(2)时机

在垃圾回收时,检查栈空间的内存利用率,当低于1/4 时,开始缩容

(3)操作

调用扩容时使用的 runtime.copystack 开辟新的栈空间,并调整原来指针的指向

栈空间减少一半,最低限制 2KB

3、疑问

- 栈扩容是什么时候做的?会不会出现函数执行到一半扩容?

是在函数调用期间进行,不是在函数执行期

- 扩容和缩容会改变已有变量的地址吗?

栈扩容时,已经创建的变量的地址可能会改变。这是因为栈扩容涉及将当前栈的内容复制到新的栈上,并更新指针以指向新的栈。
然而,对于已经创建并且在栈扩容之前就已经被赋值或引用的变量,Go运行时会采取一些优化措施来确保这些变量的地址仍然有效,它会将该变量的地址映射到新的内存地址上,以确保对该变量的引用不会失败
这种映射是通过维护一些内部数据结构来实现的,这些数据结构记录了变量在内存中的位置和偏移量。因此,虽然栈扩容可能会导致已创建的变量的地址发生改变,但在实际运行时,Go语言会采取一些措施来确保这些变量的地址仍然有效。
我们尽量使用指针或引用来访问变量,以确保在栈扩容时能够正确引用新的地址。
(如果变量存储在堆上,其地址通常不会改变。堆的扩容和缩容是动态分配和释放内存来实现的,而不是通过复制整个堆的内容。) 
  • 10
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Go语言中,内存逃逸指的是当一个对象的指针被多个方法或线程引用时,这个指针会逃逸到堆上。内存逃逸的位置由编译器决定,而不像C或C++可以使用malloc或new在堆上分配内存。根据内存分配的基本原则,当函数外部对指针没有引用时,优先分配在上;当函数外部对指针存在引用时,优先分配在堆上;当函数内部分配一个较大对象时,优先分配在堆上。\[1\] 在Go语言中,内存逃逸分析可以通过一些规则来判断。例如,当使用等号=赋值时,如果Data和Field都是引用类型的数据,则会导致Value逃逸。另外,一些特定的数据类型也会导致逃逸,比如\[\]interface{}、map\[string\]interface{}、map\[interface{}\]interface{}、map\[string\]\[\]string、map\[string\]*int、\[\]*int、func(*int)、func(\[\]string)、chan \[\]string等。具体的规则可以参考引用\[2\]中的示例。\[2\] 此外,空间不足也可能导致内存逃逸。当在函数中创建一个较大的切片或数组,并且空间不足以容纳它们时,这些切片或数组会逃逸到堆上。例如,在一个函数中创建一个长度为10000的切片,如果空间不足,这个切片就会逃逸到堆上。\[3\] 总结来说,内存逃逸是指当一个对象的指针被多个方法或线程引用时,这个指针会逃逸到堆上。在Go语言中,内存逃逸的位置由编译器决定,可以通过一些规则和空间的判断来进行分析。 #### 引用[.reference_title] - *1* [golang内存逃逸分析](https://blog.csdn.net/qq_42170897/article/details/127770234)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [golang内存逃逸](https://blog.csdn.net/wanghao3616/article/details/107284523)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [golang——内存逃逸机制](https://blog.csdn.net/weixin_45627369/article/details/127163797)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值