Go内存分配器的核心思想

1、内存分配器的核心思想

Go 的内存分配器核心思想是将内存使用多级管理,降低锁的粒度。每个线程都有自己的本地内存,使用时先从线程本地的内存池进行分配,当内存池不足时,才会从全局内存池中进行申请。

2、内存分配器的初始化

Go在启动的时候,会向系统申请一大块内存块(只是一段虚拟地址,不会真正地分配内存)。申请的内存块会划分成三个部分「mspans」,「bitmap」,「arena」。

  • arena就是堆,Go 中所有动态分配的内存都是来自这里的
  • bitmap是一个位图,用于标识 arena 中哪些地址保存了对象,并且保存了这个对象是否是否包含指针,还有一些GC信息
  • mspans保存了 mspan 们的指针。mspan 是 GO 中内存管理的基本单元,有由一组 8k 的连续内存块组成的双向链表,mspan 可以按照一定大小划分成 object。(内存对齐)。mspan 的大小等级有 67 种。

3、内存分配器的组件有什么

Go的内存分配器的组件由「线程缓存 mcache」,「中心缓存 mcentral」,「页堆 mheap」

  • 线程缓存 mcache在程序刚启动时是没有分配任何内存的,在运行时才会动态向「中心缓存 central」申请(有锁)并缓存下来。由于每个时刻只能有一个 P 独占 M,所以线程缓存没有存在多个 P 竞争申请内存的问题
  • 中心缓存 mcentral是带锁的全局缓存,被全部线程共享,每个 central 管理一种大小的 mspan,并且会同个大小的 mspan 会创建两个 central 进行管理,一个是有指针另一个是无指针(无指针在垃圾回收中无需进一步扫描是否引用活跃对象)
  • 页堆 mheap管理全部的 「中心缓存 mcentral」,一些 arena 、bit map 相关的属性,除此之外还有两个二叉搜索树,分别是 free 和 scav。free 中保存着新从 OS 中刚申请的 mspan 和非垃圾回收后的 span。scav 中保存的是被垃圾回收后的 span。

4、Go 的内存分配器的初始化以及分配内存的过程

Go 会将运行中的使用到的对象划分为三种,微对象(0到16B),小对象(16B到32K),大对象(32K以上)

微小对象申请过程:

如果是创建一个微或者小对象,就会向 mcache 进行申请。mcache 如果没有空闲的 span 的话,会向 mcentral 申请。mcentral 管理着两个 spanlist,一个是 nonempty(里面有空闲的 span),一个是 empty(无空闲的链表),mcentral 会从 nonempty 找到可用的 span,将其移至 empty list,并将其返回给线程。当 mcentral 也没有空闲时 span,会向 mheap 申请。mheap 维护着两个二叉搜索树,分别是 scav(垃圾回收后会还的) 和 free(从 OS 申请的),mheap 会优先从 scav 中分配,无则从 free 中分配。若两者都没有空闲的,则会从 OS 中申请,之后再次遍历两颗二叉搜索树。

大对象申请过程:

大对象不经过 mcache 和 mcentral,直接从 mheap 申请内存,分配在 mheap 的 arena 中

参考链接以及延伸阅读

draveness-Go 语言设计与实现-7.1 内存分配器[1]

图解Go内存管理器的内存分配策略[2]

详解Go中内存分配源码实现[3]

TCMalloc : Thread-Caching Malloc[4]

详解Go语言的内存模型及堆的分配管理[5]

图解Go语言内存分配[6]

Go语言原本-第 7 章 内存分配[7]

栈内存管理[8]

5、epoll 用到了哪些数据结构

红黑树和双端链表

1
2
3
4
5
6
7
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist; ....
}

扩展阅读

  • epoll的内部实现 看了就会懂[9]
  • linux epoll机制[10]

6、在使用 map 时尽量不要在 big map 中保存指针?为什么

Go 语言的 GC 会递归遍历并标记所有可触达的对象,标记完成之后将所有没有引用的对象进行清理。扫描到指针就会往下接着寻找,一直到结束。

Go 语言中 map 是基于数组和链表的数据结构实现的,通过链地址法解决哈希冲突,每个 bucket 可以保存 8对键值,在 8 个键值对数据后面有一个 overflow 指针,因为桶中最多只能装 8 个键值对,如果有多余的键值对落到了当前桶(哈希冲突),那么就需要再构建一个桶(称为溢出桶),通过 overflow 指针链接起来。

