Go語(yǔ)言內(nèi)存管理與布局

內(nèi)存管理

1布局

1.1操作系統(tǒng)內(nèi)存布局

1.1.1邏輯布局

1.1.2物理布局

1.2 GO 內(nèi)存布局

go沒有使用操作系統(tǒng)提供的內(nèi)存管理方案,而是自己實(shí)現(xiàn)了一套管理機(jī)制虚汛,其整體布局如下:


1.2.1 Arena

arena區(qū)域就是我們所謂的堆區(qū)卵迂,Go動(dòng)態(tài)分配的內(nèi)存都是在這個(gè)區(qū)域底扳,它把內(nèi)存分割成8KB大小的頁(yè)寂嘉,一些頁(yè)組合起來稱為mspan顾复。

1.2.2 Bitmap

bitmap區(qū)域標(biāo)識(shí)arena區(qū)域哪些地址保存了對(duì)象班挖,并且用4bit標(biāo)志位表示對(duì)象是否包含指針、GC標(biāo)記信息芯砸。bitmap中一個(gè)byte大小的內(nèi)存對(duì)應(yīng)arena區(qū)域中4個(gè)指針大邢糗健(指針大小為 8B )的內(nèi)存,所以bitmap區(qū)域的大小是512GB/(4*8B)=16GB假丧。


1.2.3 Spans

Spans存放指向mspan的指針
Mspanarena管理和分配的基本單元双揪,指向Arena中一段連續(xù)的頁(yè)
Go里mspan的Size Class共有67種,每種mspan分割的object大小是8*2n的倍數(shù)包帚,這些class不是指數(shù)式翻倍的渔期,因?yàn)榇髩K連續(xù)內(nèi)存空間很少見,會(huì)導(dǎo)致各class的mspan數(shù)量不均衡渴邦,這個(gè)是寫死在代碼里的:

// path: /usr/local/go/src/runtime/sizeclasses.go

const _NumSizeClasses = 67

var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}
var class_to_size = [_NumSizeClasses] uint16{ 0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
Mcache

每個(gè)工作線程P都會(huì)綁定一個(gè)mcache疯趟,本地緩存可用的mspan資源,這樣就可以直接給Goroutine分配谋梭,因?yàn)椴淮嬖诙鄠€(gè)Goroutine競(jìng)爭(zhēng)的情況信峻,所以不會(huì)消耗鎖資源。
mcache在初始化的時(shí)候是沒有任何mspan資源的瓮床,在使用過程中會(huì)動(dòng)態(tài)地從mcentral申請(qǐng)盹舞,之后會(huì)緩存下來产镐。

Mcentral

為所有mcache提供切分好的mspan資源。每個(gè)central保存一種特定大小的全局mspan列表矾策,包括已分配出去的和未分配出去的磷账。
每個(gè)mcentral對(duì)應(yīng)一種mspan,而mspan的種類導(dǎo)致它分割的object大小不同贾虽。當(dāng)工作線程的mcache中沒有合適(也就是特定大小的)的mspan時(shí)就會(huì)從mcentral獲取逃糟。
noscan是沒有引用外部對(duì)象,GC時(shí)無需掃描的mspan鏈表


mcentral維護(hù)兩個(gè)雙向鏈表蓬豁,nonempty表示有空閑對(duì)象的鏈表绰咽,empty表示沒有空閑對(duì)象或 span 已經(jīng)被 mcache 緩存的 span 鏈表。這兩個(gè)變量名和實(shí)際的用途是反的地粪。

//go:notinheap
type mcentral struct {
    lock      mutex
    sizeclass int32
    nonempty  mSpanList // list of spans with a free object, ie a nonempty free list
    empty     mSpanList // list of spans with no free objects (or cached in an mcache)
}

這個(gè)問題直到1.16版本才被修復(fù):

