1. 逃逸分析

  • 在 Java、Go 等语言中,对象默认分配在堆上,需要垃圾回收(GC)管理,存在性能开销。栈上的变量,函数结束后变量会跟着回收掉,不会有额外性能的开销。如果逃逸分析发现某个对象不会逃逸出方法或线程(即仅在方法内部使用),则可以直接在栈上分配,方法结束时自动回收,无需 GC 介入。
  • 什么情况下会发生内存逃逸?
什么时候什么内容会被分配到栈上呢?方法内部声明的原始类型变量;方法被调用时,传入的实参值(对于基本类型)或引用地址(对于对象引用)会被压入栈中;对象本身可能在堆上,但指向这个对象的引用变量(reference)本身是存储在栈上的;函数调用的调用栈(Call Stack)本身。
  • 堆适合不可预知大小的。(比如只有一个指针?)
  • make初始化太大的切片那么切片就会逃逸到堆中。
  • 函数/方法内部的变量会不会返回出去,比如返回局部变量指针。(返回出去就到外面的作用域去了,局部变量生命周期变长了。)
  • 在闭包中引用包外的值。
  • Channel被设计用来实现协程间通信的组件,其作用域和生命周期不可能仅限于某个函数内部,所以直接分配在堆上。

2. 内存泄漏

2.1 情景

go中的内存泄漏一般都是goroutine泄漏。

  1. 如果goroutine在执行时被阻塞而无法退出,就会导致goroutine的内存泄漏,一个goroutine的最低栈大小为2KB,在高并发的场景下,对内存的消耗也是大的。
  2. 互斥锁未释放或者造成死锁会造成内存泄漏。
  3. 计时器stop。(time.Ticker是每隔指定的时间就会向通道内写数据。作为循环触发器,必须调用stop方法才会停止,从而被GC掉,否则会一直占用内存空间。)
  4. 函数数组传参引发内存泄漏。(在函数传参的时候用到了数组传参,且这个数组够大,或者该函数短时间内被调用N 次,短时间内分配大量内存,而又来不及GC,那么就会产生临时性的内存泄漏,对于高并发场景。)
  5. 切片共享底层数组的视图。处理其中一部分数据后,忘了复制,结果导致整个大数组占用内存不能释放。s1 = append([]int{}, s0[:3]...) // 完全复制

2.2 排查

怎么定位排查内存泄漏问题? pprof是Go的性能分析工具。top、火焰图都行。(要会介绍pprof)


3. TCMalloc架构

内置运行时编程语言会自主管理内存分配。好处是通过预分配、内存池之类的操作避免系统开销带来的性能下降。运行时编程语言对此的处理有5步:

①每次从操作系统申请一大块内存
②将申请到的大块按照预订大小切分成小块,构成链表(和内核里的slab差不多)
③对象分配内存时,从链表里找一块合适的提取出去。(和伙伴系统差不多)
④回收对象是将其挂回原链表复用
⑤闲置内存过多会归还给操作系统。

内存分配器其实只会管理内存,不关心对象状态,虽然回收工作也是它做,但是是垃圾回收器触发的。

内存分配器管理的内存有2种:span大内存块,object span切割后的小块span的组织方式、裁剪、拼接都和伙伴系统一样的。go中存对象的object按照8字节倍数分为n种,会存在装不满浪费的情况,不过内存分配器会尝试将小微object组合到一个object块内。

go的内存分配器用的TCMalloc架构。由三种组件组成:cache,central,heap

  1. cache相当于前线的,工作线程都绑定一个,这样可以无锁分配object。
  2. central是cache的跟班,为cache提供切好的span。
  3. heap就是后方支援,管理闲置span,可以从操作系统申请内存。

这样其实把锁和系统调用从内存分配的关键路径移出,提升了性能

小对象多会造成GC压力

小于等于32k的对象就是小对象,其它都是大对象。一般小对象通过mspan分配内存;大对象则直接由mheap分配内存。通常小对象过多会导致GC三色法消耗过多的CPU。优化思路是,减少对象分配。

4. 垃圾回收

4.1 V1.3标记-清除法

G1.3之前mark and swap的一个改进是可以先停止STW,放在后面来清除。

4.2 V1.5用三色标记法+插入删除屏障

重复上一步,直到灰色标记表中无任何对象。收集所有白色对象(垃圾)。如果没有STW那么可能回收的同时2对3的引用解除了,但是4对3已经有引用了,4已经黑了不会再被扫描了,3就误被当作了垃圾。

插入屏障:不存在黑色对象引用白色对象的情况了,因为这个屏障会把白色会强制变成灰色。

在准备回收白色前,重新遍历扫描一次栈空间。此时加STW暂停保护栈,防止外界干扰(有新的白色被黑色添加), 这时9才不会被回收。插入写屏障的不足:结束时需要STW来重新扫描栈,大约需要10~100ms。

删除写屏障的不足:回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。

4.3 V1.8三色标记法+混合写屏障机制

具体操作:

  1. GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW)
  2. GC期间,任何在栈上创建的新对象,均为黑色。
  3. 被删除的对象标记为灰色。
  4. 被添加的对象标记为灰色。

4.4 GC触发时机

触发方式 场景
所分配的堆大小达到阈值 默认情况下,Go会自动触发由控制器计算的触发堆的大,依据 GOGC 比例
手动触发 调试/测试场景, runtime.GC方法
距离上一个GC周期的时间超过一定时间 防止长时间不GC,释放长生命周期对象,默认2分钟

[Golang中GC回收机制三色标记与混合写屏障] (https://www.bilibili.com/video/BV1wz4y1y7Kd/?share_source=copy_web&vd_source=fc187607fc6ec6bbd2c74a3d0d7484cf)

PS

  • 对象之间的引用关系是如何被发现的?
    对象间的引用关系不是动态判断的,而是由编译器在编译时静态分析并记录在案的。GC在运行时只是一个“元数据执行者”,它按照编译期提供的“地图”(gcdata)去扫描内存,从而高效、准确地发现所有引用。
  • 根节点(Roots)是什么,它们是如何被确定和组织的?
    根节点是那些不需要通过追踪其他堆对象就能直接找到的对象引用。它们主要包括四大类:
    1. 全局变量:整个程序中所有在包级别定义的全局变量。它们存在于整个程序的生命周期。
    2. 各个Goroutine的栈:每个正在运行或阻塞的Goroutine都有自己的调用栈。栈上存储着函数的局部变量和参数,这些变量可能包含对堆对象的引用。
    3. 寄存器中的值:正在执行的Goroutine,其CPU寄存器中可能也保存着临时的对象指针。
    4. 其它运行时内部结构:例如,runtime.sched(调度器)中可能包含一些全局的活跃Goroutine列表等,这些结构也持有堆对象的引用。

文章作者: 易百分
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 易百分 !
  目录