文档章节

golang手动管理内存

lubia
 lubia
发布于 2013/11/09 11:28
字数 1700
阅读 8707
收藏 26

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

前些天我介绍了我们对Lua的使用,implement our new Web Application Firewall

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

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

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

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

package main

import (  
    "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,%d,%d,%d\n", m.HeapSys, bytes, m.HeapAlloc,
            m.HeapIdle, m.HeapReleased, makes)
    }
}

程序使用 runtime.ReadMemStats函数来获取堆的使用信息。它打印了四个值,

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

HeapAlloc:堆上目前分配的内存

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

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

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

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

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

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

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

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

Quantifying the Performance of Garbage Collection vs. Explicit Memory Management

程序不断执行,idle memory(即HeapIdle)会被重用,但很少归还到操作系统。


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

程序可以这样重写:

package main

import (
	"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,%d,%d,%d,%d,%d\n", m.HeapSys, bytes, m.HeapAlloc,
			m.HeapIdle, m.HeapReleased, makes)
	}
}

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

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

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

select {

case b = <- buffer:

default :

makes += 1

b = makeBuffer()

}

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

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

select {

case buffer <- 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 main

import (
    "container/list"
    "fmt"
    "math/rand"
    "runtime"
    "time"
)

var makes int
var frees int

func 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,%d,%d,%d,%d,%d,%d\n", m.HeapSys, bytes, m.HeapAlloc
             m.HeapIdle, m.HeapReleased, makes, frees)
    }
}

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

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

© 著作权归作者所有

lubia

lubia

粉丝 7
博文 7
码字总数 6889
作品 1
朝阳
私信 提问
加载中

评论(4)

chendahui007
chendahui007
There are two separate processes 翻译成两个进程不合适吧?
坑爹的程序
好文章。收藏了。。。
l
lzmuhioin
运用go方式进行内存管理,学习
bozz
bozz
好文
golang之runtime.SetFinaliz

在实际的编程中,我们都希望每个对象释放时执行一个方法,在该方法内执行一些计数、释放或特定的要求,以往都是在对象指针置nil前调用一个特定的方法,golang提供了runtime.SetFinalizer函数...

wkh
2014/06/20
207
0
crontab 管理工具

crontab A crontab tool build by golang Crontab 背景: 在实际工作中经常需要在服务器上添加定时任务,当任务多了的时候管理起来就比较麻烦,所以想要有一个方便使用和管理的crontab工具 ...

chunhei2008
2015/01/01
3.4K
0
简单易懂的 Go 内存分配原理解读

1. 前言 编写过C语言程序的肯定知道通过malloc()方法动态申请内存,其中内存分配器使用的是glibc提供的ptmalloc2。 除了glibc,业界比较出名的内存分配器有Google的tcmalloc和Facebook的jem...

技术小能手
2018/10/15
0
0
golang cgo 使用总结(一)

CGO 提供了 golang 和 C 语言相互调用的机制。某些第三方库可能只有 C/C++ 的实现,完全用纯 golang 的实现可能工程浩大,这时候 CGO 就派上用场了。可以通 CGO 在 golang 在调用 C 的接口,...

echojson
04/18
129
1
golang: 类型转换和类型断言

类型转换在程序设计中都是不可避免的问题。当然有一些语言将这个过程给模糊了,大多数时候开发者并不需要去关注这方面的问题。但是golang中的类型匹配是很严格的,不同的类型之间通常需要手动...

陈亦
2014/01/20
34.8K
8

没有更多内容

加载失败,请刷新页面

加载更多

OSM初识(三)OSM Data

一 导出数据 将XML格式的OSM数据转换成另一种格式。 二 提取数据 剪切你选择区域内的数据,或者提取出特定区域特定的特征 三 数据格式 OSM文件仅属于OSM,不能用别的软件打开。其中后缀为bz2...

yuankaichao
33分钟前
7
0
Queue

class Queue { constructor() { this._items = []; } enqueue(item) { this._items.push(item); } dequeue() { this._items.shift();......

gtandsn
35分钟前
5
0
数据防泄漏系统有何实用价值?企业文件加密软件哪个好?江苏风奥科技

数据防泄漏系统的发展,减少了国内数据泄露事件的发生,保护了个人信息安全,企业信息安全,保密级数据安全等互联网时代发展的重点关注话题,国内信息化步伐的加快,改变了传统的办公以及发展...

fasoft
35分钟前
7
0
Helm 3 发布 | 云原生生态周报 Vol. 27

作者 | 墨封、元毅、冬岛、敖小剑、衷源 业界要闻 1.Helm 3 发布 美国时间 11 月 13 日,Helm 团队发布 Helm 3 第一个稳定版本。Helm 3 以 Helm 2 的核心特性为基础,改进了 chart 存储库、版...

一肥仔
42分钟前
3
0
Spring Boot2.x 内置Tomcat调优,使用Apr模式

启用Apr模式,需要调用方法 org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#setProtocol windows下安装配置native和apr只需要在环境变量path里能够找到 tcna...

xiaomin0322
48分钟前
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部