// Central list of free objects of a given size.
//
//go:notinheap
type mcentral struct {
    spanclass spanClass

    // partial and full contain two mspan sets: one of swept in-use
    // spans, and one of unswept in-use spans. These two trade
    // roles on each GC cycle. The unswept set is drained either by
    // allocation or by the background sweeper in every GC cycle,
    // so only two roles are necessary.
    //
    // sweepgen is increased by 2 on each GC cycle, so the swept
    // spans are in partial[sweepgen/2%2] and the unswept spans are in
    // partial[1-sweepgen/2%2]. Sweeping pops spans from the
    // unswept set and pushes spans that are still in-use on the
    // swept set. Likewise, allocating an in-use span pushes it
    // on the swept set.
    //
    // Some parts of the sweeper can sweep arbitrary spans, and hence
    // can't remove them from the unswept set, but will add the span
    // to the appropriate swept list. As a result, the parts of the
    // sweeper and mcentral that do consume from the unswept list may
    // encounter swept spans, and these should be ignored.
    partial [2]spanSet // list of spans with a free object
    full    [2]spanSet // list of spans with no free objects
}

并且還更新了spans的存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)為spanSet

// Central list of free objects of a given size.
//
//go:notinheap
type mcentral struct {
    spanclass spanClass

    // partial and full contain two mspan sets: one of swept in-use
    // spans, and one of unswept in-use spans. These two trade
    // roles on each GC cycle. The unswept set is drained either by
    // allocation or by the background sweeper in every GC cycle,
    // so only two roles are necessary.
    //
    // sweepgen is increased by 2 on each GC cycle, so the swept
    // spans are in partial[sweepgen/2%2] and the unswept spans are in
    // partial[1-sweepgen/2%2]. Sweeping pops spans from the
    // unswept set and pushes spans that are still in-use on the
    // swept set. Likewise, allocating an in-use span pushes it
    // on the swept set.
    //
    // Some parts of the sweeper can sweep arbitrary spans, and hence
    // can't remove them from the unswept set, but will add the span
    // to the appropriate swept list. As a result, the parts of the
    // sweeper and mcentral that do consume from the unswept list may
    // encounter swept spans, and these should be ignored.
    partial [2]spanSet // list of spans with a free object
    full    [2]spanSet // list of spans with no free objects
}
Mheap

代表Go程序持有的所有堆空間取募,Go程序使用一個(gè)mheap的全局對(duì)象_mheap來管理堆內(nèi)存
當(dāng)mcentral沒有空閑的mspan時(shí),會(huì)向mheap申請(qǐng)蟆技。而mheap沒有資源時(shí)玩敏,會(huì)向操作系統(tǒng)申請(qǐng)新內(nèi)存。mheap主要用于大對(duì)象的內(nèi)存分配质礼,以及管理未切割的mspan旺聚,用于給mcentral切割成小對(duì)象。
同時(shí)我們也看到眶蕉,mheap中含有所有規(guī)格的mcentral砰粹,所以,當(dāng)一個(gè)mcachemcentral申請(qǐng)mspan時(shí)造挽,只需要在獨(dú)立的mcentral中使用鎖碱璃,并不會(huì)影響申請(qǐng)其他規(guī)格的mspan
heapcentral數(shù)組尺寸是134饭入,因?yàn)槊糠Nspanclass又細(xì)分為scannoscan嵌器,scan表示該mspan中的對(duì)象包含指針,需要進(jìn)行GC掃描等管理操作圣拄,noscan表示對(duì)象中都是非引用類型嘴秸,不需要進(jìn)行掃描。

2堆管理

2.1 特點(diǎn)

  • 每次從操作系統(tǒng)申請(qǐng)一大塊的內(nèi)存庇谆,由Go來對(duì)這塊內(nèi)存做分配,減少系統(tǒng)調(diào)用
  • 內(nèi)存分配算法采用Google的TCMalloc算法凭疮。其核心思想就是把內(nèi)存切分的非常的細(xì)小饭耳,分為多級(jí)管理,以降低鎖的粒度
  • 回收對(duì)象內(nèi)存時(shí)执解,并沒有將其真正釋放掉寞肖,只是放回預(yù)先分配的大塊內(nèi)存中纲酗,以便復(fù)用。只有內(nèi)存閑置過多的時(shí)候新蟆,才會(huì)嘗試歸還部分內(nèi)存給操作系統(tǒng)觅赊,降低整體開銷
  • Go內(nèi)存管理追求低延遲,適合高并發(fā)IO密集場(chǎng)景琼稻,高吞吐量用Java吮螺,高性能用C

