golang手动管理内存
作者: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。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
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,%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)会被重用,但很少归还到操作系统。
解决此问题的一个办法是在程序中手动进行内存管理。例如,
程序可以这样重写:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
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,%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需求。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
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,%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))都可以用类似的手段回收。
golang手动管理内存的更多相关文章
- iOS手动管理内存
虽然iOS已经有了ARC帮你自动管理内存,但在有些项目中必须采用手动的方式,而且在懂得手动管理内存的情况下会是自己的代码更加完善 众所周知,基于手动管理内存的情况下必然涉及到 relese reta ...
- xcode 手动管理内存 的相关知识点总结
一.XCode4.2以后支持自动释放内存ARC xcode自4.2以后就支持自动释放内存了,但有时我们还是想手动管理内存,这如何处理呢. 很简单,想要取消自动释放,只要在 Build Setting ...
- jvm是如何管理内存的
1.JVM是如何管理内存的 Java中,内存管理是JVM自动进行的,无需人为干涉. 了解Java内存模型看这里:java内存模型是什么样的 了解jvm实例结构看这里:jvm实例的结构是什么样的 创建对 ...
- iOS 非ARC基本内存管理系列 2-多对象内存管理(3) 利用@property来自动管理内存
iOS 基本内存管理-多对象内存管理(2)中可以看到涉及到对象的引用都要手动管理内存:每个对象都需要写如下代码 // 1.对要传入的"新车"对象car和目前Person类对象所拥有 ...
- OCP读书笔记(13) - 管理内存
SGA 1. 什么是LRULRU表示Least Recently Used,也就是指最近最少使用的buffer header链表LRU链表串联起来的buffer header都指向可用数据块 2. 什 ...
- 浅谈javascript性能-管理内存
上次说到,javascript脚本到底应该放在哪里?用什么用处? 以下2点: 在Html.Body部分中的JS会在页面加载的时候执行.即-用户触发一个事件的时候执行的脚本.eg:onload事件... ...
- 内存管理 & 内存优化技巧 浅析
内存管理 浅析 下列行为都会增加一个app的内存占用: 1.创建一个OC对象: 2.定义一个变量: 3.调用一个函数或者方法. 如果app占用内存过大,系统可能会强制关闭app,造成闪退现象,影响用户 ...
- Oracle DB管理内存
• 描述SGA 中的内存组件• 实施自动内存管理• 手动配置SGA 参数• 配置自动PGA 内存管理 内存管理:概览DBA 必须将内存管理视为其工作中至关重要的部分,因为:• 可用内存空间量有限• ...
- golang包管理工具
软件开发中,不可避免的会使用到第三方库,因此包管理工具可以极大的方便开发者管理第三方依赖,避免掉入"依赖地狱". 作为google强大背书的golang语言,golang官方包管理 ...
随机推荐
- Fedora 24 Linux 环境下实现 Infinality 字体渲染增强及 Java 字体渲染改善的方法(修订)
Fedora 24 Linux 桌面环境默认字体渲染引擎 freetype 及字体配置工具 fontconfig 采用的是未经优化的编译及设置,字体渲染效果比较差.而某些 Linux 发行版的桌面字体 ...
- javascript实现复选框的全选全不选
通过复选框的id获取到复选框 元素 对复选框绑定点击事件 每个checkbox都设置相同的name checkOne 通过得到的元素获取checkbox的状态 当点击全选全不选checkbox时,检查 ...
- AC日记——集合位置 洛谷 P1491
集合位置 思路: 次短路: 先走一遍最短路: 记录最短路径,然后依次删边走最短路: 最短的长度就是次短路: 来,上代码: #include <queue> #include <cma ...
- 安卓全屏状态下键盘充满屏幕留不出ui控件的解决办法附edittext和键盘的属性
1.我们先看看常用和不常用的属性值(Edittext) android:inputType参数类型说明 android:inputType="none"--输入普通字符 andro ...
- hihoCoder #1586 : Minimum-结构体版线段树(单点更新+区间最值求区间两数最小乘积) (ACM-ICPC国际大学生程序设计竞赛北京赛区(2017)网络赛)
#1586 : Minimum Time Limit:1000ms Case Time Limit:1000ms Memory Limit:256MB Description You are give ...
- 洛谷—— P2417 课程
https://www.luogu.org/problemnew/show/2417 题目描述 n个学生去p个课堂,每一个学生都有自己的课堂,并且每个学生只能去一个课堂,题目要求能够安排每一个课堂都有 ...
- IIS 7 Access to the path ‘c:\windows\system32\inetsrv\’ is denied
https://randypaulo.wordpress.com/2011/09/13/iis-7-access-to-the-path-cwindowssystem32inetsrv-isdenie ...
- Jenkins连接git时出现“Failed to connect to repository : Command ... HEAD" returned status code 128:”的问题解决
网上说的解决方法如下: 其实生成ssh时不应该使用当前用户去生成ssh,而是使用jenkins这个用户去生成ssh,然后再去git服务器上配置你生成key,最后再jenkins上配置返回给你的key. ...
- 【IntelliJ IDEA】代码中出现Usage of API documented as @since 1.8+ more..
在idea中写代码过程中.有这种报错出现: Usage of API documented as @since 1.8+ more.. 修改JDK版本的几个地方 最后,在pom.xml文件中添加: & ...
- Ubuntu免安装配置MySQL
1.下载mysql http://cdn.mysql.com/Downloads/MySQL-5.6/mysql-5.6.21-linux-glibc2.5-x86_64.tar.gz 2.解压 ta ...