這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go語言的整個記憶體管理子系統主要由兩部分組成——記憶體 Clerk和垃圾收集器(gc)。十一小長假期為了避開我泱泱大國的人流高峰,於是在家宅了3天把Go語言的記憶體 Clerk部分的代碼給研究了一番,總的來說還是非常酷的,自己也學到了不少的東西,就此記錄分享一下。整個記憶體 Clerk完全是基於Google自家的tcmalloc的設計重新實現了一遍,因此,想看看Go語言的記憶體 Clerk實現的話,強烈建議先讀一讀tcmalloc的介紹文檔,然後看看Go runtime的malloc.h源碼檔案的注釋介紹,這樣基本就大概瞭解Go語言記憶體 Clerk的設計了。
Go的記憶體 Clerk主要也是解決小對象的分配管理和多線程的記憶體配置問題。(後面提到的記憶體 Clerk都是指代的Go語言實現的記憶體 Clerk)。記憶體 Clerk以32k作為對象大小的定奪標準,小於等於32k的記憶體對象一律視為小對象,而大於32k的對象就是大對象了。為何是32k作為分界線呢?這個我也不知道,我覺得這是一個經驗值吧,如果你知道有其他更加科學的理由,麻煩告知我一下。
記憶體 Clerk會將分配的小對象使用一個cache組件給緩衝起來,只要是分配小對象就先到cache中查詢一下,有閒置記憶體就直接返回使用,不用向作業系統申請記憶體。記憶體 Clerk的這個cache組件可能同時存在多個,也就是每個實際線程都會有一個cache組件,這樣一來,從cache裡查詢、擷取空閑記憶體的時候就不需要加鎖了,每次小對象的申請直接存取本線程對應的cache即可。我們再寫程式的時候,其實絕大多數的記憶體申請都是小於32k的,屬於小對象,因此這樣的記憶體配置全部走本地cache,不用向作業系統申請顯然是非常高效的。
有cache,必然就有cache不命中的情況,記憶體 Clerk在面對Cache
尋找不到空閑記憶體的時候,就會試圖從Central
中申請一批小對象記憶體到本機快取住,這裡的Central是所有線程共用的一個組件,不是獨佔的,因此需要加鎖操作。我們需要知道Central組件其實也是一個緩衝,但它緩衝的不是小對象記憶體塊,而是一組一組的記憶體page(一個page佔4k大小)。如果Central中沒有緩衝的空閑記憶體page的話,就從Heap
中申請記憶體來填充Central。當然對Heap的操作也是需要加鎖,所有線程共用一個Heap。Heap中沒有緩衝的記憶體,當然就直接從作業系統拿取記憶體了。
小對象的記憶體配置是通過一級一級的緩衝來實現的,目的就是為了提升記憶體配置釋放的速度以及避免記憶體片段等問題。大於32k的大對象記憶體配置就沒這麼麻煩了,不用一層一層的查詢各個緩衝組件,而是直接向Heap
申請。是大概描述了一下整個記憶體 Clerk的組件結構,Cache、Central、Heap是三個核心組件,也是後面將重點分析的對象。
記憶體 Clerk的實現我需要拆成多篇文章來寫,沒精力一口氣寫完,其實按組件拆開寫也方便閱讀嘛。後面的文章將陸續寫完各個核心組件。
***題外話,在基礎系統軟體的世界裡,記憶體管理是一個永恒的話題,所以存在tcmalloc和jemalloc這類非常優秀的記憶體 Clerk實現。據說,jemalloc在cpu核心數較多的情況下,效能還要優於tcmalloc,但估計它們之間是不相伯仲的,主體設計都差不多。jemalloc也是純C代碼,應該是非常值得一看的。不知道為何,現在對C++項目,總有研究拖延症,沒有強烈的動力去第一時間看源碼。