2.2 分配

  • 32KB 的對(duì)象,直接從mheap上分配帕翻;
  • <=16B 且沒有指針的對(duì)象使用mcache的tiny分配器分配鸠补;
  • (16B,32KB] 的對(duì)象,首先計(jì)算對(duì)象的規(guī)格大小嘀掸,然后使用mcache中相應(yīng)規(guī)格大小的mspan分配紫岩;
  • 如果mcache沒有相應(yīng)規(guī)格大小的mspan,則向mcentral申請(qǐng)
    ○ 如果當(dāng)前的span中并沒有可以使用的元素睬塌,這時(shí)就需要從mcentral中加鎖查找泉蝌。之前介紹過,在mcentral中有兩種類型的span鏈表揩晴,分別是有空閑元素的nonempty勋陪,以及沒有空閑元素的empty鏈表。會(huì)分別遍歷這兩個(gè)列表,查找是否有可用的span文狱。這是由于有些span可能已經(jīng)被垃圾回收器標(biāo)記為空閑了粥鞋,只是還沒有來得及清理。這些Span在清掃后仍然是可以使用的瞄崇,因此需要遍歷呻粹。
    ○ 如果在mcentral元素中查找到有空閑元素的span,則將其賦值到mcache中苏研,并更新allocCache等浊,同時(shí)還需要將span添加到mcentralempty鏈表中去。
  • 如果mcentral沒有相應(yīng)規(guī)格大小的mspan摹蘑,則向mheap申請(qǐng)
    ○ 當(dāng)要分配的page過大或者在邏輯處理器P的cache中沒有找到可用的頁(yè)數(shù)時(shí)筹燕,就需要對(duì)mheap加鎖,并在整個(gè)mheap管理的虛擬地址空間的位圖中查找是否有可用的pages衅鹿。而且其在本質(zhì)上涉及到Go語(yǔ)言是如何對(duì)線性的地址空間進(jìn)行位圖管理的撒踪。
    ○ 管理線性的地址空間的位圖結(jié)構(gòu)叫做基數(shù)樹(radix tree), 他和一般的基數(shù)樹結(jié)構(gòu)有點(diǎn)不太一樣大渤,這個(gè)名字很大一部分是由于父節(jié)點(diǎn)包含了子節(jié)點(diǎn)的若干信息制妄。
  • 如果mheap中也沒有合適大小的mspan,則向操作系統(tǒng)申請(qǐng)
    ○ 每一次向操作系統(tǒng)申請(qǐng)內(nèi)存時(shí)泵三,Go語(yǔ)言規(guī)定必須為heapArena 大小的倍數(shù)耕捞。heapArena是和平臺(tái)有關(guān)的內(nèi)存大小衔掸,在unix 64位系統(tǒng)中,其大小為64M俺抽。這意味著即便需要的內(nèi)存大小很少敞映,最終也至少向操作系統(tǒng)申請(qǐng)64M。多申請(qǐng)的內(nèi)存可以用于下次分配使用磷斧。

2.3 回收

2.3.1 STW(Go < 1.3)


go runtime在一定條件下(內(nèi)存超過閾值振愿、達(dá)到GC周期時(shí)間2min或者執(zhí)行runtime.GC)會(huì)觸發(fā)GC
首先啟動(dòng)垃圾回收goroutine,這個(gè)goroutine會(huì)將其他goroutine執(zhí)行到安全點(diǎn)后停止



停止后將P與M全部解綁



再把所有Goroutine放到全局調(diào)度隊(duì)列中

2.3.2 STW + 并發(fā)清除(Go < 1.5)

Mark完成后馬上就重新啟動(dòng)被暫停的任務(wù)了瞳抓,而是讓sweep任務(wù)和普通協(xié)程任務(wù)一樣并行的和其他任務(wù)一起執(zhí)行


2.3.3 三色標(biāo)記+插入寫屏障(Go < 1.8)

插入寫屏障重掃階段需要棧區(qū)進(jìn)行STW(Java CMS)埃疫,刪除寫屏障則需要在初始階段STW并生成快照SATB(Java G1),并且Go的棧不支持做寫屏障



