golang手動管理記憶體

來源:互聯網
上載者:User

 

作者: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。

123456789101112131415161718192021222324252627282930313233343536373839 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)會被重用,但很少歸還到作業系統。

 

解決此問題的一個辦法是在程式中手動進行記憶體管理。例如,

程式可以這樣重寫:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354 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需求。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293 package main import (    "container/list"    "fmt"    "math/rand"    "runtime"    "time") var makes intvar 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))都可以用類似的手段回收。

 

  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.