golang gc 问题(转的)

 

在实际使用go语言的过程当中,碰到了一些看似奇怪的内存占用现象,因而决定对go语言的垃圾回收模型进行一些研究。本文对研究的结果进行一下总结。javascript

什么是垃圾回收?

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

  • 内存泄露检测工具。这种工具的原理通常是静态代码扫描,经过扫描程序检测可能出现内存泄露的代码段。然而检测工具不免有疏漏和不足,只能起到辅助做用。
  • 智能指针。这是c++中引入的自动内存管理方法,经过拥有自动内存管理功能的指针对象来引用对象,是程序员不用太关注内存的释放,而达到内存自动释放的目的。这种方法是采用最普遍的作法,可是对程序员有必定的学习成本(并不是语言层面的原生支持),并且一旦有忘记使用的场景依然没法避免内存泄露。

为了解决这个问题,后来开发出来的几乎全部新语言(java,python,php等等)都引入了语言层面的自动内存管理 – 也就是语言的使用者只用关注内存的申请而没必要关心内存的释放,内存释放由虚拟机(virtual machine)或运行时(runtime)来自动进行管理。而这种对再也不使用的内存资源进行自动回收的行为就被称为垃圾回收。java

常见的垃圾回收方法

引用计数(reference counting)

这是最简单的一种垃圾回收算法,和以前提到的智能指针殊途同归。对每一个对象维护一个引用计数,当引用该对象的对象被销毁或更新时被引用对象的引用计数自动减一,当被引用对象被建立或被赋值给其余对象时引用计数自动加一。当引用计数为0时则当即回收对象。python

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

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

标记-清除(mark and sweep)

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

分代收集(generation)

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

GO的垃圾回收器

go语言垃圾回收整体采用的是经典的mark and sweep算法。golang

  • 1.3版本之前,golang的垃圾回收算法都很是简陋,而后其性能也广被诟病:go runtime在必定条件下(内存超过阈值或按期如2min),暂停全部任务的执行,进行mark&sweep操做,操做完成后启动全部任务的执行。在内存使用较多的场景下,go程序在进行垃圾回收时会发生很是明显的卡顿现象(Stop The World)。在对响应速度要求较高的后台服务进程中,这种延迟简直是不能忍受的!这个时期国内外不少在生产环境实践go语言的团队都或多或少踩过gc的坑。当时解决这个问题比较经常使用的方法是尽快控制自动分配内存的内存数量以减小gc负荷,同时采用手动管理内存的方法处理须要大量及高频分配内存的场景。
  • 1.3版本开始go team开始对gc性能进行持续的改进和优化,每一个新版本的go发布时gc改进都成为你们备受关注的要点。1.3版本中,go runtime分离了mark和sweep操做,和之前同样,也是先暂停全部任务执行并启动mark,mark完成后立刻就从新启动被暂停的任务了,而是让sweep任务和普通协程任务同样并行的和其余任务一块儿执行。若是运行在多核处理器上,go会试图将gc任务放到单独的核心上运行而尽可能不影响业务代码的执行。go team本身的说法是减小了50%-70%的暂停时间。
  • 1.4版本(当前最新稳定版本)对gc的性能改动并很少。1.4版本中runtime不少代码取代了原生c语言实现而采用了go语言实现,对gc带来的一大改变是能够是实现精确的gc。c语言实如今gc时没法获取到内存的对象信息,所以没法准确区分普通变量和指针,只能将普通变量当作指针,若是碰巧这个普通变量指向的空间有其余对象,那这个对象就不会被回收。而go语言实现是彻底知道对象的类型信息,在标记时只会遍历指针指向的对象,这样就避免了C实现时的堆内存浪费(解决约10-30%)。
  • 1.5版本go team对gc又进行了比较大的改进(1.4中已经埋下伏笔如write barrier的引入),官方的主要目标是减小延迟。go 1.5正在实现的垃圾回收器是“非分代的、非移动的、并发的、三色的标记清除垃圾收集器”。分代算法上文已经说起,是一种比较好的垃圾回收管理策略,然1.5版本中并未考虑实现;我猜想的缘由是步子不能迈太大,得逐步改进,go官方也表示会在1.6版本的gc优化中考虑。同时引入了上文介绍的三色标记法,这种方法的mark操做是能够渐进执行的而不需每次都扫描整个内存空间,能够减小stop the world的时间。 由此能够看到,一路走来直到1.5版本,go的垃圾回收性能也是一直在提高,可是相对成熟的垃圾回收系统(如java jvm和javascript v8),go须要优化的路径还很长(可是相信将来必定是美好的~)。

实践经验

团队在实践go语言时一样碰到最多和最棘手的问题也是内存问题(其中gc为主),这里把遇到的问题和经验总结下,欢迎你们一块儿交流探讨。算法

go程序内存占用大的问题

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

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

第二个缘由是go语言在向系统交还内存时只是告诉系统这些内存不须要使用了,能够回收;同时操做系统会采起“拖延症”策略,并非当即回收,而是等到系统内存紧张时才会开始回收这样该程序又从新申请内存时就能够得到极快的分配速度。

gc时间长的问题

对于对用户响应事件有要求的后端程序,golang gc时的stop the world兼职是噩梦。根据上文的介绍,1.5版本的go再完成上述改进后应该gc性能会提高很多,可是全部的垃圾回收型语言都不免在gc时面临性能降低,对此咱们对于应该尽可能避免频繁建立临时堆对象(如&abc{}, new, make等)以减小垃圾收集时的扫描时间,对于须要频繁使用的临时对象考虑直接经过数组缓存进行重用;不少人采用cgo的方法本身管理内存而绕开垃圾收集,这种方法除非无可奈何我的是不推荐的(容易形成不可预知的问题),固然无可奈何的状况下仍是能够考虑的,这招带来的效果仍是很明显的~

goroutine泄露的问题

咱们的一个服务须要处理不少长链接请求,实现时,对于每一个长链接请求各开了一个读取和写入协程,所有采用endless for loop不停地处理收发数据。当链接被远端关闭后,若是不对这两个协程作处理,他们依然会一直运行,而且占用的channel也不会被释放…这里就必须十分注意,在不使用协程后必定要把他依赖的channel close并经过再协程中判断channel是否关闭以保证其退出。

相关文章
相关标签/搜索