golang gc优化总结

go语言的GC

使用的内存回收机制

go语言垃圾回收总体采用的是经典的mark and sweep(标记-清除)算法。

该算法法分为两步:

  1. 标记从根变量开始迭代得遍历所有被引用的对象,对能够通过应用遍历访问到的对象都进行标记为“被引用”;
  2. 清除是在标记完成后进行的操作,对没有标记过的内存进行回收(回收同时可能伴有碎片整理操作)。

这种方法解决了引用计数的不足,但是也有比较明显的问题:

每次启动垃圾回收都会暂停当前所有的正常代码执行,回收是系统响应能力大大降低!当然后续也出现了很多mark&sweep算法的变种(如 三色标记法 )优化了这个问题。

如何工作?

代码存在于栈中,对象分配在堆中,当程序运行到一定时间的时候:

  1. 标记(mark phase):gc会暂停正在运行的程序(提高gc的优先级,抢占cpu,1.5之后是并行了),这时候,gc对所有已分配对象进行遍历,并标记处在栈中已经被引用的对象。
  2. 回收:扫描完这些对象之后,将没有被引用(reference)的对象进行回收(释放内存),并清理gc自己的对象库。

和JAVA之类的语言比起来,golang 中的垃圾回收模型还是相对简单的。

我画了个草图描述一下流程:

版本更迭
  1. GO1.5引入并发GC后,runtime会对一个goroutine在上次扫描过stack后是否执行过,进行了跟踪。STW阶段会检查每个goroutine是否执行过,然后会重新扫描那些执行过的。在GO1.7开始,runtime会维护一个独立的短list,这样就不需要在STW期间再遍历一次所有的goroutine,同时极大的减少了那些会触发kernel的NUMA迁移的内存访问。

  2. 1.7中,amd64的编译器会默认维护frame pointers,这样标准的debug和性能测试工具,例如perf,就可以debug当前的Go函数调用堆栈 了。

  3. 1.8中,由于消除了GC的“stop-the-world stack re-scanning”,使得GC STW(stop-the-world)的时间通常低于100微秒,甚至经常低于10微秒。当然这或多或少是以牺牲“吞吐”作为代价的。

  4. 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效率会变低。

解决策略:

  1. 一直使用[]byte,特别是在数据传输方面,[]byte中也包含着许多string会常用到的有效的操作。
  2. 使用更为底层的操作直接进行转化,避免复制行为的发生(主要是使用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个月翻一倍以上。这一定律揭示了信息技术进步的速度。

转载于:https://my.oschina.net/lwl1989/blog/2413699

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值