这一部分主要介绍golang gc的一些入门的相关知识,因为gc内容涉及比较多,一点一点慢慢整理。
Golang GC的背景
- golang是基于garbage collection的语言,这是它的设计原则。
- 做为一个有垃圾回收器的语言,gc与程序交互时候的效率会影响到整个程序的运行效率。
- 一般程序自己的内存管理会影响gc和程序之间的效率,甚至形成性能瓶颈。
Golang GC的相关问题
主要参的这个:
http://morsmachine.dk/machine-gc
是14年写的,估计那个时候的gc机制还比较simple,新版本的golang对gc的改动应该会比较大
还有那个go语言读书笔记中关于golang gc 的相关部分
关于内存泄露
“内存泄露”(Memory Leak)这个词看似本身很熟悉,可实际上却也从没有看过它的准确含义。
内存泄露,是从操做系统的角度上来阐述的,形象的比喻就是“操做系统可提供给全部进程的存储空间(虚拟内存空间)正在被某个进程榨干”,致使的缘由就是程序在运行的时候,会不断地动态开辟的存储空间,这些存储空间在在运行结束以后后并无被及时释放掉。应用程序在分配了某段内存以后,因为设计的错误,会致使程序失去了对该段内存的控制,形成了内存空间的浪费。
若是程序在内存空间内申请了一块内存,以后程序运行结束以后,没有把这块内存空间释放掉,并且对应的程序又没有很好的gc机制去对程序申请的空间进行回收,这样就会致使内存泄露。
从用户的角度来讲,内存泄露自己不会有什么危害,由于这不是对用户功能的影响,可是“内存泄露”若是进
对于C和C++这种没有Garbage Collection 的语言来说,咱们主要关注两种类型的内存泄漏:
-
堆内存泄漏(Heap leak)。对内存指的是程序运行中根据须要分配经过malloc,realloc new等从堆中分配的一块内存,再是完成后必须经过调用对应的 free或者delete 删掉。若是程序的设计的错误致使这部份内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
-
系统资源泄露(Resource Leak).主要指程序使用系统分配的资源好比 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,致使系统资源的浪费,严重可致使系统效能下降,系统运行不稳定。
内存泄露涉及到的相关问题还有不少,这里暂不展开讨论。
常见的GC模式
具体的优缺点能够参考这个,这里只是进行大体介绍。
-
引用计数(reference counting)每一个对象维护一个引用计数器,当引用该对象的对象被销毁或者更新的时候,被引用对象的引用计数器自动减1,当被应用的对象被建立,或者赋值给其余对象时,引用+1,引用为0的时候回收,思路简单,可是频繁更新引用计数器下降性能,存在循环以引用(php,Python所使用的)
-
标记清除(mark and sweep)就是golang所使用的,从根变量来时遍历全部被引用对象,标记以后进行清除操做,对未标记对象进行回收,缺点:每次垃圾回收的时候都会暂停全部的正常运行的代码,系统的响应能力会大大下降,各类mark&swamp变种(三色标记法),缓解性能问题。
-
分代搜集(generation)jvm就使用的分代回收的思路。在面向对象编程语言中,绝大多数对象的生命周期都很是短。分代收集的基本思想是,将堆划分为两个或多个称为代(generation)的空间。新建立的对象存放在称为新生代(young generation)中(通常来讲,新生代的大小会比 老年代小不少),随着垃圾回收的重复执行,生命周期较长的对象会被提高(promotion)到老年代中(这里用到了一个分类的思路,这个是也是科学思考的一个基本思路)。
所以,新生代垃圾回收和老年代垃圾回收两种不一样的垃圾回收方式应运而生(先分类,以后再对症下药),分别用于对各自空间中的对象执行垃圾回收。新生代垃圾回收的速度很是快,比老年代快几个数量级,即便新生代垃圾回收的频率更高,执行效率也仍然比老年代垃圾回收强,这是由于大多数对象的生命周期都很短,根本无需提高到老年代。
golang中的gc一般是如何工做的
golang中的gc基本上是标记清除的思路:
在内存堆中(因为有的时候管理内存页的时候要用到堆的数据结构,因此称为堆内存)存储着有一系列的对象,这些对象可能会与其余对象有关联(references between these objects) a tracing garbage collector 会在某一个时间点上中止本来正在运行的程序,以后它会扫描runtime已经知道的的object集合(already known set of objects),一般它们是存在于stack中的全局变量以及各类对象。gc会对这些对象进行标记,将这些对象的状态标记为可达,从中找出全部的,从当前的这些对象能够达到其余地方的对象的reference,而且将这些对象也标记为可达的对象,这个步骤被称为mark phase,即标记阶段,这一步的主要目的是用于获取这些对象的状态信息。
一旦将全部的这些对象都扫描完,gc就会获取到全部的没法reach的对象(状态为unreachable的对象),而且将它们回收,这一步称为sweep phase,便是清扫阶段。
gc仅仅搜集那些未被标记为可达(reachable)的对象。若是gc没有识别出一个reference,最后有可能会将一个仍然在使用的对象给回收掉,就引发了程序运行错误。
能够看到主要的三个步骤:扫描,回收,清扫。
感受比起其余的语言,golang中的垃圾回收模型仍是相对简单的。
gc中的问题
gc的引入能够说就是为了解决内存回收的问题。新开发的语言(java,python,php等等),在使用的时候,可使用户没必要关心内存对象的释放,只须要关心对象的申请便可,经过在runtime或者在vm中进行相关的操做,达到自动管理内存空间的效果,这种对再也不使用的内存资源进行自动回收的行为就被称为垃圾回收。
根据前面的表述,可否正常识别一个reference是gc可以正常工做的基础,所以第一个问题就是gc应该如何识别一个reference?
最大的问题:对于reference的识别比较难,machine code 很难知道,怎样才算是一个reference。若是错漏掉了一个reference,就会使得,本来没有准备好要被free掉的内存如今被错误地free掉,因此策略就是宁多勿少。
一种策略是把全部的memory空间都看作是有可能的references(指针值)。这种被称为保守型垃圾回收器(conservative garbage collector)。C 中的Boehm garbage collector就是这样工做的。就是说把内存中的普通变量也当作指针同样去处理,尽可能cover到全部的指针的状况,若是碰巧这个普通的变量值所指向的空间有其余的对象,那么这个对象是不会被回收的。而go语言实现是彻底知道对象的类型信息,在标记时只会遍历指针指向的对象,这样就避免了C实现时的堆内存浪费(解决约10-30%)。
三色标记
2014/6 1.3 引入并发清理(垃圾回收和用户逻辑并发执行?)
2015/8 1.5 引入三色标记法
关于并发清理的引入,参照的是这里在1.3版本中,go runtime分离了mark和sweep的操做,和之前同样,也是先暂停全部任务执行并启动mark(mark这部分仍是要把原程序停下来的),mark完成后就立刻就从新启动被暂停的任务了,而且让sweep任务和普通协程任务同样并行,和其余任务一块儿执行。若是运行在多核处理器上,go会试图将gc任务放到单独的核心上运行而尽可能不影响业务代码的执行,go team本身的说法是减小了50%-70%的暂停时间。
基本算法就是以前提到的清扫+回收,Golang gc优化的核心就是尽可能使得STW(Stop The World)的时间愈来愈短。
如何测量GC
以前说了那么多,那如何测量gc的之星效率,判断它究竟是否对程序的运行形成了影响呢? 第一种方式是设置godebug的环境变量,具体能够参考这一篇,真的是讲的很好的文章:连接,好比运行GODEBUG=gctrace=1 ./myserver
,若是要想对于输出结果了解,还须要对于gc的原理进行更进一步的深刻分析,这篇文章的好处在于,清晰的之处了golang的gc时间是由哪些因素决定的,所以也能够针对性的采起不一样的方式提高gc的时间:
根据以前的分析也能够知道,golang中的gc是使用标记清楚法,因此gc的总时间为:
Tgc = Tseq + Tmark + Tsweep
(T表示time)
- Tseq表示是中止用户的 goroutine 和作一些准备活动(一般很小)须要的时间
- Tmark 是堆标记时间,标记发生在全部用户 goroutine 中止时,所以能够显著地影响处理的延迟
- Tsweep 是堆清除时间,清除一般与正常的程序运行同时发生,因此对延迟来讲是不太关键的
以后粒度进一步细分,具体的概念仍是有些不太懂:
- 与Tmark相关的:1 垃圾回收过程当中,堆中活动对象的数量,2 带有指针的活动对象占据的内存总量 3 活动对象中的指针数量。
- 与Tsweep相关的:1 堆内存的总量 2 堆中的垃圾总量
如何进行gc调优(gopher大会 Danny)
硬性参数
涉及算法的问题,老是会有些参数。GOGC参数主要控制的是下一次gc开始的时候的内存使用量。
好比当前的程序使用了4M的对内存(这里说的是堆内存),便是说程序当前reachable的内存为4m,当程序占用的内存达到reachable*(1+GOGC/100)=8M的时候,gc就会被触发,开始进行相关的gc操做。
如何对GOGC的参数进行设置,要根据生产状况中的实际场景来定,好比GOGC参数提高,来减小GC的频率。
小tips
想要有深刻的insights,使用gdb时必不可少的了,这篇文章里面整理了一些gdb使用的入门技巧。
减小对象分配 所谓减小对象的分配,其实是尽可能作到,对象的重用。 好比像以下的两个函数定义:
1
2 |
func(r*Reader)Read()([]byte,error) func(r*Reader)Read(buf[]byte)(int,error) |
第一个函数没有形参,每次调用的时候返回一个[]byte,第二个函数在每次调用的时候,形参是一个buf []byte 类型的对象,以后返回读入的byte的数目。
第一个函数在每次调用的时候都会分配一段空间,这会给gc形成额外的压力。第二个函数在每次迪调用的时候,会重用形参声明。
老生常谈 string与[]byte转化 在stirng与[]byte之间进行转换,会给gc形成压力 经过gdb,能够先对比下二者的数据结构:
1
2 3 |
type = struct []uint8 { uint8 *array; int len; int cap;} type = struct string { uint8 *str; int len;} |
二者发生转换的时候,底层数据结结构会进行复制,所以致使gc效率会变低。解决策略上,一种方式是一直使用[]byte,特别是在数据传输方面,[]byte中也包含着许多string会经常使用到的有效的操做。另外一种是使用更为底层的操做直接进行转化,避免复制行为的发生。能够参考微信“雨痕学堂”中性能优化的第一部分,主要是使用unsafe.Pointer直接进行转化。
对于unsafe的使用,感受能够单独整理一出一篇文章来了,先把相关资料列在这里 http://studygolang.com/articles/685 直观上,能够把unsafe.Pointer理解成c++中的void*,在golang中,至关因而各类类型的指针进行转化的桥梁。
关于uintptr的底层类型是int,它能够装下指针所指的地址的值。它能够和unsafe.Pointer进行相互转化,主要的区别是,uintptr能够参与指针运算,而unsafe.Pointer只能进行指针转化,不能进行指针运算。想要用golang进行指针运算,能够参考这个。具体指针运算的时候,要先转成uintptr的类型,才能进一步计算,好比偏移多少之类的。
少许使用+链接string 因为采用+来进行string的链接会生成新的对象,下降gc的效率,好的方式是经过append函数来进行。
可是还有一个弊端,好比参考以下代码:
1
|
b := make([]int, 1024) b = append(b, 99) fmt.Println("len:", len(b), "cap:", cap(b)) |
在使用了append操做以后,数组的空间由1024增加到了1312,因此若是能提早知道数组的长度的话,最好在最初分配空间的时候就作好空间规划操做,会增长一些代码管理的成本,同时也会下降gc的压力,提高代码的效率。
参考资料
https://talks.golang.org/2015/go-gc.pdf
https://www.zhihu.com/question/21615032
https://blog.golang.org/go15gc
golang gc 中文入门(总结比较全面 包括golang gc 在不一样版本的比较 赞) http://www.open-open.com/lib/view/open1435846881544.html(原文)
其余垃圾回收相关文章
这个介绍的gc较为系统: http://newhtml.net/v8-garbage-collection/
1.5版本的垃圾回收器 http://ruizeng.net/go-15-release-notes/
内存泄露参考 http://blog.csdn.net/na_he/article/details/7429171
Go1.5源码剖析 https://github.com/qyuhen/book
手动管理golang gc的一个例子(比较深层次的内容) http://my.oschina.net/lubia/blog/175154