2.3.4


Go 1.8采用一種混合的 write barrier 方式 (Yuasa-style deletion write barrier [Yuasa ‘90] 和 Dijkstra-style insertion write barrier [Dijkstra ‘78])來避免堆棧重新掃描孩哑,優(yōu)點(diǎn)如下:

3 棧管理

3.1 特點(diǎn)

在Go應(yīng)用程序運(yùn)行時(shí)横蜒,每個(gè)goroutine都維護(hù)著一個(gè)自己的棧區(qū)胳蛮,這個(gè)棧區(qū)只能自己使用不能被其他goroutine使用。棧區(qū)的初始大小是2KB(比x86_64架構(gòu)下線程的默認(rèn)棧2M要小很多)丛晌,在goroutine運(yùn)行的時(shí)候棧區(qū)會(huì)按照需要增長(zhǎng)和收縮仅炊,占用的內(nèi)存最大限制的默認(rèn)值在64位系統(tǒng)上是1GB。棧大小的初始值和上限這部分的設(shè)置都可以在Go的源碼runtime/stack.go里找到澎蛛。

type g struct {
 stack       stack
  ...
}
 
type stack struct {
 lo uintptr
 hi uintptr
}

其實(shí)棧內(nèi)存空間抚垄、結(jié)構(gòu)和初始大小在最開始并不是2KB,也是經(jīng)過了幾個(gè)版本的更迭:

  • v1.0 ~ v1.1 — 最小棧內(nèi)存空間為 4KB谋逻;
  • v1.2 — 將最小棧內(nèi)存提升到了 8KB呆馁;
  • v1.3 — 使用連續(xù)棧替換之前版本的分段棧;
  • v1.4 — 將最小棧內(nèi)存降低到了 2KB毁兆;

函數(shù)棧中包含:

  • 入?yún)⒊鰠⑿畔?/li>
  • 調(diào)用函數(shù)信息
  • 函數(shù)返回地址
  • 局部變量浙滤、常量
  • 寄存器信息

3.1.1 全局棧緩存

棧空間在運(yùn)行時(shí)中包含兩個(gè)重要的全局變量气堕,分別是 runtime.stackpoolruntime.stackLarge纺腊,這兩個(gè)變量分別表示全局的棧緩存和大棧緩存,前者可以分配小于 32KB 的內(nèi)存茎芭,后者用來分配大于 32KB 的椧灸ぃ空間:

 // Number of orders that get caching. Order 0 is FixedStack
 // and each successive order is twice as large.
 // We want to cache 2KB, 4KB, 8KB, and 16KB stacks. Larger stacks
 // will be allocated directly.
 // Since FixedStack is different on different systems, we
 // must vary NumStackOrders to keep the same maximum cached size.
 //   OS               | FixedStack | NumStackOrders
 //   -----------------+------------+---------------
 //   linux/darwin/bsd | 2KB        | 4
 //   windows/32       | 4KB        | 3
 //   windows/64       | 8KB        | 2
 //   plan9            | 4KB        | 3
_NumStackOrders = 4 - sys.PtrSize/4*sys.GoosWindows - 1*sys.GoosPlan9
 
var stackpool [_NumStackOrders]mSpanList
 
type stackpoolItem struct {
 mu   mutex
 span mSpanList
}
 
var stackLarge struct {
 lock mutex
 free [heapAddrBits - pageShift]mSpanList
}
 
//go:notinheap
type mSpanList struct {
 first *mspan // first span in list, or nil if none
 last  *mspan // last span in list, or nil if none
}

可以看到這兩個(gè)用于分配空間的全局變量都與內(nèi)存管理單元 runtime.mspan 有關(guān),所以我們棧內(nèi)容的申請(qǐng)也和堆的申請(qǐng)相似梅桩,是先去當(dāng)前線程的對(duì)應(yīng)尺寸的mcache里去申請(qǐng)次氨,不夠的時(shí)候mache會(huì)從全局的mcental里取內(nèi)存等等。
其實(shí)從調(diào)度器和內(nèi)存分配的角度來看摘投,如果運(yùn)行時(shí)只使用全局變量來分配內(nèi)存的話煮寡,勢(shì)必會(huì)造成線程之間的鎖競(jìng)爭(zhēng)進(jìn)而影響程序的執(zhí)行效率,棧內(nèi)存由于與線程關(guān)系比較密切犀呼,所以在每一個(gè)線程緩存 runtime.mcache 中都加入了棧緩存減少鎖競(jìng)爭(zhēng)影響幸撕。

