教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 Go GC垃圾回收机制

Go GC垃圾回收机制

发布时间:2022-02-08   编辑:jiaochengji.com
教程集为您提供Go GC垃圾回收机制等资源,欢迎您收藏本站,我们将为您提供最新的Go GC垃圾回收机制资源
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"/></svg><h2>1、垃圾回收</h2>

曾几何时,<code>内存管理</code>是程序员开发应用的一大难题。传统的系统级编程语言(主要指C/C )中,程序员必须对内存小心的进行管理操作,控制内存的申请及释放。稍有不慎,就可能产生<code>内存泄露</code>问题,这种问题不易发现并且难以定位,一直成为困扰开发者的噩梦。如何解决这个头疼的问题呢?过去一般采用两种办法:

(1)<code>内存泄露检测工具</code>。这种工具的原理一般是<code>静态代码扫描</code>,通过扫描程序检测可能出现内存泄露的代码段。然而检测工具难免有疏漏和不足,只能起到<code>辅助作用</code>。
(2)<code>智能指针</code>。这是 c 中引入的自动内存管理方法,通过拥有自动内存管理功能的指针对象来引用对象,是程序员不用太关注内存的释放,而达到内存自动释放的目的。这种方法是采用最广泛的做法,但是对程序员有一定的学习成本(并非语言层面的原生支持),而且一旦有忘记使用的场景依然无法避免内存泄露。
为了解决这个问题,后来开发出来的几乎所有新语言(java,python,php等等)都引入了语言层面的<code>自动内存管理</code> – 也就是语言的使用者只用关注内存的申请而不必关心内存的释放,内存释放由虚拟机(virtual machine)或运行时(runtime)来<code>自动进行管理</code>。而这种对<code>不再使用的内存资源</code>进行<code>自动回收</code>的行为就被称为<code>垃圾回收</code>。

<h2>
2、常见的垃圾回收方法</h2>

<h3>
2.1 引用计数(reference counting)</h3>

这是最简单的一种垃圾回收算法,和之前提到的智能指针异曲同工。对每个对象维护一个<code>引用计数</code>,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被创建或被赋值给其他对象时引用计数自动加一。当引用计数为0时则立即回收对象。

这种方法的优点是<code>实现简单</code>,并且<code>内存的回收很及时</code>。这种算法在内存比较紧张和实时性比较高的系统中使用的比较广泛,如ios cocoa框架,php,python等。简单引用计数算法也有明显的缺点:

(1)<code>频繁更新引用计数降低了性能。</code>一种简单的解决方法就是编译器将相邻的引用计数更新操作合并到一次更新;还有一种方法是针对频繁发生的临时变量引用不进行计数,而是在引用达到0时通过扫描堆栈确认是否还有临时对象引用而决定是否释放。等等还有很多其他方法,具体可以参考这里。
(2)<code>循环引用问题。</code>当对象间发生循环引用时引用链中的对象都无法得到释放。最明显的解决办法是避免产生循环引用,如cocoa引入了strong指针和weak指针两种指针类型。或者系统检测循环引用并主动打破循环链。当然这也增加了垃圾回收的复杂度。

<h3>
2.2 标记-清除(mark and sweep)</h3>

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

<h3>
2.3 分代收集(generation)</h3>

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

<h2>
3、实际经验</h2> <h3>3.1 go程序内存占用大的问题</h3>

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

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

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

<h3>
3.2 gc时间长的问题</h3>

所有的垃圾回收型语言都难免在gc时面临<code>性能下降</code>,对此我们对于应该尽量<code>避免频繁创建临时堆对象</code>(如&abc{}, new, make等)以<code>减少垃圾收集时的扫描时间</code>,对于需要频繁使用的临时对象考虑直接通过数组缓存进行重用;很多人采用cgo的方法自己管理内存而绕开垃圾收集,这种方法除非迫不得已个人是不推荐的(容易造成不可预知的问题),当然迫不得已的情况下还是可以考虑的,这招带来的效果还是很明显的~‘

<h3>
3.3 goroutine泄露的问题</h3>

当连接被远端关闭后,如果不对这两个协程做处理,他们依然会一直运行,并且占用的channel也不会被释放…这里就必须十分注意,在不使用协程后一定要把他依赖的channel close并通过再协程中判断<code>channel是否关闭以保证其退出</code>。

<h2>
4、Go 的GC</h2> <h3>4.1 触发GC机制</h3> <ol><li>

