GC
GC是自动化内存管理回收机制
虚拟内存函数栈的数据是会根据函数返回而自动销毁的,而堆上的数据是不会随着函数自动销毁的,堆内数据会随着程序运行而逐渐变大,从而导致内存OOM,Go语言就用了GC来清理堆上的内存数据。
如何区分垃圾
堆上内存数据不在被栈上和数据段上的引用变量所指向,那就一定 是内存垃圾。这个原理称为"可达性",近似等价于”存活性“
常见垃圾GC方法
-
引用计数reference counting:php的GC,每个对象都有一个被引用的计数器,每被引用一次则被引用对象计数器+1,当引用对象失效,则被引用对象计数器-1,当计数器是0时,可回收
- 优点:实时性好,当计数器变0,触发GC清理内存
- 缺点:当AB对象是嵌套引用,则AB对象的计数器不会变成0,导致无法被回收
-
标记-清除Mark and Sweep(非移动式垃圾回收):每次启动垃圾回收都会暂停当前所有的正常代码执行,运行一个扫描线程重新运行系统获得链路,通过链路判断对象是否可被触达,如果能触发说明对象当前正在被使用,不可回收;反之,没有触达到的对象则认为无用,可以回收。
因为GC是清理堆上的数据,那么链路分为2种:
- 栈到堆
- 数据段到堆
-
三色标记法:是mark and sweep的优化版,go语言用的此方法
-
标记-整理 Mark and Compact(移动式垃圾回收):原理和标记清除Mark and Sweep是一样的链路可达性。Mark and Compact会在清理完堆上的内存后将内存重新整合到一起,避免内存碎片过多。可是整理肯定会涉及到内存的迁移和栈上数据的重新指向
-
复制式移动法(移动式垃圾回收):他同Mark and Compact算法,他会将堆内存上分成2个一样大小的内存区间(from,to)。原来存在from空间的数据,被GC清理后剩余的内存全部存到to空间,from内存清空,避免内存碎片过多。他也是会涉及到内存的迁移和栈上数据的重新指向,并且堆内存只有一半的使用率
-
分带回收:这里不一一介绍了
移动式垃圾回收 及 会移动堆内存空间,避免内存碎片过多
非移动式垃圾回收 及 不会移动堆内存空间,会有很多内存碎片,产生内存浪费
三色标记法(白,灰,黑)
三种颜色的作用:
- 白:需要清理的变量
- 灰:还需要进一波往下追踪变量
- 黑:追踪完毕,不要清理的对象
三色标记法步骤:
- 将内存(栈,堆,数据段)上的所有变量标记为白色
- 栈和数据段的变量作为根节点root
- 如果根节点变量是值变量标记为黑色,如果是引用变量标记为灰色
- 灰色变量是引用变量,他们肯定是会存着堆/栈上的某一个地址。根据这个链路往下查找有3种情况:
- 如果地址上有数据,并且是值变量,跟节点标记成灰色
- 链路上的所有节点都标记成黑色
- 如果地址上没有数据,根节点root上的灰色节点会随着函数返回而释放,那剩余的节点都是白色的,会被GC标记为要清理的内存
GC协程如何进行释放内存
根据时间片段,有多种方式:
- STW:stop the world,暂停业务进程,GC结束后才继续消费协程
- 增量式垃圾回收
- 多协程并发
- 多协程并行
- 主体并发式:STW + 多协程并发
STW
STW,stop the world;让程序暂停,GC扫描标记标记GCROOTS的对象引用。会阻塞进程,让系统有卡顿现象。
增量式垃圾回收
将STW切割成多个时间段,让垃圾回收和程序并发执行(交替执行)。但是这个会引起新的问题,就是前一个时间段的GC将节点标记成了黑色,切换到程序后被变量赋值,重新指向了白色,导致下一个时间片的GC把白色节点的内存释放导致OOM。
黑指向白的实例:
- GC段1:将一个切片变量a被标记成黑色,栈和堆上都是黑色(切片是引用变量所以存在数据结构堆内,栈存着指针)
- 切换到业务程序:创建了一个切片变量b(GC默认节点都是白色,所以此时b的堆栈都是白色节点),切片变量a的pointer被重新赋值指向切片b的七层数组。此时变量a的栈是黑色,堆是白色
强三色不变式解决 增量式垃圾回收时间段GC问题
不允许黑色节点指向白色对象,在业务程序修改操作时,创建读/写屏障:
- 写屏障:在修改对象时,通知垃圾回收器,记录到一个集合里(hash或者列表)。GC开始写读集合里的数据,将集合里的黑色节点变成灰色,或者将白色节点变成灰色。这样就可以触发重新扫描到新创建的节点(本该是白色的节点)
- 读屏障:
- 移动式垃圾回收 (会移动堆内存空间,避免内存碎片过多):TODO
- 非移动式垃圾回收 (不会移动堆内存空间,产生内存浪费):不需要读屏障
弱三色不变式解决 增量式垃圾回收时间段GC问题
所有被黑色对象引用的白色对象都处于灰色保护状态.
多线程并发回收
一个线程进行GC,其他线程进行业务代码的运行。
考虑点:
- 写屏障,被GC的读和业务线程的写同时操作,可能会引起读写不一致问题
多线程并行回收
多个线程同一个时间段进行回收。
考虑点:
- 会引发多线程抢占,所以又要考虑多线程安全锁
- 如果是移动式垃圾回收,还要考虑多个线程对同一个内存进行复制,导致内存冗余
主体并发式:STW + 多协程并发 + 增量式
在某些特定情况下,仍然需要使用Stop-The-World(STW)来保证线程安全。以下是一些可能需要使用STW的情况:
- 根扫描阶段:在垃圾回收的根扫描阶段,需要确保所有的根对象都被正确扫描,并将其标记为活动对象。为了保证扫描的准确性和一致性,可以采用STW来暂停所有用户线程的运行,然后执行根扫描操作。
- 内存分配:当进行内存分配时,需要确保内存分配器的内部数据结构状态的一致性。在某些情况下,可能需要通过STW来暂停用户线程并进行内存分配操作,以避免并发访问带来的线程安全问题。
- 垃圾回收器的调整和优化:在对垃圾回收器进行调整和优化时,可能需要使用STW来确保对垃圾回收器的修改不会与正在进行的垃圾回收过程产生冲突。这样可以避免并发访问导致的数据不一致性和错误。
go语言GC具体操作
巨人肩膀
https://blog.csdn.net/weixin_42322309/article/details/106915910?spm=1001.2014.3001.5502
https://www.bilibili.com/video/BV1n5411H7qS/?spm_id_from=333.999.0.0&vd_source=6e9548d830b147140ad59cd8010b6e7b