golang手动管理内存

golang手动管理内存,第1张

概述作者:John Graham-Cumming.   原文点击此处。翻译:Lubia Yang 前些天我介绍了我们对Lua的使用,implement our new Web Application Firewall.   另一种在CloudFlare (作者的公司)变得非常流行的语言是Golang。在过去,我写了一篇 how we use Go来介绍类似Railgun的网络服务的编写。  用Gola

作者:JohnGraham-Cumming.原文点击此处。翻译:Lubia Yang

前些天我介绍了我们对Lua的使用,implementournewWebApplicationFirewall.

另一种在CloudFlare(作者的公司)变得非常流行的语言是Golang。在过去,我写了一篇howweuseGo来介绍类似Railgun的网络服务的编写。

用Golang这样带GC的语言编写长期运行的网络服务有一个很大的挑战,那就是内存管理

为了理解Golang的内存管理有必要对run-time源码进行深挖。有两个进程区分应用程序不再使用的内存,当它们看起来不会再使用,就把它们归还到 *** 作系统(在Golang源码里称为scavenging)。

这里有一个简单的程序制造了大量的垃圾(garbage),每秒钟创建一个5,000,000到10,000bytes的数组。程序维持了20个这样的数组,其他的则被丢弃。程序这样设计是为了模拟一种非常常见的情况:随着时间的推移,程序中的不同部分申请了内存,有一些被保留,但大部分不再重复使用。在Go语言网络编程中,用goroutines来处理网络连接和网络请求时(networkconnectionsorrequests),通常goroutines都会申请一块内存(比如slice来存储收到的数据)然后就不再使用它们了。随着时间的推移,会有大量的内存被网络连接(networkconnections)使用,连接累积的垃圾comeandgone

package mainimport (      "fmt"      "math/rand"      "runtime"      "time")  func makeBuffer() []byte {      return make([]byte,rand.Intn(5000000)+5000000)  }func main() {      pool := make([][]byte,20)    var m runtime.MemStats      makes := 0      for {          b := makeBuffer()        makes += 1        i := rand.Intn(len(pool))        pool[i] = b        time.Sleep(time.Second)        bytes := 0        for i := 0; i < len(pool); i++ {            if pool[i] != nil {                bytes += len(pool[i])            }        }        runtime.ReadMemStats(&m)        fmt.Printf("%d,%d,%d\n",m.HeapSys,bytes,m.HeapAlloc,m.HeAPIdle,m.HeapReleased,makes)    }}
程序使用 runtime.ReadMemStats函数来获取堆的使用信息。它打印了四个值,

HeapSys:程序向应用程序申请的内存

HeapAlloc:堆上目前分配的内存

HeAPIdle:堆上目前没有使用的内存

HeapReleased:回收到 *** 作系统的内存

GC在Golang中运行的很频繁(参见GOGC环境变量(GOGCenvironmentvariable)来理解怎样控制垃圾回收 *** 作),因此在运行中由于一些内存被标记为”未使用“,堆上的内存大小会发生变化:这会导致HeapAlloc和HeAPIdle发生变化。Golang中的scavenger会释放那些超过5分钟仍然没有再使用的内存,因此HeapReleased不会经常变化。

下面这张图是上面的程序运行了10分钟以后的情况:


(在这张和后续的图中,左轴以是以byte为单位的内存大小,右轴是程序执行次数)

红线展示了pool中bytebuffers的数量。20个buffers很快达到150,000bytes。最上方的蓝色线表示程序从 *** 作系统申请的内存。稳定在375,000bytes。因此程序申请了2.5倍它所需的空间!

当GC发生时,HeAPIdle和HeapAlloc发生跳变。橘色的线是makeBuffer()发送的次数。

这种过度的内存申请是有GC的程序的通病,参见这篇paper

QuantifyingthePerformanceofGarbageCollectionvs.ExplicitMemoryManagement

程序不断执行,IDlememory(即HeAPIdle)会被重用,但很少归还到 *** 作系统。


解决此问题的一个办法是在程序中手动进行内存管理。例如,

程序可以这样重写:

