人吧,不能糊裡糊塗的瞎忙活,你得規劃,成為體系後,方能事半功倍。 道、法、術、器:出自老子的《道德經》 道,是規則、自然法則,上乘。 法,是方法、法理,中乘。 術,是行為、方式,下乘。“以道御術”即以道義來承載智術,悟道比修煉法術更高一籌。“術”要符合“法”,“法”要基於“道”,道法術三者兼備才能做 ...
協程棧
go 棧的位置
1. Go 協程棧位於 Go-堆記憶體上
2. Go 堆記憶體位於操作系統虛擬記憶體上
go 棧的工作流程
以main.main為出發點
- 要記錄runtime.main的棧基地址
- 記錄 a 和 b的局部變數值
- 開闢一個空間記錄 sum函數的返回值
- 記錄 b 和 a的值, 這裡是為了方便 sum在執行時候,去找這個傳入的參數
- 記錄 sum返回後,要執行的指令,就是 print的執行位置.
這裡面能有一個重要體現, a 和 b 的值被覆制了一份, 一般稱為拷貝傳遞或值傳遞
傳遞結構體時:會拷貝結構體中的全部內容.
傳遞結構體指針時:會拷貝結構體指針
小結 協程棧記錄的內容:
協程的執行路徑
局部變數
函數傳參
函數返回值
問題: 如果協程棧的空間不夠大了, 該怎麼處理?
本地變數太大:比如有個變數 map 存了特別多的數據.
棧幀太多 :比如 函數a調用函數b, 一層層套,過多了.
棧空間不足
本地變數太大 使用逃逸分析
棧幀太多 使用 棧擴容
逃逸分析
針對變數太大, go的解決辦法 在編譯階段進行逃逸分析. 不是所有的變數都能放在協程棧上
需要逃逸的變數:
棧幀回收後,需要繼續使用的變數
太大的變數
指針逃逸
好理解, 函數a返回一個局部變數 b的地址給 函數A,
這種情況在很多語言中是會出問題的,返回了一個局部變數地址, 使用時候會發生無法預料的結果.
但是go會做逃逸分析, 會把這個變數放到堆上去.
空介面逃逸
如果函數參數為 interfacef, 函數的實參很可能會逃逸,例如 fmt包中的列印.
因為 interfacef類型的函數往往會使用反射
大變數逃逸
過大的變數會導致棧空間不足
64 位機器中,一般超過 64KB 的變數會逃逸
棧擴容
Go 棧的初始空間為 2KB
在函數調用前判斷棧空間(morestack) , 這個方法在講 切換協程時候, 這個方法也是一個時機
必要時對棧進行擴容
早期使用分段棧,後期使用連續棧
分段棧
每調用一個函數, 就跳轉到一塊寫的棧空間.
連續棧
優點:空間一直連續
缺點:伸縮時的開銷大
當空間不足時擴容,變為原來的2倍
當空間使用率不足 1/4 時縮容,變為原來的 1/2
協程棧的工作流程大致如上, 但是, 不管是棧還是堆上, 這些 記憶體是如何管理, 如何分配給申請的變數的 ?
go 記憶體管理
操作系統的虛擬記憶體
不是Win 的"虛擬記憶體〞, 不是指 操作系統將 硬碟的一塊空間 當做虛擬記憶體使用.
操作系統給應用提供的虛擬記憶體空間
背後是物理記憶體,也有可能有磁碟
Linux 獲取虛擬記憶體:mmap 或 madvice
假設機器的物理記憶體只有64G, 但是通過操作系統的虛擬記憶體擴大, 所有的進程都會以為擁有了一
塊特別大的空間, 256T, 但是不能真正用到這麼多, 一旦用太多就會被系統幹掉. Linux中的OOM.
這塊是操作系統的事, go 只能合理使用操作系統給進程的記憶體.
heapArena
Go 每次申請的虛擬記憶體單元為 64MB , 就是 heapArena, 如果每次拿的太小, 會多次去申請, 影響效率.
最多有4,194,304 個虛擬記憶體單元 (2的20次方) 剛好256T
所有的 heapArena 組成了mheap(Go 堆記憶體)
抽象出了兩個概念 :
整個go的堆記憶體 mheap , 包含了很多 heapArena
代碼定義:
// 代表著 整個go的記憶體空間
type mheap struct {
_ sys.NotInHeap
lock mutex
pages pageAlloc // page allocation data structure
sweepgen uint32
allspans []*mspan // all spans out there
pagesInUse atomic.Uintptr // pages of spans in stats mSpanInUse
pagesSwept atomic.Uint64 // pages swept this cycle
pagesSweptBasis atomic.Uint64 // pagesSwept to use as the origin of the sweep ratio
sweepHeapLiveBasis uint64 // value of gcController.heapLive to use as the origin of sweep ratio; written with lock, read without
sweepPagesPerByte float64 // proportional sweep ratio; written with lock, read without
reclaimIndex atomic.Uint64
reclaimCredit atomic.Uintptr
// 包含 最多 2的20次方個 heapArena
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
arenasHugePages bool
heapArenaAlloc linearAlloc
arenaHints *arenaHint
arena linearAlloc
allArenas []arenaIdx
sweepArenas []arenaIdx
markArenas []arenaIdx
curArena struct {
base, end uintptr
}
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
spanalloc fixalloc // allocator for span*
cachealloc fixalloc // allocator for mcache*
specialfinalizeralloc fixalloc // allocator for specialfinalizer*
specialprofilealloc fixalloc // allocator for specialprofile*
specialReachableAlloc fixalloc // allocator for specialReachable
specialPinCounterAlloc fixalloc // allocator for specialPinCounter
speciallock mutex // lock for special record allocators.
arenaHintAlloc fixalloc // allocator for arenaHints
// User arena state.
// Protected by mheap_.lock.
userArena struct {
arenaHints *arenaHint
quarantineList mSpanList
readyList mSpanList
}
unused *specialfinalizer // never set, just here to force the specialfinalizer type into DWARF
}
// 代表64M的記憶體空間
type heapArena struct {
_ sys.NotInHeap
bitmap [heapArenaBitmapWords]uintptr
noMorePtrs [heapArenaBitmapWords / 8]uint8
spans [pagesPerArena]*mspan // mspan 下麵會出現
pageInUse [pagesPerArena / 8]uint8
pageMarks [pagesPerArena / 8]uint8
pageSpecials [pagesPerArena / 8]uint8
checkmarks *checkmarksMap
zeroedBase uintptr
}
heapArena 分配方式
為了更好的管理這些申請的記憶體空間, heapArena會把這些空間 分成 大小不一的記憶體塊,進行管理. 一共 67種. 這些同級別的 一組記憶體塊就是
mspan
大小8k.
因此,go裡面一共有 67種 mspan.
每組 都是 8K, 因為 單個的大小不同,所以 分配的個數也不同.
所以, 協程在申請空間時候, 分配的是mspan中的一個對象, 一個對象代表著一個級別的記憶體大小,比如1級的8個位元組的空間
申請完64M的空間後,也不是馬上把給個級別都分配一次,而是按需分配,比如現在需要1級的,那就只分配一組一級的, 可能最後這64M裡面只有一級的
type mspan struct {
_ sys.NotInHeap
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
list *mSpanList // For debugging. TODO: Remove.
startAddr uintptr // address of first byte of span aka s.base()
npages uintptr // number of pages in span
manualFreeList gclinkptr // list of free objects in mSpanManual spans
freeindex uintptr
nelems uintptr // number of object in the span.
allocCache uint64
allocBits *gcBits
gcmarkBits *gcBits
pinnerBits *gcBits // bitmap for pinned objects; accessed atomically
sweepgen uint32
divMul uint32 // for divide by elemsize
allocCount uint16 // number of allocated objects
spanclass spanClass // size class and noscan (uint8)
state mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
needzero uint8 // needs to be zeroed before allocation
isUserArenaChunk bool // whether or not this span represents a user arena
allocCountBeforeCache uint16 // a copy of allocCount that is stored just before this span is cached
elemsize uintptr // computed from sizeclass or from npages
limit uintptr // end of data in span
speciallock mutex // guards specials list and changes to pinnerBits
specials *special // linked list of special records sorted by offset.
userArenaChunkFree addrRange // interval for managing chunk allocation
freeIndexForScan uintptr
}
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
這兩個欄位說明,mspan 可以形成一個鏈表.
spanclass spanClass // 是pan的級別 1到67
這樣的結構,導致一個問題, 很多不同級別的mspan在不同的 heapArena中, 當去協程去申請時候, 比如申請一個 一級的 span, 如何去尋找 ?
需要一個 索引管理 .
中心索引mcentral
136個
mcentral
結構體,其中 68個組需要GC掃描的mspan
, 68個組不需要GC掃描的mspan(例如存放一些常量)
每一組對應一個級別的mspan
, mspan
本身是可以組成一個鏈表的 next
和prev
.
// Central list of free objects of a given size.
type mcentral struct {
_ sys.NotInHeap
spanclass spanClass // 一個具體的級別
partial [2]spanSet // list of spans with a free object 空閑的
full [2]spanSet // list of spans with no free objects 不空閑
}
mheap結構體定義的:
numSpanClasses = 136
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
從mheap出發, 通過這個 central 這個元素 , 能找到所有的mspan
小結和問題:
mcentral實際是中心素引,使用互斥鎖保護
在高併發場景下,鎖衝突問題嚴重
參考協程GMP模型,增加線程本地緩存
線程緩存 mcache
模仿GMP的本地協程隊列, 避免每次申請都去 mcentral中, 因為 要為了線程安全, mcentral 需要加鎖, 導致有性能問題.
給每一個p增加一些 span的緩存,這樣避免每次都去申請.
每個P擁有一個mcache,也就是每個M,每個線程;
一個mcache擁有136個mspan,其中68個需要GC掃描的mspan, 68個不需要GC掃描的mspan
一樣來一個
如何本地的span
滿了, 就需要去和mcentral
中的span
進行交換, 如果mcentral
中也沒有可用的,就去申請,通過mheapArena
申請一個64M的空間
代碼定義:
numSpanClasses =136
type mcache struct {
_ sys.NotInHeap
// The following members are accessed on every malloc,
// so they are grouped here for better caching.
nextSample uintptr // trigger heap sample after allocating this many bytes
scanAlloc uintptr // bytes of scannable heap allocated
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
// 包含了136個span
alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
stackcache [_NumStackOrders]stackfreelist
flushGen atomic.Uint32
}
// 部分欄位
type p struct {
id int32
m muintptr // back-link to associated m (nil if idle)
mcache *mcache //對應一個mcahe
}
小結:
Go模仿TCmalloc (C++使用,也是Google開發的),建立了自己的堆記憶體架構
使用heapArena向操作系統申請記憶體
使用heapArena時,以mspan為單位,防止碎片化
mcentral是mspan們的中心索引
mcache記錄了分配給各個P的本地mspan
如何分配這些記憶體
對象分級
Tiny 微對象(0,16B) 無指針 如果是結構體, 成員不能包含指針
Small 小對象 [16B,32KB]
Large 大對象 (32KB, +∞)
使用
微小對象分配至普通 mspan, 32k以下
大對象量身定做 mspan, 這裡值得是0級, 上面提到mspan有67級,
但是,在定義mcentral時候, 定了68個不需要GC的,還有一個是沒有固定大小的級別, 0級.
微對象分配
- 從mcache 拿到2級mspan
- 將多個微對象合併成一個16Byte 存入
大對象分配
直接從heapArena開闢0級的mspan
0級的mspan為大對象定製
在runtime的 malloc.go中能體現:
// 分配記憶體的入口
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
if size == 0 {
return unsafe.Pointer(&zerobase)
}
if size <= maxSmallSize {
// 微小對象分配
} else {
span = c.allocLarge(size, noscan)
}
}
// allocLarge allocates a span for a large object.
func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
npages := size >> _PageShift
if size&_PageMask != 0 {
npages++
}
deductSweepCredit(npages*_PageSize, npages)
spc := makeSpanClass(0, noscan) // 0級 class
s := mheap_.alloc(npages, spc)
if s == nil {
throw("out of memory")
}
}
分配規則小結
Go將對象按照大小分為3種
微小對象使用mcache
mcache中的mspan填滿後,與mcentral交換新的
mcentral不足時,在heapArena開闢新的mspan
大對象直接在heapArena開闢新的mspan