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。
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))都可以用類似的手段回收。
相關文章
- golang 系列:神祕的記憶體管理Golang記憶體
- 【記憶體管理】Oracle AMM自動記憶體管理詳解記憶體Oracle
- 【記憶體管理】Oracle如何使用ASMM自動共享記憶體管理記憶體OracleASM
- 記憶體管理 記憶體管理概述記憶體
- ORACLE AMM 、ASMM 、自動記憶體管理(官方手冊)OracleASM記憶體
- Golang 共享記憶體Golang記憶體
- 記憶體管理篇——實體記憶體的管理記憶體
- 【記憶體管理】記憶體佈局記憶體
- 記憶體管理兩部曲之實體記憶體管理記憶體
- Java的記憶體 -JVM 記憶體管理Java記憶體JVM
- Go:記憶體管理與記憶體清理Go記憶體
- JVM學習-自動記憶體管理JVM記憶體
- C語言之動態記憶體管理C語言記憶體
- JVM學習筆記——自動記憶體管理JVM筆記記憶體
- 記憶體管理兩部曲之虛擬記憶體管理記憶體
- golang 切片記憶體應用技巧Golang記憶體
- JavaScript 記憶體管理JavaScript記憶體
- iOS 記憶體管理iOS記憶體
- Android記憶體管理Android記憶體
- OC記憶體管理記憶體
- 記憶體管理-swMemoryGlobal記憶體
- Flink記憶體管理記憶體
- MySQL記憶體管理MySql記憶體
- golang 垃圾回收器如何標記記憶體?Golang記憶體
- JVM自動記憶體管理機制 二JVM記憶體
- oracle 11g自動記憶體管理Oracle記憶體
- C++動態記憶體管理——new/deleteC++記憶體delete
- 【精選】Mac 手動記憶體清理教程Mac記憶體
- linux記憶體管理(一)實體記憶體的組織和記憶體分配Linux記憶體
- Linux實體記憶體管理Linux記憶體
- Golang面向併發的記憶體模型Golang記憶體模型
- 深入理解golang:記憶體分配原理Golang記憶體
- golang的記憶體相關內容Golang記憶體
- iOS 記憶體管理MRCiOS記憶體
- “理解”iOS記憶體管理iOS記憶體
- iOS 記憶體管理研究iOS記憶體
- 01記憶體管理-概述記憶體
- python的記憶體管理Python記憶體