編寫過C語言程序的肯定知道通過malloc()方法動態(tài)申請內(nèi)存,其中內(nèi)存分配器使用的是glibc提供的ptmalloc2。 除了glibc,業(yè)界比較出名的內(nèi)存分配器有Google的tcmalloc和Facebook的jemalloc。二者在避免內(nèi)存碎片和性能上均比glic有比較大的優(yōu)勢,在多線程環(huán)境中效果更明顯。
創(chuàng)新互聯(lián)建站是一家專業(yè)提供云溪企業(yè)網(wǎng)站建設(shè),專注與成都網(wǎng)站制作、網(wǎng)站設(shè)計、H5高端網(wǎng)站建設(shè)、小程序制作等業(yè)務(wù)。10年已為云溪眾多企業(yè)、政府機構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)的建站公司優(yōu)惠進(jìn)行中。
Golang中也實現(xiàn)了內(nèi)存分配器,原理與tcmalloc類似,簡單的說就是維護(hù)一塊大的全局內(nèi)存,每個線程(Golang中為P)維護(hù)一塊小的私有內(nèi)存,私有內(nèi)存不足再從全局申請。另外,內(nèi)存分配與GC(垃圾回收)關(guān)系密切,所以了解GC前有必要了解內(nèi)存分配的原理。
為了方便自主管理內(nèi)存,做法便是先向系統(tǒng)申請一塊內(nèi)存,然后將內(nèi)存切割成小塊,通過一定的內(nèi)存分配算法管理內(nèi)存。 以64位系統(tǒng)為例,Golang程序啟動時會向系統(tǒng)申請的內(nèi)存如下圖所示:
預(yù)申請的內(nèi)存劃分為spans、bitmap、arena三部分。其中arena即為所謂的堆區(qū),應(yīng)用中需要的內(nèi)存從這里分配。其中spans和bitmap是為了管理arena區(qū)而存在的。
arena的大小為512G,為了方便管理把arena區(qū)域劃分成一個個的page,每個page為8KB,一共有512GB/8KB個頁;
spans區(qū)域存放span的指針,每個指針對應(yīng)一個page,所以span區(qū)域的大小為(512GB/8KB)乘以指針大小8byte = 512M
bitmap區(qū)域大小也是通過arena計算出來,不過主要用于GC。
span是用于管理arena頁的關(guān)鍵數(shù)據(jù)結(jié)構(gòu),每個span中包含1個或多個連續(xù)頁,為了滿足小對象分配,span中的一頁會劃分更小的粒度,而對于大對象比如超過簡培頁大小,則通過多頁實現(xiàn)。
根據(jù)對象大小,劃分了一系列class,每個class都代表一個固定大小的對象,以及每個span的大小。如下表所示:
上表中每列含義如下:
class: class ID,每個span結(jié)構(gòu)中都有一個class ID, 表示該span可處理的對象類型
bytes/obj:該class代表對象的字節(jié)數(shù)
bytes/span:攔祥唯每個span占用堆的字節(jié)數(shù),也即頁數(shù)乘以頁大小
objects: 每個span可分配的對象個數(shù),也即(bytes/spans)/(bytes/obj)waste
bytes: 每個span產(chǎn)生的內(nèi)存碎片,也即(bytes/spans)%(bytes/obj)上表可見最大的對象是32K大小,超過32K大小的由特殊的class表示,該class ID為0,每個class只包含一個對象。
span是內(nèi)存管理的基本單位,每個span用于管理特定的class對象, 跟據(jù)對象大小,span將一個或多個頁拆分成多個塊進(jìn)行管理。src/runtime/mheap.go:mspan定義了其數(shù)據(jù)結(jié)構(gòu):
以class 10為例,span和管理的內(nèi)存如下圖所示:
spanclass為10,參照class表可得出npages=1,nelems=56,elemsize為144。其中startAddr是在span初始化時就指定了某個頁的地址。allocBits指向一個位圖,每位代表一個塊是否被分配,本例中有兩個塊已經(jīng)被分配,其allocCount也為2。next和prev用于將多個span鏈接起來,這有利于管理多個span,接下來會進(jìn)行說明。
有了管理內(nèi)存的基本單位span,還要有個數(shù)據(jù)結(jié)構(gòu)來管理span,這個數(shù)據(jù)結(jié)構(gòu)叫mcentral,各線程需要內(nèi)存時從mcentral管理的span中申請內(nèi)存,為了避免多線程申請內(nèi)存時不斷的加鎖,Golang為每個線程分配了span的緩存,這個緩存即是cache。src/runtime/mcache.go:mcache定義了cache的數(shù)據(jù)結(jié)構(gòu)
alloc為mspan的指針數(shù)組,數(shù)組大小為class總數(shù)的2倍。數(shù)組中每個宴純元素代表了一種class類型的span列表,每種class類型都有兩組span列表,第一組列表中所表示的對象中包含了指針,第二組列表中所表示的對象不含有指針,這么做是為了提高GC掃描性能,對于不包含指針的span列表,沒必要去掃描。根據(jù)對象是否包含指針,將對象分為noscan和scan兩類,其中noscan代表沒有指針,而scan則代表有指針,需要GC進(jìn)行掃描。mcache和span的對應(yīng)關(guān)系如下圖所示:
mchache在初始化時是沒有任何span的,在使用過程中會動態(tài)的從central中獲取并緩存下來,跟據(jù)使用情況,每種class的span個數(shù)也不相同。上圖所示,class 0的span數(shù)比class1的要多,說明本線程中分配的小對象要多一些。
cache作為線程的私有資源為單個線程服務(wù),而central則是全局資源,為多個線程服務(wù),當(dāng)某個線程內(nèi)存不足時會向central申請,當(dāng)某個線程釋放內(nèi)存時又會回收進(jìn)central。src/runtime/mcentral.go:mcentral定義了central數(shù)據(jù)結(jié)構(gòu):
lock: 線程間互斥鎖,防止多線程讀寫沖突
spanclass : 每個mcentral管理著一組有相同class的span列表
nonempty: 指還有內(nèi)存可用的span列表
empty: 指沒有內(nèi)存可用的span列表
nmalloc: 指累計分配的對象個數(shù)線程從central獲取span步驟如下:
將span歸還步驟如下:
從mcentral數(shù)據(jù)結(jié)構(gòu)可見,每個mcentral對象只管理特定的class規(guī)格的span。事實上每種class都會對應(yīng)一個mcentral,這個mcentral的集合存放于mheap數(shù)據(jù)結(jié)構(gòu)中。src/runtime/mheap.go:mheap定義了heap的數(shù)據(jù)結(jié)構(gòu):
lock: 互斥鎖
spans: 指向spans區(qū)域,用于映射span和page的關(guān)系
bitmap:bitmap的起始地址
arena_start: arena區(qū)域首地址
arena_used: 當(dāng)前arena已使用區(qū)域的最大地址
central: 每種class對應(yīng)的兩個mcentral
從數(shù)據(jù)結(jié)構(gòu)可見,mheap管理著全部的內(nèi)存,事實上Golang就是通過一個mheap類型的全局變量進(jìn)行內(nèi)存管理的。mheap內(nèi)存管理示意圖如下:
系統(tǒng)預(yù)分配的內(nèi)存分為spans、bitmap、arean三個區(qū)域,通過mheap管理起來。接下來看內(nèi)存分配過程。
針對待分配對象的大小不同有不同的分配邏輯:
(0, 16B) 且不包含指針的對象: Tiny分配
(0, 16B) 包含指針的對象:正常分配
[16B, 32KB] : 正常分配
(32KB, -) : 大對象分配其中Tiny分配和大對象分配都屬于內(nèi)存管理的優(yōu)化范疇,這里暫時僅關(guān)注一般的分配方法。
以申請size為n的內(nèi)存為例,分配步驟如下:
Golang內(nèi)存分配是個相當(dāng)復(fù)雜的過程,其中還摻雜了GC的處理,這里僅僅對其關(guān)鍵數(shù)據(jù)結(jié)構(gòu)進(jìn)行了說明,了解其原理而又不至于深陷實現(xiàn)細(xì)節(jié)。1、Golang程序啟動時申請一大塊內(nèi)存并劃分成spans、bitmap、arena區(qū)域
2、arena區(qū)域按頁劃分成一個個小塊。
3、span管理一個或多個頁。
4、mcentral管理多個span供線程申請使用
5、mcache作為線程私有資源,資源來源于mcentral。
go語言的map多協(xié)程訪問時需要加鎖
支持==和!=操作就可以做key,實際上只有function、map、slice三個kind不支持作為key,因為只能和nil比較不能和另一個值比較。布爾猛毀晌、整型、浮點、復(fù)數(shù)、字符串、指針、channel等都可以做key。
struct能不能做key要看每一個字段,如果所有字段都可以做key,那這個struct就可以。有一個字段不能做key,這個struct就不能余耐做key。array也是,元素類型能做key,那這個array就可以。
例如:
type Foo map[struct {
B bool
I int
F float64
C complex128
S string
P *Foo
Ch chan Foo
}]bool
每一個字段都可以做key,F(xiàn)oo就可以做key。再如:
type Foo map[struct {
Fn func() Foo
M map[*Foo]int
S []Foo
}]bool
有一個字段不能做key、Foo就不允許做key,而這三個字段都不能。
字段是遞歸檢查的:
type Foo map[struct {
Sub struct {
M map[*Foo]bool
}
}]bool
Sub的M字段不能做key,Sub就枝鋒不能做key,F(xiàn)oo也就不能做key。
總之想把一個數(shù)據(jù)結(jié)構(gòu)用于map的key,就不能包含function、map和slice。
摘要: 一、前言 go語言類似Java JUC包也提供了一些列用于多線程之間進(jìn)行同步的措施,比如低級的同步措施有 鎖、CAS、原銷凱子變量操作類。相比Java來說go提供了獨特的基于通道的同步措施。本節(jié)我們先來看看go中枝殲CAS操作 二、CAS操作 go中的Cas操作與java中類似,都是借用了CPU提供的原子性指令來實現(xiàn)。
go語言類似Java JUC包也提供了一些列用于多線程之間進(jìn)行同步的措施,比如低級的同步措施有 鎖、CAS、原子變量操作類。相比Java來說go提供了獨特的基于通道的同步措施。本節(jié)我們先來看看go中虧搭喚CAS操作
go中的Cas操作與java中類似,都是借用了CPU提供的原子性指令來實現(xiàn)。CAS操作修改共享變量時候不需要對共享變量加鎖,而是通過類似樂觀鎖的方式進(jìn)行檢查,本質(zhì)還是不斷的占用CPU 資源換取加鎖帶來的開銷(比如上下文切換開銷)。下面一個例子使用CAS來實現(xiàn)計數(shù)器
go中CAS操作具有原子性,在解決多線程操作共享變量安全上可以有效的減少使用鎖所帶來的開銷,但是這是使用cpu資源做交換的。
我簡單列舉了并發(fā)編程的大綱,需要詳細(xì)的私信“555”~~