type mcache struct {
  ...
  alloc [numSpanClasses]*mspan
  
 stackcache [_NumStackOrders]stackfreelist
  ...
}
 
type stackfreelist struct {
 list gclinkptr
 size uintptr
}

3.2 分配

3.2.1 分段棧(Go < 1.3)

隨著goroutine 調(diào)用的函數(shù)層級(jí)的深入或者局部變量需要的越來越多時(shí),運(yùn)行時(shí)會(huì)調(diào)用 runtime.morestackruntime.newstack創(chuàng)建一個(gè)新的椡獗郏空間坐儿,這些棧空間是不連續(xù)的宋光,但是當(dāng)前 goroutine 的多個(gè)椕部螅空間會(huì)以雙向鏈表的形式串聯(lián)起來,運(yùn)行時(shí)會(huì)通過指針找到連續(xù)的棧片段.

分段棧雖然能夠按需為當(dāng)前 goroutine 分配內(nèi)存并且及時(shí)減少內(nèi)存的占用罪佳,但是它也存在一個(gè)比較大的問題:如果當(dāng)前 goroutine 的棧幾乎充滿逛漫,那么任意的函數(shù)調(diào)用都會(huì)觸發(fā)棧的擴(kuò)容,當(dāng)函數(shù)返回后又會(huì)觸發(fā)棧的收縮赘艳,如果在一個(gè)循環(huán)中調(diào)用函數(shù)酌毡,棧的分配和釋放就會(huì)造成巨大的額外開銷,這被稱為\color{red}{熱分裂問題(Hot split)}蕾管。

為了解決這個(gè)問題枷踏,Go在1.2版本的時(shí)候不得不將棧的初始化內(nèi)存從4KB增大到了8KB。后來把采用連續(xù)棧結(jié)構(gòu)后掰曾,又把初始棧大小減小到了2KB旭蠕。

3.2.2 連續(xù)棧(Go >= 1.3)

連續(xù)棧可以解決分段棧中存在的兩個(gè)問題旷坦,其核心原理就是每當(dāng)程序的椞桶荆空間不足時(shí),初始化一片比舊棧大兩倍的新棧并將原棧中的所有值都遷移到新的棧中塞蹭,新的局部變量或者函數(shù)調(diào)用就有了充足的內(nèi)存空間孽江。使用連續(xù)棧機(jī)制時(shí),椃纾空間不足導(dǎo)致的擴(kuò)容會(huì)經(jīng)歷以下幾個(gè)步驟:

  • 調(diào)用runtime.newstack在內(nèi)存空間中分配更大的棧內(nèi)存空間岗屏,舊棧的大小是通過我們上面說的保存在goroutine中的stack信息里記錄的棧區(qū)內(nèi)存邊界計(jì)算出來的,然后用舊棧兩倍的大小創(chuàng)建新棧漱办,創(chuàng)建前會(huì)檢查是新棧的大小是否超過了單個(gè)棧的內(nèi)存上限这刷;
  • 如果目標(biāo)棧的大小沒有超出程序的限制,會(huì)將 goroutine 切換至 _Gcopystack 狀態(tài)并調(diào)用 runtime.copystack 開始棧的拷貝娩井,在拷貝棧的內(nèi)存之前暇屋,運(yùn)行時(shí)會(huì)先通過runtime.stackalloc 函數(shù)分配新的棧空間洞辣;
  • 將指向舊棧對(duì)應(yīng)變量的指針重新指向新棧咐刨;
  • 調(diào)用runtime.stackfree銷毀并回收舊棧的內(nèi)存空間昙衅。

