Golang 1.14中內存分配、清掃和內存回收

golang內存分配

Golang的內存分配是由golang runtime完成,其內存分配方案借鑒自tcmalloc。
主要特點就是

  • 為每個工作線程(P)都維護了一個分配cache,小對象((16,32KB])和微(Tiny)對象((1B,16B])基本上無需全局鎖。
  • 為常用的內存尺寸(16B,32KB] 之間內存分類為numSpanClass(134-2)種對象尺寸。位于此大小區(qū)間的內存分配都歸到這些spanclass 尺寸的element上進行分配。如此可以減少內存碎片的產生。

本文中的element指一定大小的內存塊是內存分配的概念,并為出現(xiàn)在golang runtime源碼中
本文講述x8664架構下的內存分配

主要結構

Golang 內存分配有下面幾個主要結構

  • mspan 用于管理一連串地址連續(xù)的頁面,golang將同一mspan維護的地址空間劃分為同一個尺寸的element,每個element用于存儲一個大小屬于當前SpanClass對象。
  • mcache 每個P都有一個mcache,在此mcache上有一個長度為numSpanClass的數組,里面存儲的每個成員指向一個mspan。
  • mcentral 全局的,每個mcentral用于管理相同的element size的mspan。
  • mheap 全局的,用于管理golang的整個堆。在mheap上有numSpanClass個mcentral數組。
  • heapArena 在x8664上用于管理64MB的堆空間。一個heapArena下有多個mspan。


    圖片發(fā)自簡書App

內存分配方案

微(Tiny)對象

Tiny對象是指內存尺寸小于16B的對象,這類對象的分配使用mcache的tiny區(qū)域進行分配。當tiny區(qū)域空間耗盡時刻,它會從mcache.alloc[tinySpanClass]指向的mspan中找到空閑的區(qū)域。當然如果mcache中span空間也耗盡,它會觸發(fā)從mcentral補充mspan到mcache的流程。

小對象

小對象是指對象尺寸在(16B,32KB]之間的對象,這類對象的分配原則是:
1、首先根據對象尺寸將對象歸為某個SpanClass上,這個SpanClass上所有的element都是一個統(tǒng)一的尺寸。
2、從mcache.alloc[SpanClass]找到mspan,看看有無空閑的element,如果有分配成功。如果沒有繼續(xù)。
3、從mcentral.allocSpan[SpanClass]的nonempty和emtpy中找到合適的mspan,返回給mcache。如果沒有找到就進入mcentral.grow()—>mheap.alloc()分配新的mspan給mcentral。

大對象的分配

大對象指尺寸超出32KB的對象,此時直接從mheap中分配,不會走mcache和mcentral,直接走mheap.alloc()分配一個SpanClass==0 的mspan表示這部分分配空間。

總結

對于程序分配常用的tiny和小對象的分配,可以通過無鎖的mcache提升分配性能。mcache不足時刻會拿mcentral的鎖,然后從mcentral中充mspan 給mcache。大對象直接從mheap 中分配。

進程虛擬地址空間管理

在x8664環(huán)境上,golang管理的有效的程序虛擬地址空間實質上只有48位。在mheap中有一個pages pageAlloc成員用于管理golang堆內存的地址空間。golang從os中申請地址空間給自己管理,地址空間申請下來以后,golang會將地址空間根據實際使用情況標記為free或者alloc。如果地址空間被分配給mspan或大對象后,那么被標記為alloc,反之就是free。

地址空間的狀態(tài)

Golang認為地址空間有以下4種狀態(tài):

  • None 地址空間初始狀態(tài)
  • Reserved 地址已經被golang runtime擁有,但是os并為真正分配,訪問此類地址出發(fā)異常
  • Prepared 地址已經是Reserved,但是也并為被OS分配真正地址空間。在Linux系統(tǒng)上,Prepared對應的是地址空間為MADV_FREE 表示os可以在自己認為需要的時候回收這段地址空間。
  • Ready 地址空間真真正正被os分配。訪問此空間不會出發(fā)異常。

