首页 > golang > 深入理解GC原理
2021
01-01

深入理解GC原理

垃圾定位算法

(1)引用计数法 (Reference Counting)。  如 python  php

      通常C++通过指针引用计数来回收对象,但是这不能处理循环引用,原理是在每个对象内部维护一个引用计数,当对象被引用时引用计数加一,当对象不被引用时引用计数减一。当引用计数为 0 时,自动销毁对象。

      例如:谁想用驴干活的时候,就在驴身上画个圈圈,用一次画一个,用完了把代表本次使用的圈圈擦掉。当这头驴身上没圈圈的时候,就可以卸磨杀驴了,身上有圈圈的驴不能杀。

      这个办法的优点是:用驴的人清楚自己用了哪头驴,一旦用驴的人忘了,当事驴自己也清楚。找不到主人,驴可以自己清除圈圈,洗白白再上路。

      缺点也很明显:画圈圈也是体力活,需要时间;另一方面,驴皮就那么点面积,好几个圈圈画重叠了也是麻烦,再加上有可能圈圈套圈圈,驴也很惆怅啊。

(2)根可达算法
      从GC Roots向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的,向JAVA、Go这种带有GC功能的高级语言使用的都是这种定位算法,简单来讲,从根对象往下查找引用,可以查找到的引用标记成可达,直到算法结束之后,没有被标记的对象就是不可达的,就会被GC回收。

垃圾回收算法

(1)标记-清除 (Mark-Sweep)

        例如:想用某驴干活,就拍张这头驴的照片贴墙上,驴去干活就行了。驴干完活,去墙上把照片摘下来。有其他人用驴,也是拍照片贴墙上,以此类推。当墙上没有某头驴的照片时,这头驴就可以杀了吃肉了。

        这个办法的优点是:驴没啥负担,只管干活;墙可以很宽,贴多少照片问题不大。

        缺点是:找照片是个技术活,太笨的话,光是找照片就得很长时间。而且找照片的时候,最好别贴新的照片,不然就乱了,还得重新找。

(2)复制 (copy and collection)  

(3)标记-压缩
      以上三种算法是传统的垃圾回收算法,第一种容易产生内存碎片,第二种不会生成内存碎片,但是由于是整块复制,所以STW较长,效率太低,第三种是前两种的结合
(4)分代模型

     JVM做垃圾回收时常用的GC算法,分为年轻代和老年代,年轻代使用复制算法,老年代使用标记压缩或者标记清除。
在分代模型中,年轻代的回收算法有ParNew、Serial、Parallel Scavenge,老年代的回收算法有CMS、Serial Old、Parallel Old,年轻代和老年代的回收算法一定是成对出现的,常见的回收对是ParNew-CMS、Serial-Serial Old、Parallel Scavenge-Parallel Old(jdk1.8默认)

     例如:驴多了不能放一起,短工驴放一个地方,长工驴放另一个地方。短工驴总是干活不休息,就升级成长工驴。杀驴的时候,一个人专杀干完短工的驴,短工驴不会摆资格,杀就杀了;另一个人专杀干完长工的驴,长工驴资格老,脾气暴,杀之前要点蚊香放音乐,搞点仪式感。

     这个办法的优点是:非常驴性化,充分照顾了驴的感受;另一方面,多几个人杀驴,效率比较高。

     缺点一样很明显:驴会升级,短工驴升级到长工驴,总是很矫情,两边杀手必须做好交接工作,不然有驴忘了杀就麻烦了。

(5)三色标记法
      三色标记法是传统 Mark-Sweep 的一个改进,它是一个并发的 GC 算法。其实大部分的工作还是在标记垃圾,基本原理基于根可达


Golang 的三色标记法

golang 的垃圾回收(GC)是基于标记清扫算法,因为这种方式关注点很简单,只要找驴照片足够快,其他都好办。

但是因为低版本的Golang,找驴照片的工作不熟练,再加上这种找驴方式本身的特点,就造成了Golang经常被人诟病的两个槽点:

1、Golang的GC速度慢。

这个在各个版本,一直是golang改进的重点。到目前为止(1.14),经过超级多版本的演进,特别是在1.14版本中实现了基于信号的真抢占式调度,目前的gc速度已经相当快了。不能跟纯手动内存管理比,只能说在同样使用gc机制

