Golang 垃圾回收

一.什么是垃圾回收?

内存泄漏过去一般的发现的方式:

  • 内存泄漏监测工具: 这种工具的原理一般是静态代码的扫描,通过扫描程序检测可能出现内存泄露的代码段,然而检测工具难免有疏漏和不足, 只能起到辅助作用。
  • 智能指针:是C++中引入的自动内存管理方法,通过拥有自动内存管理功能的指针对象来引用对象,是程序员不太关注内存的释放,而达到内存自动释放的目的。这种方法是采用最广泛的做法,但是对于程序员来说有一定的学习成本,而且一旦有忘记使用的场景仍然会有内存泄露。
    为了解决这方面的问题,后来开发出来的几乎所有的语言都有语言层面的自动内存管理-也就是语言的使用者只关注内存的申请而不关注内存的释放,内存释放由虚拟机或者运行时来自动管理。而这种对于不再使用的内存资源进行自动回收的过程就被称为垃圾回收。

二.常见的垃圾回收方法

  • 引用计数
    这是最简单的一种垃圾回收算法,和之前提到的智能指针异曲同工。对每一个对象维护一个引用计数,当引用该对象的对象被销毁或者更新,被引用对象的引用计数自动减1,当被引用对象被创建或者被其他对象引用时引用计数自动增1,当引用计数为0时,立即回收该对象。
    这种方法的优点是实现简单,并且回收的速度很及时。这种算法在内存比较紧张和实时性比较高的系统中应用比较广泛,如ios的cococa框架,php、python等,简单引用计数也有明显的缺陷:

    频繁更新引用计数降低了性能,一种简单的解决方法就是编译器将相邻的引用计数更新操作合并到一次更新,还有一种方法是针对频繁发生的临时变量引用不进行计数,而是在引用达到0时通过扫描堆栈确认是否还有临时对象引用而决定是否释放
    循环引用问题,当对象中发生循环引用时引用链中的对象都无法得到释放,最明显的办法是避免出现循环引用,如cocoa中引入了strong类型和week类型的两种指针类型,或者系统检测循环引用并且打破循环链。当然也增加了垃圾回收的复杂度。

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

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

三.Go的垃圾回收器

GO语言垃圾回收总体使用的是经典的mark和sweep算法

  • 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需要优化的路径还很长(但是相信未来一定是美好的~)。

你可能感兴趣的:(golang,golang,后端)