copystack會(huì)把舊棧里的所有內(nèi)容拷貝到新棧里然后調(diào)整所有指向舊棧的變量的指針指向到新棧, 我們可以用下面這個(gè)程序驗(yàn)證下定鸟,棧擴(kuò)容后同一個(gè)變量的內(nèi)存地址會(huì)發(fā)生變化而涉。

package main
 
func main() {
 var x [10]int
 println(&x)
 a(x)
 println(&x)
}
 
//go:noinline
func a(x [10]int) {
 println(`func a`)
 var y [100]int
 b(y)
}
 
//go:noinline
func b(x [100]int) {
 println(`func b`)
 var y [1000]int
 c(y)
}
 
//go:noinline
func c(x [1000]int) {
 println(`func c`)
}

程序的輸出可以看到在棧擴(kuò)容前后,變量x的內(nèi)存地址的變化:

0xc000030738
...
...
0xc000081f38

3.3 回收

在goroutine運(yùn)行的過程中联予,如果棧區(qū)的空間使用率不超過1/4啼县,那么在垃圾回收的時(shí)候使用runtime.shrinkstack進(jìn)行棧縮容沸久,當(dāng)然進(jìn)行縮容前會(huì)執(zhí)行一堆前置檢查季眷,都通過了才會(huì)進(jìn)行縮容:

func shrinkstack(gp *g) {
 ...
 oldsize := gp.stack.hi - gp.stack.lo
 newsize := oldsize / 2
 if newsize < _FixedStack {
  return
 }
 avail := gp.stack.hi - gp.stack.lo
 if used := gp.stack.hi - gp.sched.sp + _StackLimit; used >= avail/4 {
  return
 }
 
 copystack(gp, newsize)
}

如果要觸發(fā)棧的縮容,新棧的大小會(huì)是原始棧的一半卷胯,不過如果新棧的大小低于程序的最低限制 2KB子刮,那么縮容的過程就會(huì)停止∷薪撸縮容也會(huì)調(diào)用擴(kuò)容時(shí)使用的 runtime.copystack 函數(shù)開辟新的椈案妫空間,將舊棧的數(shù)據(jù)拷貝到新棧以及調(diào)整原來指針的指向卵慰。

在下面的例子里沙郭,當(dāng)main函數(shù)里的其他函數(shù)執(zhí)行完后,只有main函數(shù)還在棧區(qū)的空間里裳朋,如果這個(gè)時(shí)候系統(tǒng)進(jìn)行垃圾回收就會(huì)對(duì)這個(gè)goroutine的棧區(qū)進(jìn)行縮容病线。在這里我們可以在程序里通過調(diào)用runtime.GC,強(qiáng)制系統(tǒng)進(jìn)行垃圾回收鲤嫡,來試驗(yàn)看一下椝吞簦縮容的過程和效果:

func main() {
   var x [10]int
   println(&x)
   a(x)
   runtime.GC()
   println(&x)
}

如果要觸發(fā)棧的縮容,新棧的大小會(huì)是原始棧的一半暖眼,不過如果新棧的大小低于程序的最低限制 2KB诫肠,那么縮容的過程就會(huì)停止栋豫。縮容也會(huì)調(diào)用擴(kuò)容時(shí)使用的 runtime.copystack 函數(shù)開辟新的椢Х剩空間怨愤,將舊棧的數(shù)據(jù)拷貝到新棧以及調(diào)整原來指針的指向篮愉。

在下面的例子里猪勇,當(dāng)main函數(shù)里的其他函數(shù)執(zhí)行完后,只有main函數(shù)還在棧區(qū)的空間里,如果這個(gè)時(shí)候系統(tǒng)進(jìn)行垃圾回收就會(huì)對(duì)這個(gè)goroutine的棧區(qū)進(jìn)行縮容掀泳。在這里我們可以在程序里通過調(diào)用runtime.GC藕畔,強(qiáng)制系統(tǒng)進(jìn)行垃圾回收韭邓,來試驗(yàn)看一下棧縮容的過程和效果:

func main() {
   var x [10]int
   println(&x)
   a(x)
   runtime.GC()
   println(&x)
}

修改源碼文件runtime.stack.go,把常量stackDebug的值修改為1袜茧,執(zhí)行命令go build -gcflags -S main.go后會(huì)看到類似下面的輸出:

