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