Go語言內置運行時(就是runtime),不同於傳統的內存分配方式,go為自主管理,最開始是基於tcmalloc架構,后面逐步迭新。自主管理可實現更好的內存使用模式,如內存池、預分配等,從而避免了系統調用所帶來的性能問題。
1. 基本策略
- 每次從操作系統申請一大塊內存,然后將其按特定大小分成小塊,構成鏈表(組織方式是一個單鏈表數組,數組的每個元素是一個單鏈表,鏈表中的每個元素具有相同的大小。);
- 為對象分配內存時從大小合適的鏈表提取一小塊,避免每次都向操作系統申請內存,減少系統調用。
- 回收對象內存時將該小塊重新歸還到原鏈表,以便復用;若閑置內存過多,則歸還部分內存到操作系統,降低整體開銷。
1.1 內存塊
span:即上面所說的操作系統分配的大塊內存,由多個地址連續的頁組成;
object:由span按特定大小切分的小塊內存,每一個可存儲一個對象;
按照用途,span面向內部管理,object面向對象分配。
關於span
內存分配器按照頁數來區分不同大小的span,如以頁數為單位將span存放到管理數組中,且以頁數作為索引;
span大小並非不變,在沒有獲取到合適大小的閑置span時,返回頁數更多的span,然后進行剪裁,多余的頁數構成新的span,放回管理數組;
分配器還可以將相鄰的空閑span合並,以構建更大的內存塊,減少碎片提供更靈活的分配策略。
分配的內存塊大小
在$GOROOT/src/runtime/malloc.go文件下可以找到相關信息。
1 //malloc.go 2 _PageShift = 13 3 _PageSize = 1<< _PageShift //8KB
用於存儲對象的object,按8字節倍數分為n種。如,大小為24的object可存儲范圍在17~24字節的對象。在造成一些內存浪費的同時減少了小塊內存的規格,優化了分配和復用的管理策略。
分配器還會將多個微小對象組合到一個object塊內,以節約內存。
1 //malloc.go 2 _NumSizeClasses = 67
1 //mheap.go 2 type mspan struct { 3 next *mspan //雙向鏈表 next span in list, or nil if none 4 prev *mspan //previous span in list, or nil if none 5 list *mSpanList //用於調試。TODO: Remove. 6 7 //起始序號 = (address >> _PageShift) 8 startAddr uintptr //address of first byte of span aka s.base() 9 npages uintptr //number of pages in span 10 11 //待分配的object鏈表 12 manualFreeList gclinkptr //list of free objects in mSpanManual spans 13 }
分配器初始化時,會構建對照表存儲大小和規格的對應關系,包括用來切分的span頁數。
1 //msize.go 2 3 // Malloc small size classes. 4 // 5 // See malloc.go for overview. 6 // See also mksizeclasses.go for how we decide what size classes to use. 7 8 package runtime 9 10 // 如果需要,返回mallocgc將分配的內存塊的大小。 11 func roundupsize(size uintptr) uintptr { 12 if size < _MaxSmallSize { 13 if size <= smallSizeMax-8 { 14 return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]) 15 } else { 16 return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]]) 17 } 18 } 19 if size+_PageSize < size { 20 return size 21 } 22 return round(size, _PageSize) 23 }
如果對象大小超出特定閾值限制,會被當做大對象(large object)特別對待。
1 //malloc.go 2 _MaxSmallSize = 32 << 10 //32KB
這里的對象分類:
- 小對象(tiny): size < 16byte;
- 普通對象: 16byte ~ 32K;
- 大對象(large):size > 32K;
1.2 內存分配器
分配器分為三個模塊
cache:每個運行期工作線程都會綁定一個cache,用於無鎖object分配(Central組件其實也是一個緩存,但它緩存的不是小對象內存塊,而是一組一組的內存page(一個page占4k大小))。
1 //mcache.go 2 type mcache struct{ 3 以spanClass為索引管理多個用於分配的span 4 alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass 5 }
central:為所有cache提供切分好的后備span資源。
1 //mcentral.go 2 type mcentral struct{ 3 spanclass spanClass //規格 4 //鏈表:尚有空閑object的span 5 nonempty mSpanList // list of spans with a free object, ie a nonempty free list 6 // 鏈表:沒有空閑object,或已被cache取走的span 7 empty mSpanList // list of spans with no free objects (or cached in an mcache) 8 } 9
heap:管理閑置span,需要時間向操作系統申請新內存(堆分配器,以8192byte頁進行管理)。
1 type mheap struct{ 2 largealloc uint64 // bytes allocated for large objects 3 //頁數大於127(>=127)的閑置span鏈表 4 largefree uint64 // bytes freed for large objects (>maxsmallsize) 5 nlargefree uint64 // number of frees for large objects (>maxsmallsize) 6 //頁數在127以內的閑置span鏈表數組 7 nsmallfree [_NumSizeClasses]uint64 // number of frees for small objects (<=maxsmallsize) 8 //每個central對應一種sizeclass 9 central [numSpanClasses]struct { 10 mcentral mcentral 11 pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte 12 }
一個線程有一個cache對應,這個cache用來存放小對象。所有線程共享Central和Heap。
虛擬地址空間
內存分配和垃圾回收都依賴連續地址,所以系統預留虛擬地址空間,用於內存分配,申請內存時,系統承諾但不立即分配物理內存。虛擬地址分成三個區域:
- 頁所屬span指針數組 spans 512MB spans_mapped
- GC標記位圖 bitmap 32GB bit_map
- 用戶內存分配區域 arena 512GB arena_start arena_used arena_end
三個數組組成一個高性能內存管理結構。使用arena地址向操作系統申請內存,其大小決定了可分配用戶內存上限;bitmap為每個對象提供4bit 標記位,用以保存指針、GC標記等信息;創建span時,按頁填充對應spans空間。這些區域的相關屬性保存在heap里,其中包括遞進的分配位置mapped/used。
各個模塊關系圖如下:
1.3 內存分配流程
從對象的角度:
1、計算待分配對象規格大小(size class);
2、cache.alloc數組中找到對應規格的apan;
3、span.freelist提取可用object,若該span.freelist為空從central獲取新sapn;
4、若central.nonempty為空,從heap.free/freelarge獲取,並切分成object 鏈表;
5、如heap沒有大小合適的閑置span,向操作系統申請新內存塊。
釋放流程:
1、將標記為可回收的object交還給所屬span.freelist;
2、該span被放回central,可供任意cache重新獲取使用;
3、如span已回收全部object,則將其交還給heap,以便重新切分復用;
4、定期掃描heap里長期閑置的span,釋放其占用內存。
(注:以上不包括大對象,它直接從heap分配和回收)
cache為每個工作線程私有且不被共享,是實現高性能無鎖分配內存的核心。central是在多個cache中提高object的利用率,避免浪費。回收操作將span交還給central后,該span可被其他cache重新獲取使用。將span歸還給heap是為了在不同規格object間平衡。
2. 內存分配器初始化
初始化流程:
1 func mallocinit() { 2 testdefersizes() 3 4 if heapArenaBitmapBytes&(heapArenaBitmapBytes-1) != 0 { 5 // heapBits需要位圖上的模塊化算法工作地址。 6 throw("heapArenaBitmapBytes not a power of 2") 7 } 8 9 // //復制類大小以用於統計信息表。 10 for i := range class_to_size { 11 memstats.by_size[i].size = uint32(class_to_size[i]) 12 } 13 14 // 檢查 physPageSize. 15 if physPageSize == 0 { 16 // 操作系統初始化代碼無法獲取物理頁面大小。 17 throw("failed to get system page size") 18 } 19 if physPageSize < minPhysPageSize { 20 print("system page size (", physPageSize, ") is smaller than minimum page size (", minPhysPageSize, ")\n") 21 throw("bad system page size") 22 } 23 if physPageSize&(physPageSize-1) != 0 { 24 print("system page size (", physPageSize, ") must be a power of 2\n") 25 throw("bad system page size") 26 } 27 28 // 初始化堆。 29 mheap_.init() 30 //為當前對象綁定cache對象 31 _g_ := getg() 32 _g_.m.mcache = allocmcache() 33 34 //創建初始 arena 增長提示。 35 if sys.PtrSize == 8 && GOARCH != "wasm" { 36 //在64位計算機上: 37 // 1.從地址空間的中間開始,可以輕松擴展到連續范圍,而無需運行其他映射。 38 // 39 // 2.這使Go堆地址調試時更容易識別。 40 // 41 // 3. gccgo中的堆棧掃描仍然很保守,因此將地址與其他數據區分開很重要。 42 // 43 //在AIX上,對於64位,mmaps從0x0A00000000000000開始設置保留地址,如果失敗,則嘗試0x1c00000000000000~0x7fc0000000000000。 44 // 流程. 45 for i := 0x7f; i >= 0; i-- { 46 var p uintptr 47 switch { 48 case GOARCH == "arm64" && GOOS == "darwin": 49 p = uintptr(i)<<40 | uintptrMask&(0x0013<<28) 50 case GOARCH == "arm64": 51 p = uintptr(i)<<40 | uintptrMask&(0x0040<<32) 52 case GOOS == "aix": 53 if i == 0 { 54 //我們不會直接在0x0A00000000000000之后使用地址,以避免與非執行程序所完成的其他mmap發生沖突。 55 continue 56 } 57 p = uintptr(i)<<40 | uintptrMask&(0xa0<<52) 58 case raceenabled: 59 // TSAN運行時要求堆的范圍為[0x00c000000000,0x00e000000000)。 60 p = uintptr(i)<<32 | uintptrMask&(0x00c0<<32) 61 if p >= uintptrMask&0x00e000000000 { 62 continue 63 } 64 default: 65 p = uintptr(i)<<40 | uintptrMask&(0x00c0<<32) 66 } 67 hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc()) 68 hint.addr = p 69 hint.next, mheap_.arenaHints = mheap_.arenaHints, hint 70 } 71 } else { 72 //在32位計算機上,需要更加關注保持可用堆是連續的。 73 // 74 // 1.我們為所有的heapArenas保留空間,這樣它們就不會與heap交錯。它們約為258MB。 75 // 76 // 2. 我們建議堆從二進制文件的末尾開始,因此我們有最大的機會保持其連續性。 77 // 78 // 3. 我們嘗試放出一個相當大的初始堆保留。 79 80 const arenaMetaSize = (1 << arenaBits) * unsafe.Sizeof(heapArena{}) 81 meta := uintptr(sysReserve(nil, arenaMetaSize)) 82 if meta != 0 { 83 mheap_.heapArenaAlloc.init(meta, arenaMetaSize) 84 } 85 86 procBrk := sbrk0() 87 88 p := firstmoduledata.end 89 if p < procBrk { 90 p = procBrk 91 } 92 if mheap_.heapArenaAlloc.next <= p && p < mheap_.heapArenaAlloc.end { 93 p = mheap_.heapArenaAlloc.end 94 } 95 p = round(p+(256<<10), heapArenaBytes) 96 // // 因為我們擔心32位上的碎片,所以我們嘗試進行較大的初始保留。 97 arenaSizes := []uintptr{ 98 512 << 20, 99 256 << 20, 100 128 << 20, 101 } 102 for _, arenaSize := range arenaSizes { 103 a, size := sysReserveAligned(unsafe.Pointer(p), arenaSize, heapArenaBytes) 104 if a != nil { 105 mheap_.arena.init(uintptr(a), size) 106 p = uintptr(a) + size // For hint below 107 break 108 } 109 } 110 hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc()) 111 hint.addr = p 112 hint.next, mheap_.arenaHints = mheap_.arenaHints, hint 113 } 114 }
大概流程:
1、創建對象規格大小對照表;
2、計算相關區域大小,並嘗試從某個指定位置開始保留地址空間;
3、在heap里保存區域信息,包括起始位置和大小;
4、初始化heap其他屬性。
看一下保留地址操作細節:
1 //mem_linux.go 2 func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer { 3 p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0) //PORT_NONE: 頁面無法訪問; 4 if err != 0 { 5 return nil 6 } 7 return p 8 } 9 10 func sysMap(v unsafe.Pointer, n uintptr, sysStat *uint64) { 11 mSysStatInc(sysStat, n) 12 13 p, err := mmap(v, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE, -1, 0) //_MAP_FIXED: 必須使用指定起始位置 14 if err == _ENOMEM { 15 throw("runtime: out of memory") 16 } 17 if p != v || err != 0 { 18 throw("runtime: cannot map pages in arena address space") 19 } 20 }
函數mmap()要求操作系統內核創建新的虛擬存儲器區域,可指定起始位置和長度。
3. 內存分配
編譯器有責任盡可能使用寄存器和棧來存儲對象,有助於提升性能,減少垃圾回收器的壓力。
以new函數為例看一下內存分配
1 //test.go 2 package main 3 4 import () 5 6 func test() *int { 7 x :=new(int) 8 *x = 0xAABB 9 return x 10 } 11 12 func main(){ 13 println(*test()) 14 }
在默認有內聯優化的時候:
內聯優化是避免棧和搶占檢查這些成本的經典優化方法。
在沒有內聯優化的時候new函數會調用newobject在堆上分配內存。要在兩個棧幀間傳遞對象,因此會在堆上分配而不是返回一個失效棧幀里的數據。而當內聯后它實際上就成了main棧幀內的局部變量,無須去堆上操作。
GO語言支持逃逸分析(eseape, analysis), 它會在編譯期通過構建調用圖來分析局部變量是否會被外部調用,從而決定是否可以直接分配在棧上。
編譯參數-gcflags "-m" 可輸出編譯優化信息,其中包括內聯和逃逸分析。性能測試時使用go-test-benchemem參數可以輸出堆分配次數統計。
3.1 newobject分配內存的過程
1 //mcache.go 2 3 //小對象的線程(按Go,按P)緩存。 不需要鎖定,因為它是每個線程的(每個P)。 mcache是從非GC的內存中分配的,因此任何堆指針都必須進行特殊處理。 4 //go:not in heap 5 type mcache struct { 6 ... 7 // Allocator cache for tiny objects w/o pointers.See "Tiny allocator" comm ent in malloc.go. 8 // tiny指向當前微小塊的開頭,如果沒有當前微小塊,則為nil。 9 // 10 // tiny是一個堆指針。 由於mcache位於非GC的內存中,因此我們通過在標記終止期間在releaseAll中將其清除來對其進行處理。 11 tiny uintptr 12 tinyoffset uintptr 13 local_tinyallocs uintptr // 未計入其他統計信息的微小分配數 14 15 // 其余的不是在每個malloc上訪問的。 16 alloc [numSpanClasses]*mspan // 要分配的范圍,由spanClass索引 17 }
內置new函數的實現
1 //malloc.go 2 // implementation of new builtin 3 // compiler (both frontend and SSA backend) knows the signature 4 // of this function 5 func newobject(typ *_type) unsafe.Pointer { 6 return mallocgc(typ.size, typ, true) 7 } 8 9 // Allocate an object of size bytes. 10 // Small objects are allocated from the per-P cache's free lists. 11 // Large objects (> 32 kB) are allocated straight from the heap. 12 ///分配一個大小為字節的對象。小對象是從per-P緩存的空閑列表中分配的。 大對象(> 32 kB)直接從堆中分配。 13 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { 14 if gcphase == _GCmarktermination { //垃圾回收有關 15 throw("mallocgc called with gcphase == _GCmarktermination") 16 } 17 18 if size == 0 { 19 return unsafe.Pointer(&zerobase) 20 } 21 if debug.sbrk != 0 { 22 align := uintptr(16) 23 if typ != nil { 24 align = uintptr(typ.align) 25 } 26 return persistentalloc(size, align, &memstats.other_sys) //圍繞sysAlloc的包裝程序,可以分配小塊。沒有相關的自由操作。用於功能/類型/調試相關的持久數據。如果align為0,則使用默認的align(當前為8)。返回的內存將被清零。考慮將持久分配的類型標記為go:notinheap。 27 } 28 29 // assistG是要為此分配收費的G,如果GC當前未激活,則為n。 30 var assistG *g 31 32 ... 33 34 // Set mp.mallocing to keep from being preempted by GC. 35 //加鎖放防止GC被搶占。 36 mp := acquirem() 37 if mp.mallocing != 0 { 38 throw("malloc deadlock") 39 } 40 if mp.gsignal == getg() { 41 throw("malloc during signal") 42 } 43 mp.mallocing = 1 44 45 shouldhelpgc := false 46 dataSize := size 47 48 //當前線程所綁定的cache 49 c := gomcache() 50 var x unsafe.Pointer 51 // 判斷分配的對象是否 是nil或非指針類型 52 noscan := typ == nil || typ.kind&kindNoPointers != 0 53 //微小對象 54 if size <= maxSmallSize { 55 //無須掃描非指針微小對象(16) 56 if noscan && size < maxTinySize { 57 // Tiny allocator. 58 //微小的分配器將幾個微小的分配請求組合到一個內存塊中。當所有子對象均不可訪問時,將釋放結果存儲塊。子對象必須是noscan(沒有指針),以確保限制可能浪費的內存量。 59 //用於合並的存儲塊的大小(maxTinySize)是可調的。當前設置為16字節. 60 //小分配器的主要目標是小字符串和獨立的轉義變量。在json基准上,分配器將分配數量減少了約12%,並將堆大小減少了約20%。 61 off := c.tinyoffset 62 // 對齊所需(保守)對齊的小指針。調整偏移量。 63 if size&7 == 0 { 64 off = round(off, 8) 65 } else if size&3 == 0 { 66 off = round(off, 4) 67 } else if size&1 == 0 { 68 off = round(off, 2) 69 } 70 //如果剩余空間足夠. 當前mcache上綁定的tiny 塊內存空間足夠,直接分配,並返回 71 if off+size <= maxTinySize && c.tiny != 0 { 72 // 返回指針,調整偏移量為下次分配做好准備。 73 x = unsafe.Pointer(c.tiny + off) 74 c.tinyoffset = off + size 75 c.local_tinyallocs++ 76 mp.mallocing = 0 77 releasem(mp) 78 return x 79 } 80 //當前mcache上的 tiny 塊內存空間不足,分配新的maxTinySize塊。就是從sizeclass=2的span.freelist獲取一個16字節object。 81 span := c.alloc[tinySpanClass] 82 // 嘗試從 allocCache 獲取內存,獲取不到返回0 83 v := nextFreeFast(span) 84 if v == 0 { 85 // 沒有從 allocCache 獲取到內存,netxtFree函數 嘗試從 mcentral獲取一個新的對應規格的內存塊(新span),替換原先內存空間不足的內存塊,並分配內存,后面解析 nextFree 函數 86 v, _, shouldhelpgc = c.nextFree(tinySpanClass) 87 } 88 x = unsafe.Pointer(v) 89 (*[2]uint64)(x)[0] = 0 90 (*[2]uint64)(x)[1] = 0 91 // 對比新舊兩個tiny塊剩余空間,看看我們是否需要用剩余的自由空間來替換現有的微型塊。新塊分配后其tinyyoffset = size,因此比對偏移量即可 92 if size < c.tinyoffset || c.tiny == 0 { 93 //用新塊替換 94 c.tiny = uintptr(x) 95 c.tinyoffset = size 96 } 97 //消費一個新的完整tiny塊 98 size = maxTinySize 99 } else { 100 // 這里開始 正常對象的 內存分配 101 102 // 首先查表,以確定 sizeclass 103 var sizeclass uint8 104 if size <= smallSizeMax-8 { 105 sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv] 106 } else { 107 sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv] 108 } 109 size = uintptr(class_to_size[sizeclass]) 110 spc := makeSpanClass(sizeclass, noscan) 111 //找到對應規格的span.freelist,從中提取object 112 span := c.alloc[spc] 113 // 同小對象分配一樣,嘗試從 allocCache 獲取內存,獲取不到返回0 114 v := nextFreeFast(span) 115 116 //沒有可用的object。從central獲取新的span。 117 if v == 0 { 118 v, span, shouldhelpgc = c.nextFree(spc) 119 } 120 x = unsafe.Pointer(v) 121 if needzero && span.needzero != 0 { 122 memclrNoHeapPointers(unsafe.Pointer(v), size) 123 } 124 } 125 } else { 126 // 這里開始大對象的分配 127 128 // 大對象的分配與 小對象 和普通對象 的分配有點不一樣,大對象直接從 mheap 上分配 129 var s *mspan 130 shouldhelpgc = true 131 systemstack(func() { 132 s = largeAlloc(size, needzero, noscan) 133 }) 134 s.freeindex = 1 135 s.allocCount = 1 136 //span.start實際由address >> pageshift生成。 137 x = unsafe.Pointer(s.base()) 138 size = s.elemsize 139 } 140 141 // bitmap標記... 142 // 檢查出發條件,啟動垃圾回收 ... 143 144 return x 145 }
代碼基本思路:
1. 判定對象是大對象、小對象還是微小對象。
2. 如果是微小對象:
直接從 mcache 的alloc 找到對應 classsize 的 mspan;
若當前mspan有足夠空間,分配並修改mspan的相關屬性(nextFreeFast函數中實現);
若當前mspan的空間不足,則從 mcentral重新獲取一塊 對應 classsize的 mspan,替換原先的mspan,然后分配並修改mspan的相關屬性;
對於微小對象,它不能是指針,因為多個微小對象被組合到一個object里,顯然無法應對辣雞掃描。其次它從span.freelist獲取一個16字節的object,然后利用偏移量來記錄下一次分配的位置。
3. 如果是小對象,內存分配邏輯大致同微小對象:
首先查表,以確定 需要分配內存的對象的 sizeclass,並找到 對應 classsize的 mspan;
若當前mspan有足夠的空間,分配並修改mspan的相關屬性(nextFreeFast函數中實現);
若當前mspan沒有足夠的空間,從 mcentral重新獲取一塊對應 classsize的 mspan,替換原先的mspan,然后分配並修改mspan的相關屬性;
4. 如果是大對象,直接從mheap進行分配,這里的實現依靠 largeAlloc
函數實現,再看一下這個函數的實現。還是在malloc.go下面:
1 func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan { 2 // print("largeAlloc size=", size, "\n") 3 4 // 內存溢出判斷 5 if size+_PageSize < size { 6 throw("out of memory") 7 } 8 9 // 計算出對象所需的頁數 10 npages := size >> _PageShift 11 if size&_PageMask != 0 { 12 npages++ 13 } 14 15 // Deduct credit for this span allocation and sweep if 16 // necessary. mHeap_Alloc will also sweep npages, so this only 17 // pays the debt down to npage pages. 18 // 清理(Sweep)垃圾 19 deductSweepCredit(npages*_PageSize, npages) 20 21 // 分配函數的具體實現 22 s := mheap_.alloc(npages, makeSpanClass(0, noscan), true, needzero) 23 if s == nil { 24 throw("out of memory") 25 } 26 s.limit = s.base() + size 27 // bitmap 記錄分配的span 28 heapBitsForAddr(s.base()).initSpan(s) 29 return s 30 }
再看看 mheap_.allo()函數的實現:
1 //mheap.go 2 // alloc allocates a new span of npage pages from the GC'd heap. 3 // Either large must be true or spanclass must indicates the span's size class and scannability. 4 // If needzero is true, the memory for the returned span will be zeroed. 5 func (h *mheap) alloc(npage uintptr, spanclass spanClass, large bool, needzero bool) *mspan { 6 // Don't do any operations that lock the heap on the G stack. 7 // It might trigger stack growth, and the stack growth code needs 8 // to be able to allocate heap. 9 //如果needzero為true,則返回范圍的內存將為零。 10 //不要執行任何將堆鎖定在G堆棧上的操作。 11 //它可能會觸發堆棧增長,而堆棧增長代碼需要能夠分配堆。 12 var s *mspan 13 systemstack(func() { 14 s = h.alloc_m(npage, spanclass, large) 15 }) 16 17 if s != nil { 18 if needzero && s.needzero != 0 { 19 memclrNoHeapPointers(unsafe.Pointer(s.base()), s.npages<<_PageShift) 20 } 21 s.needzero = 0 22 } 23 return s 24 }
1 //mheap.go 2 func (h *mheap) alloc_m(npage uintptr, spanclass spanClass, large bool) *mspan { 3 _g_ := getg() 4 if _g_ != _g_.m.g0 { 5 throw("_mheap_alloc not on g0 stack") 6 } 7 lock(&h.lock) 8 9 // 清理垃圾,內存塊狀態標記 省略... 10 11 // 從 heap中獲取指定頁數的span 12 s := h.allocSpanLocked(npage, &memstats.heap_inuse) 13 if s != nil { 14 // Record span info, because gc needs to be 15 // able to map interior pointer to containing span. 16 atomic.Store(&s.sweepgen, h.sweepgen) 17 h.sweepSpans[h.sweepgen/2%2].push(s) // Add to swept in-use list.// 忽略 18 s.state = _MSpanInUse 19 s.allocCount = 0 20 s.spanclass = spanclass 21 // 重置span的狀態 22 if sizeclass := spanclass.sizeclass(); sizeclass == 0 { 23 s.elemsize = s.npages << _PageShift 24 s.divShift = 0 25 s.divMul = 0 26 s.divShift2 = 0 27 s.baseMask = 0 28 } else { 29 s.elemsize = uintptr(class_to_size[sizeclass]) 30 m := &class_to_divmagic[sizeclass] 31 s.divShift = m.shift 32 s.divMul = m.mul 33 s.divShift2 = m.shift2 34 s.baseMask = m.baseMask 35 } 36 37 // update stats, sweep lists 38 h.pagesInUse += uint64(npage) 39 if large { 40 // 更新 mheap中大對象的相關屬性 41 memstats.heap_objects++ 42 mheap_.largealloc += uint64(s.elemsize) 43 mheap_.nlargealloc++ 44 atomic.Xadd64(&memstats.heap_live, int64(npage<<_PageShift)) 45 // Swept spans are at the end of lists. 46 // 根據頁數判斷是busy還是 busylarge鏈表,並追加到末尾 47 if s.npages < uintptr(len(h.busy)) { 48 h.busy[s.npages].insertBack(s) 49 } else { 50 h.busylarge.insertBack(s) 51 } 52 } 53 } 54 // gc trace 標記,省略... 55 unlock(&h.lock) 56 return s 57 }
//mheap.go
1 func (h *mheap) allocSpanLocked(npage uintptr, stat *uint64) *mspan { 2 var list *mSpanList 3 var s *mspan 4 5 // Try in fixed-size lists up to max. 6 // 先嘗試獲取指定頁數的span,如果沒有,則試試頁數更多的 7 for i := int(npage); i < len(h.free); i++ { 8 list = &h.free[i] 9 if !list.isEmpty() { 10 s = list.first 11 list.remove(s) 12 goto HaveSpan 13 } 14 } 15 // Best fit in list of large spans. 16 // 從 freelarge 上找到一個合適的span節點返回 ,下面繼續分析這個函數 17 s = h.allocLarge(npage) // allocLarge removed s from h.freelarge for us 18 if s == nil { 19 // 如果 freelarge上找不到合適的span節點,就只有從 系統 重新分配了 20 // 后面繼續分析這個函數 21 if !h.grow(npage) { 22 return nil 23 } 24 // 從系統分配后,再次到freelarge 上尋找合適的節點 25 s = h.allocLarge(npage) 26 if s == nil { 27 return nil 28 } 29 } 30 31 HaveSpan: 32 // 從 free 上面獲取到了 合適頁數的span 33 // Mark span in use. 省略.... 34 35 if s.npages > npage { 36 // Trim extra and put it back in the heap. 37 // 創建一個 s.napges - npage 大小的span,並放回 heap 38 t := (*mspan)(h.spanalloc.alloc()) 39 t.init(s.base()+npage<<_PageShift, s.npages-npage) 40 // 更新獲取到的span s 的屬性 41 s.npages = npage 42 h.setSpan(t.base()-1, s) 43 h.setSpan(t.base(), t) 44 h.setSpan(t.base()+t.npages*pageSize-1, t) 45 t.needzero = s.needzero 46 s.state = _MSpanManual // prevent coalescing with s 47 t.state = _MSpanManual 48 h.freeSpanLocked(t, false, false, s.unusedsince) 49 s.state = _MSpanFree 50 } 51 s.unusedsince = 0 52 // 將s放到spans 和 arenas 數組里面 53 h.setSpans(s.base(), npage, s) 54 55 *stat += uint64(npage << _PageShift) 56 memstats.heap_idle -= uint64(npage << _PageShift) 57 58 //println("spanalloc", hex(s.start<<_PageShift)) 59 if s.inList() { 60 throw("still in list") 61 } 62 return s 63 }
1 //mheap.go 2 func (h *mheap) allocLarge(npage uintptr) *mspan { 3 // Search treap for smallest span with >= npage pages. 4 return h.freelarge.remove(npage) 5 } 6 7 // 上面的 h.freelarge.remove 即調用這個函數 8 // 典型的二叉樹尋找算法 9 func (root *mTreap) remove(npages uintptr) *mspan { 10 t := root.treap 11 for t != nil { 12 if t.spanKey == nil { 13 throw("treap node with nil spanKey found") 14 } 15 if t.npagesKey < npages { 16 t = t.right 17 } else if t.left != nil && t.left.npagesKey >= npages { 18 t = t.left 19 } else { 20 result := t.spanKey 21 root.removeNode(t) 22 return result 23 } 24 } 25 return nil 26 }
1 func (h *mheap) grow(npage uintptr) bool { 2 ask := npage << _PageShift 3 // 向系統申請內存,后面繼續追蹤 sysAlloc 這個函數 4 v, size := h.sysAlloc(ask) 5 if v == nil { 6 print("runtime: out of memory: cannot allocate ", ask, "-byte block (", memstats.heap_sys, " in use)\n") 7 return false 8 } 9 10 // Create a fake "in use" span and free it, so that the 11 // right coalescing happens. 12 // 創建 span 來管理剛剛申請的內存 13 s := (*mspan)(h.spanalloc.alloc()) 14 s.init(uintptr(v), size/pageSize) 15 h.setSpans(s.base(), s.npages, s) 16 atomic.Store(&s.sweepgen, h.sweepgen) 17 s.state = _MSpanInUse 18 h.pagesInUse += uint64(s.npages) 19 // 將剛剛申請的span放到 arenas 和 spans 數組里面 20 h.freeSpanLocked(s, false, true, 0) 21 return true 22 }
1 func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { 2 n = round(n, heapArenaBytes) 3 4 // First, try the arena pre-reservation. 5 // 從 arena 中 獲取對應大小的內存, 獲取不到返回nil 6 v = h.arena.alloc(n, heapArenaBytes, &memstats.heap_sys) 7 if v != nil { 8 // 從arena獲取到需要的內存,跳轉到 mapped操作 9 size = n 10 goto mapped 11 } 12 13 // Try to grow the heap at a hint address. 14 // 嘗試 從 arenaHint向下擴展內存 15 for h.arenaHints != nil { 16 hint := h.arenaHints 17 p := hint.addr 18 if hint.down { 19 p -= n 20 } 21 if p+n < p { 22 // We can't use this, so don't ask. 23 // 表名 hint.down = false 不能向下擴展內存 24 v = nil 25 } else if arenaIndex(p+n-1) >= 1<<arenaBits { 26 // 超出 heap 可尋址的內存地址,不能使用 27 // Outside addressable heap. Can't use. 28 v = nil 29 } else { 30 // 當前hint可以向下擴展內存,利用mmap向系統申請內存 31 v = sysReserve(unsafe.Pointer(p), n) 32 } 33 if p == uintptr(v) { 34 // Success. Update the hint. 35 if !hint.down { 36 p += n 37 } 38 hint.addr = p 39 size = n 40 break 41 } 42 // Failed. Discard this hint and try the next. 43 // 44 // TODO: This would be cleaner if sysReserve could be 45 // told to only return the requested address. In 46 // particular, this is already how Windows behaves, so 47 // it would simply things there. 48 if v != nil { 49 sysFree(v, n, nil) 50 } 51 h.arenaHints = hint.next 52 h.arenaHintAlloc.free(unsafe.Pointer(hint)) 53 } 54 55 if size == 0 { 56 if raceenabled { 57 // The race detector assumes the heap lives in 58 // [0x00c000000000, 0x00e000000000), but we 59 // just ran out of hints in this region. Give 60 // a nice failure. 61 throw("too many address space collisions for -race mode") 62 } 63 64 // All of the hints failed, so we'll take any 65 // (sufficiently aligned) address the kernel will give 66 // us. 67 v, size = sysReserveAligned(nil, n, heapArenaBytes) 68 if v == nil { 69 return nil, 0 70 } 71 72 // Create new hints for extending this region. 73 hint := (*arenaHint)(h.arenaHintAlloc.alloc()) 74 hint.addr, hint.down = uintptr(v), true 75 hint.next, mheap_.arenaHints = mheap_.arenaHints, hint 76 hint = (*arenaHint)(h.arenaHintAlloc.alloc()) 77 hint.addr = uintptr(v) + size 78 hint.next, mheap_.arenaHints = mheap_.arenaHints, hint 79 } 80 81 // Check for bad pointers or pointers we can't use. 82 { 83 var bad string 84 p := uintptr(v) 85 if p+size < p { 86 bad = "region exceeds uintptr range" 87 } else if arenaIndex(p) >= 1<<arenaBits { 88 bad = "base outside usable address space" 89 } else if arenaIndex(p+size-1) >= 1<<arenaBits { 90 bad = "end outside usable address space" 91 } 92 if bad != "" { 93 // This should be impossible on most architectures, 94 // but it would be really confusing to debug. 95 print("runtime: memory allocated by OS [", hex(p), ", ", hex(p+size), ") not in usable address space: ", bad, "\n") 96 throw("memory reservation exceeds address space limit") 97 } 98 } 99 100 if uintptr(v)&(heapArenaBytes-1) != 0 { 101 throw("misrounded allocation in sysAlloc") 102 } 103 104 // Back the reservation. 105 sysMap(v, size, &memstats.heap_sys) 106 107 mapped: 108 // Create arena metadata. 109 // 根據 v 的address,計算出 arenas 的L1 L2 110 for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ { 111 l2 := h.arenas[ri.l1()] 112 if l2 == nil { 113 // 如果 L2 為 nil,則分配 arenas[L1] 114 // Allocate an L2 arena map. 115 l2 = (*[1 << arenaL2Bits]*heapArena)(persistentalloc(unsafe.Sizeof(*l2), sys.PtrSize, nil)) 116 if l2 == nil { 117 throw("out of memory allocating heap arena map") 118 } 119 atomic.StorepNoWB(unsafe.Pointer(&h.arenas[ri.l1()]), unsafe.Pointer(l2)) 120 } 121 122 // 如果 arenas[ri.L1()][ri.L2()] 不為空 說明已經實例化過了 123 if l2[ri.l2()] != nil { 124 throw("arena already initialized") 125 } 126 var r *heapArena 127 // 從 arena 上分配內存 128 r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys)) 129 if r == nil { 130 r = (*heapArena)(persistentalloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys)) 131 if r == nil { 132 throw("out of memory allocating heap arena metadata") 133 } 134 } 135 136 // Store atomically just in case an object from the 137 // new heap arena becomes visible before the heap lock 138 // is released (which shouldn't happen, but there's 139 // little downside to this). 140 atomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r)) 141 } 142 // ... 143 return 144 }
大對象的分配流程至此結束。
3.2 小對象和微小對象的分配
nextFreeFast()函數返回 span 上可用的地址,如果找不到 則返回0
1 func nextFreeFast(s *mspan) gclinkptr { 2 // 計算s.allocCache從低位起有多少個0 3 theBit := sys.Ctz64(s.allocCache) // Is there a free object in the allocCache? 4 if theBit < 64 { 5 6 result := s.freeindex + uintptr(theBit) 7 if result < s.nelems { 8 freeidx := result + 1 9 if freeidx%64 == 0 && freeidx != s.nelems { 10 return 0 11 } 12 // 更新bitmap、可用的 slot索引 13 s.allocCache >>= uint(theBit + 1) 14 s.freeindex = freeidx 15 s.allocCount++ 16 // 返回 找到的內存的地址 17 return gclinkptr(result*s.elemsize + s.base()) 18 } 19 } 20 return 0 21 }
mcache.nextFree()函數。如果 nextFreeFast 找不到 合適的內存,就會進入這個函數。nextFree 如果在cached span 里面找到未使用的object,則返回,否則,調用refill 函數,從 central 中獲取對應classsize的span,然后 從新的span里面找到未使用的object返回。
1 //mcache.go 2 func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) { 3 // 先找到 mcache 中 對應 規格的 span 4 s = c.alloc[spc] 5 shouldhelpgc = false 6 // 在 當前span中找到合適的 index索引 7 freeIndex := s.nextFreeIndex() 8 if freeIndex == s.nelems { 9 // The span is full. 10 // freeIndex == nelems 時,表示當前span已滿 11 if uintptr(s.allocCount) != s.nelems { 12 println("runtime: s.allocCount=", s.allocCount, "s.nelems=", s.nelems) 13 throw("s.allocCount != s.nelems && freeIndex == s.nelems") 14 } 15 // 調用refill函數,從 mcentral 中獲取可用的span,並替換掉當前 mcache里面的span 16 systemstack(func() { 17 c.refill(spc) 18 }) 19 shouldhelpgc = true 20 s = c.alloc[spc] 21 22 // 再次到新的span里面查找合適的index 23 freeIndex = s.nextFreeIndex() 24 } 25 26 if freeIndex >= s.nelems { 27 throw("freeIndex is not valid") 28 } 29 30 // 計算出來 內存地址,並更新span的屬性 31 v = gclinkptr(freeIndex*s.elemsize + s.base()) 32 s.allocCount++ 33 if uintptr(s.allocCount) > s.nelems { 34 println("s.allocCount=", s.allocCount, "s.nelems=", s.nelems) 35 throw("s.allocCount > s.nelems") 36 } 37 return 38 }
mcache.refill()函數
Refill 根據指定的sizeclass獲取對應的span,並作為 mcache的新的sizeclass對應的span
1 //mcache.go
2 func (c *mcache) refill(spc spanClass) { 3 _g_ := getg() 4
5 _g_.m.locks++
6 // Return the current cached span to the central lists.
7 s := c.alloc[spc] 8
9 if uintptr(s.allocCount) != s.nelems { 10 throw("refill of span with free space remaining") 11 } 12
13 // 判斷s是不是 空的span
14 if s != &emptymspan { 15 s.incache = false
16 } 17 // 嘗試從 mcentral 獲取一個新的span來代替老的span 18 // Get a new cached span from the central lists.
19 s = mheap_.central[spc].mcentral.cacheSpan() 20 if s == nil { 21 throw("out of memory") 22 } 23
24 if uintptr(s.allocCount) == s.nelems { 25 throw("span has no free space") 26 } 27 // 更新mcache的span
28 c.alloc[spc] = s 29 _g_.m.locks--
30 }
如果 從 mcentral 找不到對應的span,就會開始內存擴張,和上面分析的 mheap.alloc
就相同了
3.3 內置函數make() 和 new() 的區別
1 // The new built-in function allocates memory. The first argument is a type, 2 // not a value, and the value returned is a pointer to a newly 3 // allocated zero value of that type. 4 // new內置函數分配內存。 第一個參數是一個類型,而不是一個值,返回的值是一個指向該類型新分配的零值的指針。
5 func new(Type) *Type 6
7 // The make built-in function allocates and initializes an object of type 8 // slice, map, or chan (only). Like new, the first argument is a type, not a 9 // value. Unlike new, make's return type is the same as the type of its 10 // argument, not a pointer to it. The specification of the result depends on 11 // the type: 12 // Slice: The size specifies the length. The capacity of the slice is 13 // equal to its length. A second integer argument may be provided to 14 // specify a different capacity; it must be no smaller than the 15 // length, so make([]int, 0, 10) allocates a slice of length 0 and 16 // capacity 10. 17 // Map: An empty map is allocated with enough space to hold the 18 // specified number of elements. The size may be omitted, in which case 19 // a small starting size is allocated. 20 // Channel: The channel's buffer is initialized with the specified 21 // buffer capacity. If zero, or the size is omitted, the channel is unbuffered. 22 // make 內置函數分配並初始化一個 slice、map 或 chan(僅限)類型的對象。 和 new 一樣,第一個參數是一個類型,而不是一個值。 與 new 不同,make 的返回類型與其參數的類型相同,而不是指向它的指針。 結果的規范取決於類型: 23 // 切片:大小指定長度。 切片的容量等於其長度。 可以提供第二個整數參數來指定不同的容量; 它必須不小於長度,因此 make([]int, 0, 10) 分配長度為 0 和容量為 10 的切片。 24 // Map: 一個空的映射被分配了足夠的空間來容納指定數量的元素。 可以省略大小,在這種情況下分配一個小的起始大小。 25 // 通道:通道的緩沖區被初始化為指定的緩沖區容量。 如果為零,或者省略了大小,則通道是無緩沖的。
26 func make(t Type, size ...IntegerType) Type
1. 返回值:
從定義中可以看出,new返回的是指向Type的指針,指向分配的內存地址,該指針可以被隱式地消除引用。 同時 new 函數會把分配的內存置為零,也就是類型的零值。
make直接返回的是Type類型值,一個T類型的結構。
2. 參數:
new只有一個Type參數,Type可以是任意類型數據。 make可以有多個參數,其中第一個參數與new的參數相同,但是只能是slice,map,或者chan中的一種。對於不同類型,size參數說明如下:
- 對於slice,第一個size表示長度,第二個size表示容量,且容量不能小於長度。如果省略第二個size,默認容量等於長度。
- 對於map,會根據size大小分配資源,以足夠存儲size個元素。如果省略size,會默認分配一個小的起始size。
- 對於chan,size表示緩沖區容量。如果省略size,channel為無緩沖channel。
3.類型:
make 被用來分配引用類型的內存: map, slice, channel
new 被用來分配除了引用類型的所有其他類型的內存: int, string, array,自定義類型等
var sum *int sum = new(int) //分配空間 *sum = 98 fmt.Println(*sum) type Student struct { name string age int } var s *Student s = new(Student) //分配空間 s.name ="dequan" fmt.Println(s)
4.原理
make 在編譯期的類型檢查階段,Go語言其實就將代表 make 關鍵字的 OMAKE 節點根據參數類型的不同轉換成了 OMAKESLICE、OMAKEMAP 和 OMAKECHAN 三種不同類型的節點,這些節點最終也會調用不同的運行時函數來初始化數據結構。
new 內置函數 new 會在編譯期的 SSA 代碼生成階段經過 callnew 函數的處理,如果請求創建的類型大小是 0,那么就會返回一個表示空指針的 zerobase 變量,在遇到其他情況時會將關鍵字轉換成 newobject。原理如上所述。
主要區別如下:
- make 只能用來分配及初始化類型為 slice、map、chan 的數據。new 可以分配任意類型的數據;
- new 分配返回的是指針,即類型 *Type。make 返回引用,即 Type;
- new 分配的空間被清零。make 分配空間后,會進行初始化;
4. 總結
1. 判定對象大小:
2. 若是微小對象:
- 從 mcache 的 alloc 找到對應 classsize 的 mspan;
- 當前mspan有足夠的空間時,分配並修改mspan的相關屬性(nextFreeFast函數中實現);
- 若當前mspan沒有足夠的空間,從 mcentral 重新獲取一塊對應 classsize的 mspan,替換原先的mspan,然后分配並修改mspan的相關屬性;
- 若 mcentral 沒有足夠的對應的classsize的span,則去向mheap申請;
- 若對應classsize的span沒有了,則找一個相近的classsize的span,切割並分配;
- 若找不到相近的classsize的span,則去向系統申請,並補充到mheap中;
3. 若是小對象,內存分配邏輯大致同小對象:
- 查表以確定需要分配內存的對象的 sizeclass,找到 對應classsize的 mspan;
- mspan有足夠的空間時,分配並修改mspan的相關屬性(nextFreeFast函數中實現);
- 若當前mspan沒有足夠的空間,從 mcentral重新獲取一塊對應 classsize的 mspan,替換原先的mspan,然后分配並修改mspan的相關屬性;
- 若mcentral沒有足夠的對應的classsize的span,則去向mheap申請;
- 若對應classsize的span沒有了,則找一個相近的classsize的span,切割並分配
- 若找不到相近的classsize的span,則去向系統申請,並補充到mheap中
4. 若是大對象,直接從mheap進行分配
- 若對應classsize的span沒有了,則找一個相近的classsize的span,切割並分配;
- 若找不到相近的classsize的span,則去向系統申請,並補充到mheap中;