...
shrinking stack 32768->16384
stackalloc 16384
  allocated 0xc000076000
copystack gp=0xc000000180 [0xc00007a000 0xc000081e60 0xc000082000] -> [0xc000076000 0xc000079e60 0xc00007a000]/16384
...

3.4 問題

問:局部變量什么時(shí)候分配在堆上俺夕,什么時(shí)候在棧上?
答:這個(gè)由編譯器決定,和具體的語(yǔ)法無關(guān)捌议,各版本golang的實(shí)現(xiàn)也有差異轿曙,golang刻意弱化了堆和棧的概念。
Golang 編譯器會(huì)將函數(shù)的局部變量分配到函數(shù)棧幀(stack frame)上。然而悦陋,如果編譯器不能確保變量在函數(shù) return 之后不再被引用(逃逸分析)幸逆,編譯器就會(huì)將變量分配到堆上。而且,如果一個(gè)局部變量非常大还绘,那么它也應(yīng)該被分配到堆上而不是棧上楚昭。
比如這段代碼,就不會(huì)在堆上分配內(nèi)存拍顷,即使我們用new分配:

const Width, Height = 640, 480
type Cursor struct {
    X, Y int
}
 
func Center(c *Cursor) {
    c.X += Width / 2
    c.Y += Height / 2
}
 
func CenterCursor() {
    c := new(Cursor)
    Center(c)
    fmt.Println(c.X, c.Y)
}

驗(yàn)證結(jié)果如下:

go tool compile -m test.go
 
test.go:17: can inline Center
test.go:24: inlining call to Center
test.go:25: c.X escapes to heap
test.go:25: c.Y escapes to heap
test.go:23: CenterCursor new(Cursor) does not escape
test.go:25: CenterCursor ... argument does not escape
test.go:17: Center c does not escape

這段代碼則會(huì)在堆上分配對(duì)象:

package main
 
import (
    "fmt"
)
 
func main() {
    var a [1]int
    c := a[:]
    fmt.Println(c)
}

匯編代碼有調(diào)用newobject抚太,其中test.go:8說明變量a的內(nèi)存是在堆上分配的:

go tool compile -S test.golang
 
"".main t=1 size=336 value=0 args=0x0 locals=0x98
    0x0000 00000 (test.go:7)    TEXT    "".main(SB), $152-0
    0x0000 00000 (test.go:7)    MOVQ    (TLS), CX
    0x0009 00009 (test.go:7)    LEAQ    -24(SP), AX
    0x000e 00014 (test.go:7)    CMPQ    AX, 16(CX)
    0x0012 00018 (test.go:7)    JLS 320
    0x0018 00024 (test.go:7)    SUBQ    $152, SP
    0x001f 00031 (test.go:7)    FUNCDATA    $0, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB)
    0x001f 00031 (test.go:7)    FUNCDATA    $1, gclocals·6e96661712a005168eba4ed6774db961(SB)
    0x001f 00031 (test.go:8)    LEAQ    type.[1]int(SB), BX
    0x0026 00038 (test.go:8)    MOVQ    BX, (SP)
    0x002a 00042 (test.go:8)    PCDATA  $0, $0
    0x002a 00042 (test.go:8)    CALL    runtime.newobject(SB)
    0x002f 00047 (test.go:8)    MOVQ    8(SP), AX
    0x0034 00052 (test.go:9)    CMPQ    AX, $0
    0x0038 00056 (test.go:9)    JEQ $1, 313
    0x003e 00062 (test.go:9)    MOVQ    $1, DX
    0x0045 00069 (test.go:9)    MOVQ    $1, CX

3.5 閉包

  1. Go語(yǔ)言支持閉包
  2. \color{red}{Go語(yǔ)言能通過逃逸分析(escape analyze)識(shí)別出變量的作用域,自動(dòng)將變量在堆上分配昔案。將閉包環(huán)境變量在堆上分配是Go實(shí)現(xiàn)閉包的基礎(chǔ)}
  3. 返回閉包時(shí)并不是單純返回一個(gè)函數(shù)尿贫,而是返回了一個(gè)結(jié)構(gòu)體,記錄下函數(shù)返回地址和引用的環(huán)境中的變量地址

