浅析 Golang 垃圾回收机制

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Google 搜索 "},{"type":"text","marks":[{"type":"strong"}],"text":"Golang GC"},{"type":"text","text":" 排名靠前的文章都讲的不错,从设计到实现,从演进到源码,一应俱全。但是庞杂的信息会给人一种恐惧感,让人望而却步。本文尝试使用较为简单易懂的语言和图像,讲解 Golang 的垃圾回收机制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"垃圾回收算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前比较常见的垃圾回收算法有三种:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"引用计数:为每个对象维护一个引用计数,当引用该对象的对象销毁时,引用计数 -1,当对象引用计数为 0 时回收该对象。"}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代表语言:"},{"type":"text","marks":[{"type":"strong"}],"text":"Python"},{"type":"text","text":"、"},{"type":"text","marks":[{"type":"strong"}],"text":"PHP"},{"type":"text","text":"、"},{"type":"text","marks":[{"type":"strong"}],"text":"Swift"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"优点:对象回收快,不会出现内存耗尽或达到某个阈值时才回收。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺点:不能很好的处理循环引用,而实时维护引用计数也是有损耗的。"}]}]}]},{"type":"numberedlist","attrs":{"start":2,"normalizeStart":2},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"标记-清除:从根变量开始遍历所有引用的对象,标记引用的对象,没有被标记的进行回收。"}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代表语言:"},{"type":"text","marks":[{"type":"strong"}],"text":"Golang"},{"type":"text","text":"(三色标记法)"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"优点:解决了引用计数的缺点。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺点:需要 STW,暂时停掉程序运行。"}]}]}]},{"type":"numberedlist","attrs":{"start":3,"normalizeStart":3},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。"}]}]}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代表语言:"},{"type":"text","marks":[{"type":"strong"}],"text":"Java"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"优点:回收性能好"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":1,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺点:算法复杂"}]}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Golang 垃圾回收"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"跳过原理,我们先来介绍 Golang 的三色标记法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三色标记法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"三色标记法只是为了叙述方便而抽象出来的一种说法,实际上的对象是没有三色之分的。这里的三色,对应了垃圾回收过程中对象的三种状态:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"灰色:对象还在标记队列中等待"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"黑色:对象已被标记,"},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 对应位为 "},{"type":"codeinline","content":[{"type":"text","text":"1"}]},{"type":"text","text":" -- 该对象不会在本次 GC 中被回收"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"白色:对象未被标记,"},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 对应位为 "},{"type":"codeinline","content":[{"type":"text","text":"0"}]},{"type":"text","text":" -- 该对象将会在本次 GC 中被清理"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具体流程如下图:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/46/46a0b312f6e4bb90deea79ace6f945dd.jpeg","alt":null,"title":"三色标记法","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"boxShadow"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"回收原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通过上图,应该对三色标记法有了一个比较直观的了解,那么我们现在来讲讲原理。简单的讲,就是标记内存中那些还在使用中(即被引用了)的部分,而内存中不再使用(即未被引用)的部分,就是要回收的垃圾,需要将其回收,以供后续内存分配使用。上图中的 A、B、D 就是被引用正在使用的内存,而C、F、E 曾经被使用过,但现在没有任何对象引用,就需要被回收掉。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而 Root 区域主要是程序运行到当前时刻的栈和全局数据区域,是实时正在使用到的内存,当然应该优先标记。而考虑到内存块中存放的可能是指针,所以还需要递归的进行标记,待全部标记完后,就会对未被标记的内存进行回收。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"内存标记"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"golang 中采用 span 数据结构管理内存,span 中维护了一个个内存块,并由一个位图 "},{"type":"codeinline","content":[{"type":"text","text":"allocBits"}]},{"type":"text","text":" 表示内存块的分配情况,而上文中提到的 "},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 是记录每块内存块被引用情况的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d1/d1dee05d9d8c2f0ddc777552996d09ce.jpeg","alt":null,"title":"内存标记","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上图,"},{"type":"codeinline","content":[{"type":"text","text":"allocBits"}]},{"type":"text","text":" 记录了每块内存的分配情况,而 "},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 记录了每块内存的标记情况。在标记阶段会对每块内存进行标记,有对象引用的内存标记为 1,没有对象引用的为 0。而 "},{"type":"codeinline","content":[{"type":"text","text":"allocBits"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 的数据结构是完全一样的,在结束标记后,将 "},{"type":"codeinline","content":[{"type":"text","text":"allocBits"}]},{"type":"text","text":" 指向 "},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":",则有标记的才是存活的,这样就完成了内存回收。而 "},{"type":"codeinline","content":[{"type":"text","text":"gcmarkBits"}]},{"type":"text","text":" 则会在下次标记时重新分配内存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"垃圾回收优化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在前文中提到,golang 的垃圾回收算法属于 "},{"type":"text","marks":[{"type":"strong"}],"text":"标记-清除"},{"type":"text","text":",是需要 STW 的。STW 就是 "},{"type":"text","marks":[{"type":"strong"}],"text":"Stop The World"},{"type":"text","text":" 的意思,在 golang 中就是要停掉所有的 goroutine,专心进行垃圾回收,待垃圾回收结束后再恢复 goroutine。而 STW 时间的长短直接影响了应用的执行,如果时间过长,那将是灾难性的。为了缩短 STW 时间,golang 不对优化垃圾回收算法,其中"},{"type":"text","marks":[{"type":"strong"}],"text":"写屏障(Write Barrier)"},{"type":"text","text":"和"},{"type":"text","marks":[{"type":"strong"}],"text":"辅助GC(Mutator Assist)"},{"type":"text","text":"就是两种优化垃圾回收的方法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"写屏障(Write Barrier)"},{"type":"text","text":":上面说到的 STW 的目的是防止 GC 扫描时内存变化引起的混乱,而写屏障就是让 goroutine 与 GC 同时运行的手段,虽然不能完全消除 STW,但是可以大大减少 STW 的时间。写屏障在 GC 的特定时间开启,开启后指针传递时会把指针标记,即本轮不回收,下次 GC 时再确定。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"辅助 GC(Mutator Assist)"},{"type":"text","text":":为了防止内存分配过快,在 GC 执行过程中,GC 过程中 mutator 线程会并发运行,而 mutator assist 机制会协助 GC 做一部分的工作。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"垃圾回收触发机制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"内存分配量达到阈值"},{"type":"text","text":":每次内存分配都会检查当前内存分配量是否达到阈值,如果达到阈值则触发 GC。"},{"type":"codeinline","content":[{"type":"text","text":"阈值 = 上次 GC 内存分配量 * 内存增长率"}]},{"type":"text","text":",内存增长率由环境变量 "},{"type":"codeinline","content":[{"type":"text","text":"GOGC"}]},{"type":"text","text":" 控制,默认为 100,即每当内存扩大一倍时启动 GC。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"定时触发 GC"},{"type":"text","text":":默认情况下,2分钟触发一次 GC,该间隔由 "},{"type":"codeinline","content":[{"type":"text","text":"src/runtime/proc.go"}]},{"type":"text","text":" 中的 "},{"type":"codeinline","content":[{"type":"text","text":"forcegcperiod"}]},{"type":"text","text":" 声明。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"手动触发 GC"},{"type":"text","text":":在代码中,可通过使用 "},{"type":"codeinline","content":[{"type":"text","text":"runtime.GC()"}]},{"type":"text","text":" 手动触发 GC。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"GC 优化建议"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上文可知,GC 性能是与对象数量有关的,对象越多 GC 性能越差,对程序的影响也越大。所以在开发中要尽量减少对象分配个数,采用对象复用、将小对象组合成大对象或采用小数据类型(如使用 "},{"type":"codeinline","content":[{"type":"text","text":"int8"}]},{"type":"text","text":" 代替 "},{"type":"codeinline","content":[{"type":"text","text":"int"}]},{"type":"text","text":")等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"结语"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一门编程语言的垃圾回收机制会直接影响使用其开发应用的性能。在日常开发工作中也因注意到其作用,有助于开发出高性能的应用,这也是 GC 常常在面试中被问到的原因。同时,了解 GC 对了解内存管理也很有帮助。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a4/a45c3a602d914f821ca970e51f9dcd2d.gif","alt":null,"title":"欢迎扫描二维码关注公众号,了解更多云原生知识","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章