的编程语言里,达到了应有的水平(之前确实有几个版本,达不到及格线)。

2、Golang的GC的STW(STOP THE WORLD)噩梦

前面提到过,找驴照片的时候,不能有新的照片贴墙上。这就造成了找驴的过程中,整个世界都停止了(其实有点夸张,毕竟只是照片墙不接受新照片,驴其实还在干活或者等死)。这个从根本上说,是机制问题。只要还用这个机制,那世界还是会时间停止。只不过,随着找驴速度的加快,这个时间裂缝,已经压缩到一个非常非常短的瞬间。当然,Golang本身的演进过程中,采用了很多细节技术,不但让这个STW的时间越来越短,而且还尽量的减少了这个停止世界的大小,把影响范围收窄。


一轮完整的 GC,总是从 Off,如果不是 Off 状态,则代表上一轮GC还未完成,如果这时修改指针的值,是直接修改的。

Stack scan: 收集根对象(全局变量和 goroutine 栈上的变量),该阶段会开启写屏障(Write Barrier)。

Mark: 标记对象,直到标记完所有根对象和根对象可达对象。此时写屏障会记录所有指针的更改(通过 mutator)。

Mark Termination: 重新扫描部分全局变量和发生更改的栈变量,完成标记,该阶段会STW(Stop The World),也是 gc 时造成 go 程序停顿的主要阶段。

Sweep: 并发的清除未标记的对象。


通过三色标记清扫法与写屏障来减少 STW 的时间.

三色标记法的流程如下,它将对象通过白、灰、黑进行标记

1.所有对象最开始都是白色.

2.从 root 开始找到所有可达对象,标记为灰色,放入待处理队列。

3.遍历灰色对象队列,将其引用对象标记为灰色放入待处理队列,自身标记为黑色。

4.循环步骤3直到灰色队列为空为止,此时所有引用对象都被标记为黑色,所有不可达的对象依然为白色,白色的就是需要进行回收的对象。

三色标记法相对于普通标记清扫,减少了 STW 时间. 这主要得益于标记过程是 “on-the-fly” 的,在标记过程中是不需要 STW 的,它与程序是并发执行的,这就大大缩短了 STW 的时间.


标记垃圾会产生的问题

A对象已经标记并且引用的对象B也已经被标记,所以A放到黑色集合里,B对象被标记但是C对象还没标记,所以B是灰色

(1)浮动垃圾

如果B到C的引用断开,那么B找不到引用会被标黑,此时C就成了浮动垃圾,这种情况不碍事,大不了下次GC再收集

(2)漏标或者错标或者称作悬挂指针

      但是如果此时用户goroutine新建对象A对对象C的引用,也就是从已经被标记成黑色的对象新建了引用指向了白色对象,因为A已经标黑,此时C将作为白色不可达对象被收集,这就出大问题了,程序里面A对象还正在引用C对象,

但是GC把C对象看成垃圾给回收了,造成空指针异常。


写屏障 (Write Barrier)

为了解决漏标的问题,需要使用写屏障,原理就是当A对象被标黑,此时A又引用C,就把C变灰入队

写屏障一定是在进行内存写操作之前执行的。

强三色不变性:黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象;

所以新生成的对象,一律都标位灰色!

弱三色不变性 :黑色节点允许引用白色节点,但是该白色节点有其他灰色节点间接的引用(确保不会被遗漏)当白色节点被删除了一个引用时,悲观地认为它一定会被一个黑色节点新增引用,所以将它置为灰色


Go 语言中使用两种写屏障技术,分别是 Dijkstra 提出的插入写屏障和 Yuasa 提出的删除写屏障。


GC 触发条件

    主动触发,用户代码中调用 runtime.GC 会主动触发 GC

    默认每 2min 未产生 GC 时,golang 的守护协程 sysmon 会强制触发 GC

    当 go 程序分配的内存增长超过阈值时,会触发 GC


各版本golang如何处理GC: https://phpmianshi.com/?id=5197

关于内存逃逸产生的GC问题可以参考:https://phpmianshi.com/?id=5196 


本文》有 0 条评论

留下一个回复