2019独角兽企业重金招聘Python工程师标准>>>
go语言的GC
使用的内存回收机制
go语言垃圾回收总体采用的是经典的mark and sweep(标记-清除)算法。
该算法法分为两步:
- 标记从根变量开始迭代得遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;
- 清除是在标记完成后进行的操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。
这种方法解决了引用计数的不足,但是也有比较明显的问题:
每次启动垃圾回收都会暂停当前所有的正常代码执行,回收是系统响应能力大大降低!当然后续也出现了很多mark&sweep算法的变种(如 三色标记法 )优化了这个问题。
如何工作?
代码存在于栈中,对象分配在堆中,当程序运行到一定时间的时候:
- 标记(mark phase):gc会暂停正在运行的程序(提高gc的优先级,抢占cpu,1.5之后是并行了),这时候,gc对所有已分配对象进行遍历,并标记处在栈中已经被引用的对象。
- 回收:扫描完这些对象之后,将没有被引用(reference)的对象进行回收(释放内存),并清理gc自己的对象库。
和JAVA之类的语言比起来,golang 中的垃圾回收模型还是相对简单的。
版本更迭
-
GO1.5引入并发GC后,runtime会对一个goroutine在上次扫描过stack后是否执行过,进行了跟踪。STW阶段会检查每个goroutine是否执行过,然后会重新扫描那些执行过的。在GO1.7开始,runtime会维护一个独立的短list,这样就不需要在STW期间再遍历一次所有的goroutine,同时极大的减少了那些会触发kernel的NUMA迁移的内存访问。
-
1.7中,amd64的编译器会默认维护frame pointers,这样标准的debug和性能测试工具,例如perf,就可以debug当前的Go函数调用堆栈 了。
-
1.8中,由于消除了GC的“stop-the-world stack re-scanning”,使得GC STW(stop-the-world)的时间通常低于100微秒,甚至经常低于10微秒。当然这或多或少是以牺牲“吞吐”作为代价的。
-
1.9中,用于触发垃圾收集的库函数现在可触发并发垃圾收集,并在吞吐和低延迟上做了一个的平衡。
具体来说,runtime.GC,debug.SetGCPercent和debug.FreeOSMemory,可触发并发垃圾回收,阻止调用goroutine,直到垃圾收集完成。
此外,如果由于新的GOGC值的需要,debug.SetGCPercent函数可以仅触发垃圾回收,这使得可以即时调整GOGC。在使用包含许多对象的大型(> 50GB)堆的应用程序中,对象的分配性能显着提高。runtime.ReadMemStats函数即使对于非常大的堆也少于100μs。
硬件参数调优
涉及算法的问题,总是会有些参数。GO gc参数主要控制的是下一次gc开始的时候的内存使用量。
比如当前的程序使用了4M的对内存(这里说的是堆内存),即是说程序当前reachable的内存为4m,当程序占用的内存达到reachable*(1+GO gc/100)=8M的时候,gc就会被触发,开始进行相关的gc操作。
如何对GO gc的参数进行设置,要根据生产情况中的实际场景来定,比如GO gc参数提升,来减少gc的频率。
代码规范(gopher 大会)
减少对象
减少对象分配:所谓减少对象的分配,实际上是尽量做到,对象的重用。
比如像如下的两个函数定义:
func(r*Reader)Read()([]byte,error)
//此函数没有形参,每次调用的时候返回一个[]byte,第二个函数在每次调用的时候,形参是一个buf []byte 类型的对象,之后返回读入的byte的数目。
func(r*Reader)Read(buf[]byte)(int,error)
//此函数在每次调用的时候都会分配一段空间,这会给gc造成额外的压力。第二个函数在每次迪调用的时候,会重用形参声明。
string与[]byte转化
在stirng与[]byte之间进行转换,会给gc造成压力 通过gdb,可以先对比下两者的数据结构:
type = struct []uint8 { uint8 *array; int len; int cap;}
type = struct string { uint8 *str; int len;}
两者发生转换的时候,底层数据结结构会进行复制,因此导致gc效率会变低。
解决策略:
- 一直使用[]byte,特别是在数据传输方面,[]byte中也包含着许多string会常用到的有效的操作。
- 使用更为底层的操作直接进行转化,避免复制行为的发生(主要是使用unsafe.Pointer直接进行转化。参考资料-雨痕血糖)。
字符串拼接
遵循策略:
尽量减少使用+对字符串进行拼接,由于采用+来进行string的连接会生成新的对象,降低gc的效率,好的方式是通过append函数来进行。
但是要注意如下问题:
b := make([]int, 1024)
b = append(b, 99)
fmt.Println("len:", len(b), "cap:", cap(b))
在使用了append操作之后,数组的空间由1024增长到了1312。
所以如果能提前知道数组的长度的话,最好在最初分配空间的时候就做好空间规划操作,会增加一些代码管理的成本,同时也会降低gc的压力,提升代码的效率。
总结
作为一个code来说,要重视自己的代码性能,减少内存分配和提高对象重用尤为重要。
特别是在摩尔定律即将失效的年代,性能又开始重新被提上的议程。
ps:
摩尔定律是由英特尔(Intel)创始人之一戈登·摩尔(Gordon Moore)提出来的。其内容为:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。换言之,每一美元所能买到的电脑性能,将每隔18-24个月翻一倍以上。这一定律揭示了信息技术进步的速度。