Golang同時定義了下面幾個地址空間操作函數:

  • sysReserved 調用此函數后,地址從None轉換為Reserved狀態(tài)
  • sysAlloc 地址從None轉換為Ready狀態(tài),一般都是golang runtime自己內存管理對象的分配使用sysAlloc
  • sysFree 地址空間從任意狀態(tài)轉換為None
  • sysMap 地址空間從Reserved轉換為Prepared
  • sysUsed 地址空間從Prepared轉換為Ready
  • sysUnused 地址空間從Ready轉換為Prepared


golang如何管理自己的虛擬地址空間

在mheap結構中,有一個名為pages成員,它用于golang 堆使用虛擬地址空間進行管理。其類型為pageAlloc

type pageAlloc struct{
...
summary [summacryLevels][]pallocSum
chunks [1<<pallocChunksL1Bits][1<<pallocChunksL2Bits]
...
}
type pallocSum uint64
type pallocData struct{
     pallocBits
     scavenged pageBits
}
type pallocBits pageBits
// pallocChunkPages/64 =256/64 =4
type pageBits [pallocChunkPages/64]uint64

pageAlloc 結構表示的golang 堆的所有地址空間。其中最重要的成員有兩個:

  • summary 為二維數組,組成一個pallocSum的radix tree。在golang 1.14中x8664 radix tree 為4級。最終那一級節(jié)點表示一個chunk(2MB)地址空間的分配情況。
    pallocSum定義為64bit 長整型,它被分為三個bitmap:start,max 和end。start表示這段地址空間從起始地址開始連續(xù)的free的地址空間的頁面數量;max這段地址空間最大連續(xù)頁的數量,end 為這段地址空間的最后一個頁編號。


  • chunks 數組每個成員表示了地址空間內所有chunk里頁面分配和scavenge情況。其結構為pallocData,分為兩個成員,pallocBits成員表示已分配頁面的bitmap,scavenged 成員表示已scavenged的頁面的bitmap。
    chunk如前所述在x8664上為2MB,也就是常規(guī)的一個巨頁頁面大小
    當alloc mspan時刻,需要使用pageAlloc,涉及到下面函數的使用:

  • (s* pageAlloc)alloc找到一個地址空間可以容納待分配的頁面數量,并把這段地址空間標記為alloc。

  • (s*pageAlloc)grow 增長mheap管理的堆地址空間

  • (s*pageAlloc)free 釋放地址空間(將這段地址空間標記為free)
    alloc mspan時,先使用pageAalloc.alloc嘗試分配一段地址空間,如果沒有分配成功,就使用grow增加一段地址空間映射,最后再使用alloc分配。

空間清掃sweep

在golang的gc流程中會將未使用的對象標記為未使用,但是這些對象所使用的地址空間并未交還給os。地址空間的申請和釋放都是以golang的page為單位(實際以chunk為單位)進行的。sweep的最終結果只是將某個地址空間標記可被分配,并未真正釋放地址空間給os,真正釋放是后文的scavenge過程。

mspan的sweep

在gc mark結束以后會使用sweep()去嘗試free一個span;在mheap.alloc 申請mspan時刻,也使用sweep去清掃一下。
清掃mspan主要涉及到下面函數

  • mspan.sweep()掃描這個span中element的使用情況,當最終如果整個mspan所有的element都釋放了,那么使用freeSpan()
  • mheap.freeSpan()釋放golang runtime的這個mspan對象,同時將mspan表示的地址空間標記為可分配。

地址空間回收(scavenge)

如上節(jié)所述,sweep只是將page標記為可分配,但是并未把地址空間釋放;真正的地址空間釋放是scavenge過程。
真正的scavenge是由pageAlloc.scavenge()—>sysUnused()將掃描到待釋放的chunk所表示的地址空間釋放掉(使用sysUnused()將地址空間還給os)
golang的scavenge過程有兩種:

  • 同步scavenge,當每次mheap.grow() 增長mheap的內存的時刻,如果增長量達到一定水平就會觸發(fā)scavenge的掃描過程。在scavenge掃描過程中,golang會嘗試釋放一定數量的chunk。
  • 后臺scavenge,golang內部有定時器和相關的goroutine,定期掃描程序內存使用量,當內存使用量超出一定閾值的時候,也會調用scavenge過程,嘗試釋放內存給os系統(tǒng)。
  • golang runtime package中定義了 debug.freeOSMemory 會手動觸發(fā)一次gc,并調用scavengeAll,對mheap管理所有地址空間進行scavenge *
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容