在申请内存的时候,检查当前当前已<code>分配的内存</code>是否<code>大于上次GC后的内存的</code>2倍`,若是则触发(主GC线程为当前M)

</li><li>

<code>监控线程</code>发现上次GC的时间已经<code>超过两分钟</code>了,触发;将一个G任务放到全局G队列中去。(主GC线程为执行这个G任务的M)

</li></ol><h3>
4.2 Go GC执行过程(1.4版本)</h3>

⚠️注:为什么要stop the world呢?

<pre><code class="lang-go hljs">(<span class="token number">1</span>)给所有的对象都做一个初始标记。 当程序运行时,将运行的程序做另一种标记。 方便清除。(个人理解) (<span class="token number">2</span>)所有的线程全部冻结意味着用户逻辑是暂停的。 这样所有的对象都不会被修改了,这时候去扫描是绝对安全的。 </code></pre>

每当触发的时候,在主GC线程中就会走如下的GC流程:

<ol><li>

stop the world,等待所有的M休眠;此时所有的业务逻辑代码都停止

</li><li>

标记:分配gc标记任务,唤醒 gcproc个 M(就是第一步休眠的那些),分别做这个,直到所有的M都做完,才结束;并且所有M再次进入休眠

</li><li>

清理:有一个单独的goroutine去清理已经标记的内存对象快

</li><li>

start the world,设置gcwaiting=0,唤醒所有的M(不会超过P个数)

</li></ol><h3>
4.3 三色标记法(>=1.5版本)</h3>

我们先来看看Golang的三色标记法的大体流程。

首先:程序创建的对象都标记为白色。

gc开始:扫描所有可到达的对象,标记为灰色

从灰色对象中找到其引用对象标记为灰色,把灰色对象本身标记为黑色

监视对象中的内存修改,并持续上一步的操作,直到灰色标记的对象不存在

此时,gc回收白色对象。

最后,将所有黑色对象变为白色,并重复以上所有过程。

标记-清除(mark and sweep)算法的STW(stop the world)操作,就是runtime把所有的线程全部冻结掉,所有的线程全部冻结意味着<code>用户逻辑是暂停的</code>。这样所有的对象都不会被<code>修改</code>了,这时候去<code>扫描</code>是<code>绝对安全</code>的。

Go如何减短这个过程呢?标记-清除(mark and sweep)算法包含两部分逻辑:<code>标记</code>和<code>清除</code>。

我们知道Golang三色标记法中最后只剩下的黑白两种对象,黑色对象是程序恢复后接着使用的对象,如果不碰触黑色对象,只清除白色的对象,肯定不会影响程序逻辑。所以:<code>清除操作和用户逻辑可以并发</code>。

<h3>
process新生成对象的时候,GC该如何操作呢?不会乱吗?</h3>

我们看如下图,在此状态下:process程序又新生成了一个对象,我们设想会变成这样:

但是这样显然是不对的,因为按照三色标记法的步骤,这样新生成的对象A最后会被清除掉,这样会影响程序逻辑。

Golang为了解决这个问题,引入了<code>写屏障</code>这个机制。

写屏障:该屏障之前的写操作和之后的写操作相比,先被系统其它组件感知。

通俗的讲:就是在gc跑的过程中,可以<code>监控</code>对象的<code>内存修改</code>,并对对象进行<code>重新标记</code>。(实际上也是超短暂的stw,然后对对象进行标记)

在上述情况中,新生成的对象,一律都标位灰色!

即下图:

那么,灰色或者黑色对象的引用改为白色对象的时候,Golang是该如何操作的?
看如下图,一个黑色对象引用了曾经标记的白色对象。

这时候,<code>写屏障机制被触发</code>,向<code>GC发送信号</code>,<code>GC重新扫描对象并标位灰色</code>。

因此,gc一旦开始,无论是创建对象还是对象的引用改变,都会先变为<code>灰色</code>。

学习链接:
https://blog.csdn.net/u010649766/article/details/80582153
https://blog.csdn.net/liangzhiyang/article/details/52670021
https://www.jianshu.com/p/8e0a3709b2ee

到此这篇关于“Go GC垃圾回收机制”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Golang理解-垃圾回收机制
Golang垃圾回收机制
golang垃圾回收
大家一起学Golang——轻描淡写GC垃圾回收
php垃圾回收机制处理办法
go 垃圾回收:三色算法
Go 语言的垃圾回收算法被吹过头?与Java比如何?
Go GC垃圾回收机制
golang编程技巧:利用GC机制优雅地关闭协程,避免内存泄漏
golang runtime 简析

上一篇:golang内存对齐原理 下一篇:golang斗地主
[关闭]
~ ~