package mainimport (	"fmt"	"math/rand"	"runtime"	"time")func makeBuffer() []byte {	return make([]byte,rand.Intn(5000000)+5000000)}func main() {	pool := make([][]byte,20)	buffer := make(chan []byte,5)	var m runtime.MemStats	makes := 0	for {		var b []byte		select {		case b = <-buffer:		default:			makes += 1			b = makeBuffer()		}		i := rand.Intn(len(pool))		if pool[i] != nil {			select {			case buffer <- pool[i]:				pool[i] = nil			default:			}		}		pool[i] = b		time.Sleep(time.Second)		bytes := 0		for i := 0; i < len(pool); i++ {			if pool[i] != nil {				bytes += len(pool[i])			}		}		runtime.ReadMemStats(&m)		fmt.Printf("%d,makes)	}}


这张图展示了完全不同的情况。实际使用的buffer几乎等于从 *** 作系统中申请的内存。同时GC几乎没有工作可做。堆上只有很少的HeAPIdle最终需要归还到 *** 作系统。

这段程序中内存回收机制的关键 *** 作就是一个缓冲的channel——buffer,在上面的代码中,buffer是一个可以存储5个[]byteslice的容器。当程序需要空间时,首先会使用select从buffer中读取:

select{

caseb=<-buffer:

default:

makes+=1

b=makeBuffer()

}

这永远不会阻塞因为如果channel中有数据,就会被读出,如果channel是空的(意味着接收会阻塞),则会创建一个。

使用类似的非阻塞机制将slice回收到buffer:

select{

casebuffer<-pool[i]:

pool[i]=nil

default:

}

如果buffer这个channel满了,则以上的写入过程会阻塞,这种情况下default触发。这种简单的机制可以用于安全的创建一个共享池,甚至可通过channel传递实现多个goroutines之间的完美、安全共享。

在我们的实际项目中运用了相似的技术,实际使用中(简单版本)的回收器(recycler)展示在下面,有一个goroutine处理buffers的构造并在多个goroutine之间共享。get(获取一个新buffer)和give(回收一个buffer到pool)这两个channel被所有goroutines使用。

回收器对收回的buffer保持连接,并定期的丢弃那些过于陈旧可能不会再使用的buffer(在示例代码中这个周期是一分钟)。这让程序可以自动应对爆发性的buffers需求。

package mainimport (    "container/List"    "fmt"    "math/rand"    "runtime"    "time")var makes intvar frees intfunc makeBuffer() []byte {    makes += 1    return make([]byte,rand.Intn(5000000)+5000000)}type queued struct {    when time.Time    slice []byte}func makeRecycler() (get,give chan []byte) {    get = make(chan []byte)    give = make(chan []byte)    go func() {        q := new(List.List)        for {            if q.Len() == 0 {                q.PushFront(queued{when: time.Now(),slice: makeBuffer()})            }            e := q.Front()            timeout := time.NewTimer(time.Minute)            select {            case b := <-give:                timeout.Stop()                q.PushFront(queued{when: time.Now(),slice: b})           case get <- e.Value.(queued).slice:               timeout.Stop()               q.Remove(e)           case <-timeout.C:               e := q.Front()               for e != nil {                   n := e.Next()                   if time.Since(e.Value.(queued).when) > time.Minute {                       q.Remove(e)                       e.Value = nil                   }                   e = n               }           }       }    }()    return}func main() {    pool := make([][]byte,20)    get,give := makeRecycler()    var m runtime.MemStats    for {        b := <-get        i := rand.Intn(len(pool))        if pool[i] != nil {            give <- pool[i]        }        pool[i] = b        time.Sleep(time.Second)        bytes := 0        for i := 0; i < len(pool); i++ {            if pool[i] != nil {                bytes += len(pool[i])            }        }        runtime.ReadMemStats(&m)        fmt.Printf("%d,m.HeapAlloc             m.HeAPIdle,makes,frees)    }}


执行程序10分钟,图像会类似于第二幅:

这些技术可以用于程序员知道某些内存可以被重用,而不用借助于GC,可以显著的减少程序的内存使用,同时可以使用在其他数据类型而不仅是[]byteslice,任意类型的Gotype(用户定义的或许不行(user-definedornot))都可以用类似的手段回收。

总结

以上是内存溢出为你收集整理的golang手动管理内存全部内容,希望文章能够帮你解决golang手动管理内存所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1278966.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-09
下一篇 2022-06-09

发表评论

登录后才能评论

评论列表(0条)

保存