GC

回到正题:什么是GC?为什么需要GC,
说到这个问题之前,需要了解的是相对底层语言:C++、C都是可以直接和操作系统交互的,操作系统将自己的接口都暴露给这些底层语言,我们知道内存条,1G的内存能存下1G的数据,但是我们知道内存条的单元都是一个个连续性的字节,我们如果要存放一个变量A,那么要将变量A的值存到内存条0x01,那么后面我们读取这个值也就需要从0x01这个地址中读取,也就是需要程序员记住这个地址,如果是你,你肯定在想我应该只要记住A变量就好了呀,print(A)就应该打印值出来才对,这就是你的不对了。

那么这个问题和GC存在什么关系呢,高级语言不需要你关心底层内存排放,语言级别帮你做了隔离,但是呢,内存条始终有大小限制,所以当内存快满了,或者说需要新分配内存,但是现在内存不足了,是不是可以考虑删除一些不要的内存,然后给新来的分配内存呢。这个清扫过程就是:GC,当然这个GC仅仅是在高级语言(面向业务)中才有,像在C和c++中需要程序员手动释放堆上垃圾,栈上的是根据函数释放才会释放

在c语言中和c++中,我们分配内存,如果说将局部变量的指针返回给外部,那么就会出现Dangling pointer,悬挂指针问题,但是如果忘记内存销毁,又容易出现内存泄露,所以用这两种语言来进行编写代码给程序员的心智带来极大的负担,所以才会出现高级的语言。

值得注意的一点是:在golang的内存管理板块,堆和栈空间的区分并没有那么明显,栈空间需要增长和分配,也会在内存管理单元heapArena(听起来是堆内存)分配,所以在go语言上说栈和堆仅仅是两种不太一样的内存分配形式而已

GC发展过程

GC开始的本质是:我们需要通过栈扫描根节点,什么是栈扫描根节点呢,我可不可以是堆扫描根节点呢,这个答案是都可以,主要看变量是否存在逃逸。本质就是扫描内存根节点,比如我一个main函数调用b函数,b函数有一个变量a,此时a就可以看出根节点,但是a具体分配到堆还是栈上可以通过go语言的build 加特定参数可以查看

扫描根节点的目的是追踪链路上可达的对象,那么此时如果我再将这些可达对象标记一下是不是没有被标记的节点就是不可达对象,那么是不是就可以进行删除了

mark-sweep:标记清除

其实这个阶段是很好理解的,其实上面的说法已经就是标记清除算法,根节点扫描后进行标记清除,此时这个过程中就会出现一个问题?程序运行是动态的,你在标记的时候,我在创建或者删除,这个问题怎么破。

STW,stop the word ,这个问题本身就是并发问题,在多核心计算机中大多数都能遇见这样的一些问题,所以我们在这个问题上其实也就相对比较有经验,直接粗暴的停止程序运行,这就是stw的作用,但是这样做,程序必然就会卡顿,这个解决方案也是经常被人诟病。

引用计数

这个垃圾解决方案就是给每个对象标记一个计数器,当引用技术为0的对象就被干掉,如果被引用的话技术器加1,但是这里就会出现一个问题:A引用B,同时B引用A,比如写一个链表的时候一般,节点会存放list的指针,同时list也会存放头尾节点指针,此时,AB永远都回收不了

在更新引用计数的时候,对cpu的压力还是很大的,

三色标记

其实我们需要要记住,所有的垃圾回收的最终目标就是:缩短或者干掉STW,所以我们在研究垃圾回收的时候我们需要尽可能的去往这个方面去思考,然后进入我们接下来的三色标记,先不要问原因,看完后再回过头来品位这句话

白色节点:白色节点代表需要被销毁的节点,在节点初期都是白色节点,然后将白色节点置灰
灰色节点: 当扫描进行到当前白色节点,将白色节点置灰,白色节点上游节点置黑
黑色节点:代表已经被标记的节点,代表节点干净

  1. 最终节点状态只有:白色、灰色
  2. 扫描结束条件是for (gray == nil)
  3. 扫描节点是进行广度优先扫描
三色不变式

看到这里你要想一下,就算是三色标记回收也没有解决STW这个问题。但是不要着急
首先看看三色标记过程可能会出现的情况:

  • . 在标记过程中黑色节点如果创建新的节点,就被扫描不到,就容易被当做垃圾干掉
  • . 如果在灰色节点扫描过程中,程序干掉了下游的白色节点,此时下游节点又被黑色节点引用就容易出现这个白色节点被干掉,但是这个节点又是重要节点

所以问题大概是上面两个问题解决方案就是:

  • 强三色不变式: 黑色节点不能引用白色节点,应对问题1
  • 弱三色不变式: 黑色节点可以引用白色节点,但是白色节点上游必须要有可达的灰色节点,应对问题2
三色不变式的实现: 插入、删除写屏障
  1. 插入写屏障,当黑色节点创建的节点的时候,将创建的节点置为灰色满足强三色不变式的问题
  2. 删除写屏障,当被删除对象为灰色或者白色都将标记为灰色
三色不变式子的弊端:

第一个问题: 由于屏障机制类似给程序加了一个中间件,必然带来额外的开销,那么面对堆内存还好,但是在栈上的内存释放添加如果加入屏障,那么就会出现一个很大的弊端,就是性能带来的开销,在栈上的方法调用是非常频繁的,但是又不一定出现新的内存分配,如果说在栈上进行屏障机制,创建对象的代价就变得不可接收,所以栈上就没有使用屏障。但是栈内容不得不扫描,所以要启动stw,时间大概是在10~100ms左右

第二个问题:删除写屏障,可以直观的看到,如果说我们在执行的删除写屏障,后续白色节点不管是不是垃圾,都能再存活一个GC时间,这个精度就不能保障了

混合写屏障机制 + 三色标记:
  • GC期间栈上创建所有对象都是黑色(这个就不是屏障功能了)
  • GC期间堆上创建、删除对象都是灰色

注意:GC期间有限扫描标记栈上节点

关于STW的一些问题

为什么还需要stw?
目前golang还是需要stw,但是整体功能从标记到清除其实已经是不同的功能属性了,现在stw的作用是通知所有的P去告知执行的M目前状态进去GC,第二个功能就是开启混合写屏障以及内部的必须的内容,比如runtime的锁的状态,goroutine此时挂起是否存在危险

浅谈GC

go 语言的GC目前大体的核心标记清除就是这样的,其实关于混合写屏障+ 三色标记我还是有点问题的,其实上面的内容大致来源我会放在底部,在整个过程中栈扫描期间其实并没有完成弱三色不定式。其实这里我大概可以猜测一下栈扫描还是在stw下进行的,只是后面满足新创建节点是黑色而已。

A->B->C->D
1->2->3

比如说A,1都是栈上对象,此时A链扫描到A,1链扫描到3,此时B删除了CD,那么如果没有弱三色不变式的屏障功能就会出现一个问题: 此时对象1 引用C对象。那么就会出现一个问题,CD是白色被黑色引用。其实这个应该是这么进行处理的:stw过程应该是扫描栈节点 + 准备写屏障,但是我目前还没看源码,有时间我会去了解一下的。