<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # 7.1 設計原則 到目前為止,我們已經分析了 Go 程序如何啟動、初始化需要進行的關鍵步驟、初始化結束后, 主 goroutine 如何被調度器進行調度。現在我們來看 Go 中另一重要的關鍵組件:內存分配器。 Go 的內存分配器基于 Thread-Cache Malloc (tcmalloc) \[1\],tcmalloc 為每個線程實現了一個本地緩存, 區分了小對象(小于 32kb)和大對象分配兩種分配類型,其管理的內存單元稱為 span。 我們不再介紹更多 tcmalloc 的具體細節,因為 Go 的內存分配器與 tcmalloc 存在一定差異。 這個差異來源于 Go 語言被設計為沒有顯式的內存分配與釋放, 完全依靠編譯器與運行時的配合來自動處理,因此也就造就了內存分配器、垃圾回收器兩大組件。 我們知道,在計算機領域中,無外乎時間換空間、空間換時間。統一管理內存會提前分配或一次性釋放一大塊內存, 進而減少與操作系統溝通造成的開銷,進而提高程序的運行性能。 支持內存管理另一個優勢就是能夠更好的支持垃圾回收,這一點我們留到垃圾回收器一節中進行討論。 ## 主要結構 Go 的內存分配器主要包含以下幾個核心組件: * heapArena: 保留整個虛擬地址空間 * mheap:分配的堆,在頁大小為 8KB 的粒度上進行管理 * mspan:是 mheap 上管理的一連串的頁 * mcentral:收集了給定大小等級的所有 span * mcache:為 per-P 的緩存。 其中頁是向操作系統申請內存的最小單位,目前設計為 8KB。 每一個結構雖然不都像是調度器 M/P/G 結構那樣的大部頭,但初次閱讀這些結構時想要理清他們之間的關系還是比較麻煩的。 傳統意義上的棧被 Go 的運行時霸占,不開放給用戶態代碼;而傳統意義上的堆內存,又被 Go 運行時劃分為了兩個部分, 一個是 Go 運行時自身所需的堆內存,即堆外內存;另一部分則用于 Go 用戶態代碼所使用的堆內存,也叫做 Go 堆。 Go 堆負責了用戶態對象的存放以及 goroutine 的執行棧。 ### Arena #### heapArena Go 堆被視為由多個 arena 組成,每個 arena 在 64 位機器上為 64MB,且起始地址與 arena 的大小對齊, 所有的 arena 覆蓋了整個 Go 堆的地址空間。 ``` const ( pageSize = 8192 // 8KB heapArenaBytes = 67108864 // 64MB heapArenaBitmapBytes = heapArenaBytes / 32 // 2097152 pagesPerArena = heapArenaBytes / pageSize // 8192 ) //go:notinheap type heapArena struct { bitmap [heapArenaBitmapBytes]byte spans [pagesPerArena]*mspan pageInUse [pagesPerArena / 8]uint8 pageMarks [pagesPerArena / 8]uint8 zeroedBase uintptr } ``` #### arenaHint 結構比較簡單,是 arenaHint 鏈表的節點結構,保存了 arena 的起始地址、是否為最后一個 arena,以及下一個 arenaHint 指針。 ``` //go:notinheap type arenaHint struct { addr uintptr down bool next *arenaHint } ``` ### mspan 然而管理 arena 如此粒度的內存并不符合實踐,相反,所有的堆對象都通過 span 按照預先設定好的 大小等級分別分配,小于 32KB 的小對象則分配在固定大小等級的 span 上,否則直接從 mheap 上進行分配。 `mspan`是相同大小等級的 span 的雙向鏈表的一個節點,每個節點還記錄了自己的起始地址、 指向的 span 中頁的數量。它要么位于 ``` //go:notinheap type mspan struct { // 雙向鏈表 next *mspan // 鏈表中的下一個 span,如果為空則為 nil prev *mspan // 鏈表中的前一個 span,如果為空則為 nil ... startAddr uintptr // span 的第一個字節的地址,即 s.base() npages uintptr // 一個 span 中的 page 數量 manualFreeList gclinkptr // mSpanManual span 的釋放對象鏈表 ... freeindex uintptr nelems uintptr // span 中對象的數量 allocCache uint64 allocBits *gcBits ... allocCount uint16 // 分配對象的數量 spanclass spanClass // 大小等級與 noscan (uint8) incache bool // 是否被 mcache 使用 state mSpanState // mspaninuse 等等信息 ... } ``` ### mcache 是一個 per-P 的緩存,它是一個包含不同大小等級的 span 鏈表的數組,其中 mcache.alloc 的每一個數組元素 都是某一個特定大小的 mspan 的鏈表頭指針。 ``` //go:notinheap type mcache struct { ... tiny uintptr tinyoffset uintptr local_tinyallocs uintptr alloc [numSpanClasses]*mspan // 用來分配的 spans,由 spanClass 索引 stackcache [_NumStackOrders]stackfreelist ... } ``` 當 mcache 中 span 的數量不夠使用時,會向 mcentral 的 nonempty 列表中獲得新的 span。 ### mcentral mcentral ``` //go:notinheap type mcentral struct { lock mutex spanclass spanClass nonempty mSpanList // 帶有自由對象的 span 列表,即非空閑列表 empty mSpanList // 沒有自由對象的 span 列表(或緩存在 mcache 中) ... } ``` 當 mcentral 中 nonempty 列表中也沒有可分配的 span 時,則會向 mheap 提出請求,從而獲得 新的 span,并進而交給 mcache。 ### mheap ``` //go:notinheap type mheap struct { lock mutex pages pageAlloc ... allspans []*mspan // 所有 spans 從這里分配出去 scavengeGoal uint64 reclaimIndex uint64 reclaimCredit uintptr arenas [1 &lt;&lt; arenaL1Bits]*[1 &lt;&lt; arenaL2Bits]*heapArena heapArenaAlloc linearAlloc arenaHints *arenaHint arena linearAlloc allArenas []arenaIdx curArena struct { base, end uintptr } central [numSpanClasses]struct { mcentral mcentral pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte } ... // 各種分配器 spanalloc fixalloc // span* 分配器 cachealloc fixalloc // mcache* 分配器 treapalloc fixalloc // treapNodes* 分配器,用于大對象 specialfinalizeralloc fixalloc // specialfinalizer* 分配器 specialprofilealloc fixalloc // specialprofile* 分配器 speciallock mutex // 特殊記錄分配器的鎖 arenaHintAlloc fixalloc // arenaHints 分配器 ... } ``` ## 分配概覽 在分析具體的分配過程之前,我們需要搞清楚究竟什么時候會發生分配。 Go 程序的執行是基于 goroutine 的,goroutine 和傳統意義上的程序一樣,也有棧和堆的概念。只不過 Go 的運行時幫我們屏蔽掉了這兩個概念,只在運行時內部區分并分別對應:goroutine 執行棧以及 Go 堆。 goroutine 的執行棧與傳統意義上的棧一樣,當函數返回時,在棧上就會被回收,棧中的對象都會被回收,從而 無需 GC 的標記;而堆則麻煩一些,由于 Go 支持垃圾回收,只要對象生存在堆上,Go 的運行時 GC 就會在 后臺將對應的內存進行標記從而能夠在垃圾回收的時候將對應的內存回收,進而增加了開銷。 下面這個程序給出了四種情況: ``` package main type smallobj struct { arr [1 &lt;&lt; 10]byte } type largeobj struct { arr [1 &lt;&lt; 26]byte } func f1() int { x := 1 return x } func f2() *int { y := 2 return &amp;y } func f3() { large := largeobj{} println(&amp;large) } func f4() { small := smallobj{} print(&amp;small) } func main() { x := f1() y := f2() f3() f4() println(x, y) } ``` 我們使用`-gcflags "-N -l -m"`編譯這段代碼能夠禁用編譯器與內聯優化并進行逃逸分析: ``` # alloc.go # go build -gcflags "-N -l -m" -ldflags=-compressdwarf=false -o alloc.out alloc.go # command-line-arguments ./alloc.go:18:9: &amp;y escapes to heap ./alloc.go:17:2: moved to heap: y ./alloc.go:22:2: moved to heap: large ./alloc.go:23:10: f3 &amp;large does not escape ./alloc.go:28:8: f4 &amp;small does not escape ``` * 情況1:`f1`中`x`的變量被返回,沒有發生逃逸; * 情況2:`f2`中`y`的指針被返回,進而發生了逃逸; * 情況3:`f3`中`large`無法被一個執行棧裝下,即便沒有返回,也會直接在堆上分配; * 情況4:`f4`中`small`對象能夠被一個執行棧裝下,變量沒有返回到棧外,進而沒有發生逃逸。 如果我們再仔細檢查一下他們的匯編: ``` TEXT main.f2(SB) /Users/changkun/dev/go-under-the-hood/demo/4-mem/alloc/alloc.go ... alloc.go:17 0x104e086 488d05939f0000 LEAQ type.*+40256(SB), AX alloc.go:17 0x104e08d 48890424 MOVQ AX, 0(SP) alloc.go:17 0x104e091 e8cabffbff CALL runtime.newobject(SB) alloc.go:17 0x104e096 488b442408 MOVQ 0x8(SP), AX alloc.go:17 0x104e09b 4889442410 MOVQ AX, 0x10(SP) alloc.go:17 0x104e0a0 48c70002000000 MOVQ $0x2, 0(AX) ... TEXT main.f3(SB) /Users/changkun/dev/go-under-the-hood/demo/4-mem/alloc/alloc.go ... alloc.go:22 0x104e0ed 488d05ecf60000 LEAQ type.*+62720(SB), AX alloc.go:22 0x104e0f4 48890424 MOVQ AX, 0(SP) alloc.go:22 0x104e0f8 e863bffbff CALL runtime.newobject(SB) alloc.go:22 0x104e0fd 488b7c2408 MOVQ 0x8(SP), DI alloc.go:22 0x104e102 48897c2418 MOVQ DI, 0x18(SP) alloc.go:22 0x104e107 b900008000 MOVL $0x800000, CX alloc.go:22 0x104e10c 31c0 XORL AX, AX alloc.go:22 0x104e10e f348ab REP; STOSQ AX, ES:0(DI) ... ``` 就會發現,對于產生在 Go 堆上分配對象的情況,均調用了運行時的`runtime.newobject`方法。 當然,關鍵字`new`同樣也會被編譯器翻譯為此函數,這個我們已經在實踐中知道了。 所以`runtime.newobject`就是內存分配的核心入口了。 ### 分配入口 單看`runtime.newobject`其實非常簡單,他只是簡單的調用了`mallocgc`: ``` // 創建一個新的對象 func newobject(typ *_type) unsafe.Pointer { return mallocgc(typ.size, typ, true) // true 內存清零 } ``` 其中`_type`為 Go 類型的實現,通過其`size`屬性能夠獲得該類型所需要的大小。 ``` func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { // 創建大小為零的對象,例如空結構體 if size == 0 { return unsafe.Pointer(&amp;zerobase) } mp := acquirem() mp.mallocing = 1 ... // 獲取當前 g 所在 M 所綁定 P 的 mcache c := gomcache() var x unsafe.Pointer noscan := typ == nil || typ.kind&amp;kindNoPointers != 0 if size &lt;= maxSmallSize { if noscan &amp;&amp; size &lt; maxTinySize { // 微對象分配 ... } else { // 小對象分配 ... } } else { // 大對象分配 ... } ... mp.mallocing = 0 releasem(mp) ... return x ``` 在分配過程中,我們會發現需要持有 M 才可進行分配,這是因為分配不僅可能涉及 mcache,還需要將正在分配的 M 標記為`mallocing`,用于記錄當前 M 的分配狀態。 ### 小對象分配 當對一個小對象(<32KB)分配內存時,會將該對象所需的內存大小調整到某個能夠容納該對象的大小等級(size class), 并查看 mcache 中對應等級的 mspan,通過掃描 mspan 的`freeindex`來確定是否能夠進行分配。 當沒有可分配的 mspan 時,會從 mcentral 中獲取一個所需大小空間的新的 mspan,從 mcentral 中分配會對其進行加鎖, 但一次性獲取整個 span 的過程均攤了對 mcentral 加鎖的成本。 如果 mcentral 的 mspan 也為空時,則它也會發生增長,從而從 mheap 中獲取一連串的頁,作為一個新的 mspan 進行提供。 而如果 mheap 仍然為空,或者沒有足夠大的對象來進行分配時,則會從操作系統中分配一組新的頁(至少 1MB), 從而均攤與操作系統溝通的成本。 ### 微對象分配 對于過小的微對象(<16B),它們的分配過程與小對象的分配過程基本類似,但是是直接存儲在 mcache 上,并由其以 16B 的塊大小直接進行管理和釋放。 ### 大對象分配 大對象分配非常粗暴,不與 mcache 和 mcentral 溝通,直接繞過并通過 mheap 進行分配。 ## 小結 圖 1 展示了所有結構的關系。 ![](https://golang.design/under-the-hood/assets/mem-struct.png)**圖 1: Go 內存管理結構總覽** heap 最中間的灰色區域 arena 覆蓋了 Go 程序的整個虛擬內存, 每個 arena 包括一段 bitmap 和一段指向連續 span 的指針; 每個 span 由一串連續的頁組成;每個 arena 的起始位置通過 arenaHint 進行記錄。 分配的順序從右向左,代價也就越來越大。 小對象和微對象優先從白色區域 per-P 的 mcache 分配 span,這個過程不需要加鎖(白色); 若失敗則會從 mheap 持有的 mcentral 加鎖獲得新的 span,這個過程需要加鎖,但只是局部(灰色); 若仍失敗則會從右側的 free 或 scav 進行分配,這個過程需要對整個 heap 進行加鎖,代價最大(黑色)。 ## 進一步閱讀的參考文獻 1. [TCMalloc : Thread-Caching Malloc](http://goog-perftools.sourceforge.net/doc/tcmalloc.html)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看