因为 overflow 指针的缘故,所以无论 map 保存的是什么,GC 的时候就会把所有的 bmap 扫描一遍,带来巨大的 GC 开销。官方 issues 就有关于这个问题的讨论,runtime: Large maps cause significant GC pauses #9477[11]

1
2
3
4
5
6
// If we have a map[k]v where both k and v does not contain pointers and we want to improve scan performance, then we can do the following.
// 如果我们有一个map [k] v,其中k和v都不包含指针,并且我们想提高扫描性能,则可以执行以下操作。
//Add 'allOverflow []unsafe.Pointer' to hmap and store all overflow buckets in it. Then mark bmap as noScan. This will make scanning very fast, as we won't scan any user data.
// 将'allOverflow [] unsafe.Pointer'添加到 hmap 并将所有溢出存储桶存储在其中。 然后将 bmap 标记为noScan。 这将使扫描非常快,因为我们不会扫描任何用户数据。
// In reality it will be somewhat more complex, because we will need to remove old overflow buckets from allOverflow. And also it will increase size of hmap, so it may require some reshuffling of data as well.
// 实际上,它将有些复杂,因为我们需要从allOverflow中删除旧的溢出桶。 并且它还会增加 hmap 的大小,因此也可能需要重新整理数据。

最终官方在 hmap 中增加了 overflow 相关字段完成了上面的优化,这是具体的 commit[12] 地址。

下面看下具体是如何实现的,基于最新Go1.16 ,源码位置在https://github.com/golang/go/blob/dev.boringcrypto.go1.16/src/cmd/compile/internal/gc/reflect.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// bmap makes the map bucket type given the type of the map.
func bmap(t *types.Type) *types.Type {
...
// The first field is: uint8 topbits[BUCKETSIZE].
...

// If keys and elems have no pointers, the map implementation
// can keep a list of overflow pointers on the side so that
// buckets can be marked as having no pointers.
// Arrange for the bucket to have no pointers by changing
// the type of the overflow field to uintptr in this case.
// See comment on hmap.overflow in runtime/map.go.
otyp := types.NewPtr(bucket)
if !elemtype.HasPointers() && !keytype.HasPointers() {
otyp = types.Types[TUINTPTR]
}
overflow := makefield("overflow", otyp)
field = append(field, overflow)

// link up fields
...

// Check invariants that map code depends on.
...

// Double-check that overflow field is final memory in struct,
// with no padding at end.
...
}

通过注释可以看出,如果 map 中保存的键值都不包含指针(通过 Haspointers 判断),就使用一个 uintptr 类型代替 bucket 的指针用于溢出桶 overflow 字段,uintptr 类型在 Go 语言中就是个大小可以保存得下指针的整数,不是指针,就相当于实现了 将 bmap 标记为 noScan, GC 的时候就不会遍历完整个 map 了。

图片

参考资料

[1] draveness-Go 语言设计与实现-7.1 内存分配器: https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#71-%e5%86%85%e5%ad%98%e5%88%86%e9%85%8d%e5%99%a8

[2] 图解Go内存管理器的内存分配策略: https://blog.csdn.net/kevin_tech/article/details/108426871

[3] 详解Go中内存分配源码实现: https://www.cnblogs.com/luozhiyun/p/14349331.html

[4] TCMalloc : Thread-Caching Malloc: http://goog-perftools.sourceforge.net/doc/tcmalloc.html

[5] 详解Go语言的内存模型及堆的分配管理: https://zhuanlan.zhihu.com/p/76802887

[6] 图解Go语言内存分配: https://zhuanlan.zhihu.com/p/59125443

[7] Go语言原本-第 7 章 内存分配: https://golang.design/under-the-hood/zh-cn/part2runtime/ch07alloc/

[8] 栈内存管理: https://studygolang.com/articles/25547

[9] epoll的内部实现 看了就会懂: https://blog.csdn.net/tianjing0805/article/details/76021440

[10] linux epoll机制: https://blog.csdn.net/u010657219/article/details/44061629

[11] runtime: Large maps cause significant GC pauses #9477: https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fgolang%2Fgo%2Fissues%2F9477

[12] commit: https://links.jianshu.com/go?to=https%3A%2F%2Fgo-review.googlesource.com%2Fc%2Fgo%2F%2B%2F3288

[13] Go语言使用 map 时尽量不要在 big map 中保存指针: https://www.jianshu.com/p/5903323a7110

图片

转载自他人blog

-------------本文结束 感谢阅读-------------