golang垃圾回收机制

最近在准备面试,整理了一下golang 中的 gc 机制,基本上是标记清除的思路:
在内存堆中(由于有的时候管理内存页的时候要用到堆的数据结构,所以称为堆内存)存储着有一系列的对象,这些对象可能会与其他对象有关联(references between these objects) a tracing garbage collector 会在某一个时间点上停止原本正在运行的程序,之后它会扫描 runtim e已经知道的的 object 集合(already known set of objects),通常它们是存在于 stack 中的全局变量以及各种对象。gc 会对这些对象进行标记,将这些对象的状态标记为可达,从中找出所有的,从当前的这些对象可以达到其他地方的对象的 reference,并且将这些对象也标记为可达的对象,这个步骤被称为 mark phase,即标记阶段,这一步的主要目的是用于获取这些对象的状态信息。

一旦将所有的这些对象都扫描完,gc 就会获取到所有的无法 reach 的对象(状态为 unreachable 的对象),并且将它们回收,这一步称为 sweep phase,即是清扫阶段。

gc 仅仅搜集那些未被标记为可达(reachable)的对象。如果 gc 没有识别出一个 reference,最后有可能会将一个仍然在使用的对象给回收掉,就引起了程序运行错误。

可以看到主要的三个步骤:扫描,回收,清扫。

mark and sweep算法

标记-清除(mark and sweep)
该方法分为两步,标记从根变量开始迭代得遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;标记完成后进行清除操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。这种方法解决了引用计数的不足,但是也有比较明显的问题:每次启动垃圾回收都会暂停当前所有的正常代码执行,回收是系统响应能力大大降低!当然后续也出现了很多mark&sweep算法的变种(如三色标记法)优化了这个问题。

分代收集(generation)
经过大量实际观察得知,在面向对象编程语言中,绝大多数对象的生命周期都非常短。分代收集的基本思想是,将堆划分为两个或多个称为 代(generation)的空间。新创建的对象存放在称为 新生代(young generation)中(一般来说,新生代的大小会比 老年代小很多),随着垃圾回收的重复执行,生命周期较长的对象会被 提升(promotion)到老年代中。因此,新生代垃圾回收和老年代垃圾回收两种不同的垃圾回收方式应运而生,分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度非常快,比老年代快几个数量级,即使新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是因为大多数对象的生命周期都很短,根本无需提升到老年代。

go 1.5正在实现的垃圾回收器是“非分代的、非移动的、并发的、三色的标记清除垃圾收集器”

触发GC机制

  1. 在申请内存的时候,检查当前当前已分配的内存是否大于上次GC后的内存的2倍,若是则触发(主GC线程为当前M)
  2. 监控线程发现上次GC的时间已经超过两分钟了,触发;将一个G任务放到全局G队列中去。(主GC线程为执行这个G任务的M)

1.4版本的,GC过程在标记过程是(STW):
每当触发的时候,在主GC线程中就会走如下的GC流程:

  1. stop the world,等待所有的M休眠;此时所有的业务逻辑代码都停止
  2. 标记:分配gc标记任务,唤醒 gcproc个 M(就是第一步休眠的那些),分别做这个,直到所有的M都做完,才结束;并且所有M再次进入休眠
  3. 清理:有一个单独的goroutine去清理已经标记的内存对象快
  4. start the world,设置gcwaiting=0,唤醒所有的M(不会超过P个数)

在1.5版本里面对GC做了很大的优化;采用三色标记,将标记过程细化成三段,只有前后的两段是stw的;极大地缩短了gc的stw时间

三色标记算法

三色标记算法是对标记阶段的改进,原理如下:
1、起初所有对象都是白色。
2、从根出发扫描所有可达对象,标记为灰色,放入待处理队列。
3、从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色,并放入黑色集合中。
重复 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。
4、 re-scan 全局指针和栈。因为 mark 和用户程序是并行的,所以在过程 1 的时候可能会有新的对象分配,这个时候就需要通过写屏障(write barrier)记录下来。re-scan 再完成检查一下

三色标记的一个明显好处是能够让用户程序和 mark 并发的进行

STW(Stop The World)有两个过程。

  1. 第一个是 GC 将要开始的时候,这个时候主要是一些准备工作,比如 enable write barrier。
  2. 第二个过程就是上面提到的 re-scan 过程。如果这个时候没有 stw,那么 mark 将无休止。

go程序内存占用大的问题
对后台服务进行压力测试时发现,我们模拟大量的用户请求访问后台服务,这时各服务模块能观察到明显的内存占用上升。但是当停止压测时,内存占用并未发生明显的下降。花了很长时间定位问题,使用gprof等各种方法,依然没有发现原因。最后发现原来这时正常的…主要的原因有两个,

一是go的垃圾回收有个触发阈值,这个阈值会随着每次内存使用变大而逐渐增大(如初始阈值是10MB则下一次就是20MB,再下一次就成为了40MB…),如果长时间没有触发gc go会主动触发一次(2min)。高峰时内存使用量上去后,除非持续申请内存,靠阈值触发gc已经基本不可能,而是要等最多2min主动gc开始才能触发gc。

第二个原因是go语言在向系统交还内存时只是告诉系统这些内存不需要使用了,可以回收;同时操作系统会采取“拖延症”策略,并不是立即回收,而是等到系统内存紧张时才会开始回收这样该程序又重新申请内存时就可以获得极快的分配速度。

内存被提前回收的问题:
在项目中,如果一个对象采用堆内存,被存储在一个全局map中,它的成员对象也是堆内存,成员对象同时存储在其它map对象中,当对象从map中删除时,

goroutine泄露的问题
我们的一个服务需要处理很多长连接请求,实现时,对于每个长连接请求各开了一个读取和写入协程,全部采用endless for loop不停地处理收发数据。当连接被远端关闭后,如果不对这两个协程做处理,他们依然会一直运行,并且占用的channel也不会被释放…这里就必须十分注意,在不使用协程后一定要把他依赖的channel close并通过再协程中判断channel是否关闭以保证其退出。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值