4 Runtime與Debug

runtime.GC函數(shù)

會(huì)讓運(yùn)行時(shí)系統(tǒng)進(jìn)行一次強(qiáng)制性的垃圾收集:

  1. 強(qiáng)制的垃圾回收:不管怎樣爱沟,都要進(jìn)行的垃圾回收帅霜。
  2. 非強(qiáng)制的垃圾回收:只會(huì)在一定條件下進(jìn)行的垃圾回收(即運(yùn)行時(shí),系統(tǒng)自上次垃圾回收之后新申請(qǐng)的堆內(nèi)存的單元(也成為單元增量)達(dá)到指定的數(shù)值)呼伸。
debug.SetGCPercent函數(shù)

用于設(shè)置一個(gè)比率(垃圾收集比率),前面所說的單元增量與前一次垃圾收集時(shí)的碎內(nèi)存的單元數(shù)量和此垃圾手機(jī)比率有關(guān)钝尸。
<觸發(fā)垃圾收集的堆內(nèi)存單元增量>=<上一次垃圾收集完的堆內(nèi)存單元數(shù)量>*(<垃圾收集比率>/100)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末括享,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子珍促,更是在濱河造成了極大的恐慌铃辖,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件猪叙,死亡現(xiàn)場(chǎng)離奇詭異娇斩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)穴翩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門犬第,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人芒帕,你說我怎么就攤上這事歉嗓。” “怎么了背蟆?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵鉴分,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我带膀,道長(zhǎng)志珍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任垛叨,我火速辦了婚禮伦糯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己舔株,他們只是感情好莺琳,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著载慈,像睡著了一般惭等。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上办铡,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天辞做,我揣著相機(jī)與錄音,去河邊找鬼寡具。 笑死秤茅,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的童叠。 我是一名探鬼主播框喳,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼厦坛!你這毒婦竟也來了五垮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤杜秸,失蹤者是張志新(化名)和其女友劉穎放仗,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撬碟,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡诞挨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了呢蛤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惶傻。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖顾稀,靈堂內(nèi)的尸體忽然破棺而出达罗,到底是詐尸還是另有隱情,我是刑警寧澤静秆,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布乍钻,位于F島的核電站沮尿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜韵卤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一款咖、第九天 我趴在偏房一處隱蔽的房頂上張望衬衬。 院中可真熱鬧,春花似錦狱从、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至誉察,卻和暖如春与涡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背持偏。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工驼卖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鸿秆。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓酌畜,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親卿叽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子桥胞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 這篇文章可以看作是內(nèi)存管理這篇長(zhǎng)文的學(xué)習(xí)總結(jié)吧,原文基于源碼剖析了整個(gè)go的內(nèi)存管理考婴,非常詳盡埠戳。 程序中的數(shù)據(jù)和變...
    彥幀閱讀 1,082評(píng)論 0 0
  • 1. 內(nèi)存分配步驟 go 給對(duì)象分配內(nèi)存的主要流程: object size > 32K,則使用 mheap 直接...
    斜不靠譜閱讀 250評(píng)論 0 0
  • Go語(yǔ)言內(nèi)置運(yùn)行時(shí)(就是runtime)蕉扮,拋棄了傳統(tǒng)的內(nèi)存分配方式,改為自主管理颗圣。這樣可以自主地實(shí)現(xiàn)更好的內(nèi)存使用...
    ddu_sw閱讀 1,368評(píng)論 0 5
  • 從源碼角度看Golang的堆內(nèi)存管理 本章主要從源碼角度針對(duì)Go堆上的內(nèi)存管理進(jìn)行分析喳钟。僅關(guān)注linux系統(tǒng)下的邏...
    thinkboy234閱讀 2,513評(píng)論 0 6
  • 接上一篇,下面來看看內(nèi)存分配的初始化在岂、分配等奔则。 初始化 首先會(huì)申請(qǐng)一段連續(xù)的內(nèi)存空間以供使用,大小(64位機(jī)器上)...
    鄒志全閱讀 431評(píng)論 0 3