第 25 篇:Go 内存分配器解密:TCMalloc 的演进

大家好,我是公号「左诗右码」作者 Alex。

在面试中,当你被问到“Go 是如何分配内存的?”时,如果你的回答仅仅停留在“堆和栈”,那是远远不够的。
Go 的内存分配器是基于 Google 的 TCMalloc (Thread-Caching Malloc) 算法演进而来的,它的核心设计理念就是:多级缓存,降低锁竞争

今天,我们就来扒一扒这个复杂但极其精妙的内存分配机制。


1. 请简述 Go 是如何分配内存的?(Go 内存分配器架构)

Go 的内存分配器主要由三个核心组件构成:mcachemcentralmheap

你可以把它们想象成三级“仓库”:

  1. mcache (线程缓存)

    • 位置:绑定在每个 P (Processor) 上。
    • 特点无锁访问。因为每个 P 同一时间只会运行一个 Goroutine,所以 G 在申请内存时,优先去当前 P 的 mcache 里拿,速度极快。
    • 存储:存的是各种大小规格的 mspan(内存块)。
  2. mcentral (中心缓存)

    • 位置:全局共享。
    • 特点需要加锁。当 mcache 里的内存用完时,P 会去 mcentral 申请一批新的 mspan
    • 作用:作为中间商,平衡各个 P 之间的内存需求。
  3. mheap (堆)

    • 位置:全局共享。
    • 特点大锁。这是 Go 内存管理的最高层级。
    • 作用:如果 mcentral 也没货了,就会向 mheap 申请。mheap 负责直接向操作系统(OS)申请大块的虚拟内存(Arena),然后切割成 mspan 下发给 mcentral

一句话总结分配流程
Goroutine 需要内存 -> 查本地无锁 mcache -> 不够就加锁查全局 mcentral -> 还不够就查 mheap -> 最终向 OS 申请。


2. 大对象、小对象与微对象的分配策略

Go 会根据你申请的内存大小,采用不同的分配策略:

2.1 微对象 (Tiny Objects, < 16B)

比如 bool, int8 等。
Go 会把它们拼凑在一个 16 字节的内存块(Tiny Block)中分配。这就像是“拼车”,极大减少了内存碎片的产生。

2.2 小对象 (Small Objects, 16B ~ 32KB)

这是最常见的场景(绝大多数的结构体、切片都在这个范围)。
Go 会计算出它属于哪个具体的跨度类 (Size Class),然后直接去 mcache 对应的 mspan 链表中找空闲的格子。

2.3 大对象 (Large Objects, > 32KB)

如果对象非常大,它会直接绕过 mcachemcentral,直接向 mheap 申请分配。因为大对象在本地缓存里放不下,搬来搬去开销太大。


3. 为什么小对象多了会造成 GC 压力?

这是一个非常经典的高级面试题。

在实际业务中,如果我们频繁地申请和丢弃大量的小对象(比如在 for 循环里不断拼接字符串、解析巨大的 JSON 产生无数小结构体),会导致非常严重的后果:

  1. 扫描成本剧增
    Go 的垃圾回收(GC)是并发标记清除。GC 标记阶段需要遍历堆上的所有存活对象。小对象数量越多,对象之间的引用关系图(对象图)就越庞大、越复杂,GC 扫描的时间就越长。
  2. STW 延迟变长
    虽然 Go 优化了 STW 时间,但在 GC 开启和结束的瞬间仍有短暂的 STW。对象图过于复杂会拖慢整个 GC 周期,导致 CPU 大量时间被用在 runtime.gcBgMarkWorker 上,业务处理能力下降。
  3. 内存碎片与分配开销
    即便有 TCMalloc 的 Size Class 优化,极高频的申请依然会击穿 mcache,导致频繁向 mcentral 甚至 mheap 申请,增加了锁竞争和系统调用。

优化建议
面对大量小对象,我们通常的解决办法是对象池化。利用上一篇讲过的 sync.Pool 将小对象复用起来,让它们“长生不老”,从而逃过 GC 的频繁扫描和回收。


总结

  1. 分配架构mcache (P 本地无锁) -> mcentral (全局共享有锁) -> mheap (堆顶层)。
  2. 分配策略:微对象拼车,小对象查 mcache,大对象直达 mheap。
  3. 小对象危机:数量庞大的小对象会极大增加 GC 标记阶段的扫描负担,拖慢系统性能,需用 sync.Pool 破局。

了解了对象在堆上是如何分配的,下一个问题来了:Go 是怎么知道一个变量应该分配在堆上,还是分配在栈上的呢?
下一篇,我们来聊聊神秘的逃逸分析

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • Go语言内置运行时(就是runtime),抛弃了传统的内存分配方式,改为自主管理。这样可以自主地实现更好的内存使用...
    ddu_sw阅读 1,621评论 0 5
  • go的内存分配 Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分...
    GGBond_8488阅读 925评论 0 1
  • 转载:Go内存分配那些事,就这么简单! 这是Go语言充电站的第27期分享。 新老朋友好久不见,我是大彬,这篇文章准...
    meng_philip123阅读 1,454评论 0 1
  • 这篇文章可以看作是内存管理这篇长文的学习总结吧,原文基于源码剖析了整个go的内存管理,非常详尽。 程序中的数据和变...
    彦帧阅读 1,287评论 0 0
  • 总结 Go的内存结构 每个Go程序进程都由操作系统(OS)预先分配了一些虚拟内存,这是该进程可以访问的全部内存。在...
    GGBond_8488阅读 445评论 0 1

友情链接更多精彩内容