深入淺出內(nèi)存管理:空間分配及逃逸分析
1. 引言
內(nèi)存管理,是開發(fā)者在程序編寫和調(diào)優(yōu)的過(guò)程中不可繞開的話題,也是走向資深程序員必須要了解的計(jì)算機(jī)知識(shí)。
有經(jīng)驗(yàn)的面試官會(huì)從內(nèi)存管理的掌握程度去考察一個(gè)候選人的技術(shù)水平,這里面涉及到的知識(shí)可能包括操作系統(tǒng)、計(jì)算機(jī)組成原理以及編程語(yǔ)言的底層實(shí)現(xiàn)等。
說(shuō)到內(nèi)存,其實(shí)就是存儲(chǔ)器,我們可以從馮.諾依曼的計(jì)算機(jī)結(jié)構(gòu)來(lái)了解存儲(chǔ)器的概念:
圖片
什么?馮諾依曼你都不知道,是不是和我一樣,計(jì)算機(jī)基礎(chǔ)的課程沒(méi)有好好學(xué)呀?
別急!接下來(lái)我們由淺入深講到的內(nèi)容,就算不了解計(jì)算機(jī)底層原理的同學(xué)也可以弄懂,一起接著往下看吧~
總之,存儲(chǔ)器是計(jì)算機(jī)中不可或缺的一部分,內(nèi)存管理,其實(shí)就是對(duì)存儲(chǔ)器的存儲(chǔ)空間管理。
接下來(lái),我們會(huì)從內(nèi)存分類、以及 Go 語(yǔ)言的內(nèi)存空間分配上,結(jié)合常見(jiàn)的逃逸分析場(chǎng)景,來(lái)學(xué)習(xí)內(nèi)存管理相關(guān)的知識(shí)。
2. 虛擬內(nèi)存
2.1 虛擬內(nèi)存和物理內(nèi)存的區(qū)別
我們都知道,以前的計(jì)算機(jī)存儲(chǔ)器空間很小,我們?cè)谶\(yùn)行計(jì)算機(jī)程序的時(shí)候物理尋址的范圍非常有限。
比如,在 32 位的機(jī)器上,尋址范圍只有 2 的 32 次方,也就是 4G。
并且,對(duì)于程序來(lái)說(shuō),這是固定的,我們可以想象一下,如果每開一個(gè)計(jì)算機(jī)進(jìn)程就給它們分配 4G 的物理內(nèi)存,那資源消耗就太大了。
圖片
資源的利用率也是一個(gè)巨大的問(wèn)題,沒(méi)有分配到資源的進(jìn)程就只能等待,當(dāng)一個(gè)進(jìn)程結(jié)束以后再把等待的進(jìn)程裝入內(nèi)存,而這種頻繁地裝入內(nèi)存操作效率也很低。
并且,由于指令都是可以訪問(wèn)物理內(nèi)存的,那么任何進(jìn)程都可以修改內(nèi)存中其它進(jìn)程的數(shù)據(jù),甚至修改內(nèi)核地址空間的數(shù)據(jù),這是非常不安全的。
由于物理內(nèi)存使用時(shí),資源消耗大、利用率低及不安全的問(wèn)題。因此,引入了虛擬內(nèi)存。
虛擬內(nèi)存是計(jì)算機(jī)系統(tǒng)內(nèi)存管理的一種技術(shù),通過(guò)分配虛擬的邏輯內(nèi)存地址,讓每個(gè)應(yīng)用程序都認(rèn)為自己擁有連續(xù)可用的內(nèi)存空間。
而實(shí)際上,這些內(nèi)存空間通常是被分隔開的多個(gè)物理內(nèi)存碎片,還有部分暫時(shí)存儲(chǔ)在外部磁盤存儲(chǔ)器上,在需要時(shí)進(jìn)行數(shù)據(jù)交換。
2.2 虛擬內(nèi)存轉(zhuǎn)換
既然計(jì)算機(jī)用到的都是虛擬內(nèi)存,那我們?nèi)绾文玫秸鎸?shí)的物理內(nèi)存地址呢?答案就是內(nèi)存映射,即如何把虛擬地址(又被稱作邏輯地址)轉(zhuǎn)換成物理地址。
圖片
在 Linux 操作系統(tǒng)下,內(nèi)存最先有兩種管理方式,分別是頁(yè)式存儲(chǔ)管理和段式存儲(chǔ)管理,其中:
- 頁(yè)式存儲(chǔ)能有效地解決內(nèi)存碎片,提高內(nèi)存利用率;
- 分段式存儲(chǔ)管理能反映程序的邏輯結(jié)構(gòu),并有利于段的共享;
通俗來(lái)講就是內(nèi)存有兩種單位,一種是分頁(yè),一種是分段。分頁(yè)就是把整個(gè)虛擬和物理內(nèi)存空間切割成很多塊固定尺寸的大小,虛擬地址和物理地址間通過(guò)頁(yè)表來(lái)進(jìn)行映射:
圖片
分頁(yè)內(nèi)存都是預(yù)先劃分好的,所以不會(huì)產(chǎn)生間隙非常小的內(nèi)存碎片,分配時(shí)利用率比較高。
而分段就不一樣了,它是基于程序的邏輯來(lái)分段的,由于程序?qū)傩钥赡艽蟛幌嗤?,所以分段的大小也?huì)大小不一。
分段管理時(shí),虛擬地址和物理地址間通過(guò)段表來(lái)進(jìn)行映射:
圖片
不難發(fā)現(xiàn),分段內(nèi)存管理的切分不是均勻的,而是根據(jù)不同的程序所占用的內(nèi)存來(lái)分配。
這樣帶來(lái)的問(wèn)題是,假設(shè)程序1的內(nèi)存(1G)用完了釋放后,另一個(gè)程序4(假設(shè)內(nèi)存需要1000M)裝到物理內(nèi)存中可能還剩余 24M 內(nèi)存,如果系統(tǒng)中有大量的這種內(nèi)存碎片,那整體的內(nèi)存利用率就會(huì)很低。
于是,段頁(yè)式內(nèi)存管理方式出現(xiàn)了,它將以上兩種存儲(chǔ)管理方法結(jié)合起來(lái):即先把用戶程序分成若干個(gè)段,為每一個(gè)段分配一個(gè)段名,再把每個(gè)段分成若干個(gè)頁(yè)。
在段頁(yè)式系統(tǒng)中,為了實(shí)現(xiàn)從邏輯地址到物理地址的轉(zhuǎn)換,系統(tǒng)中需要同時(shí)配置段表和頁(yè)表,利用段表和頁(yè)表進(jìn)行從用戶地址到物理內(nèi)存空間的映射。
系統(tǒng)為每個(gè)進(jìn)程創(chuàng)建一張段表,每個(gè)分段上有一個(gè)頁(yè)表。段表包括段號(hào)、頁(yè)表長(zhǎng)度和頁(yè)表始址,頁(yè)表包含頁(yè)號(hào)和塊號(hào)。
圖片
在地址轉(zhuǎn)換時(shí),首先通過(guò)段表查到頁(yè)表地址,再通過(guò)頁(yè)表獲取頁(yè)幀號(hào),最終形成物理地址。
虛擬內(nèi)存到物理內(nèi)存的映射,是操作系統(tǒng)層面去管理的。而我們?cè)陂_發(fā)時(shí),涉及到的內(nèi)存管理,往往只是軟件程序去調(diào)用虛擬內(nèi)存時(shí)要做的工作:
圖片
接下來(lái),我們從虛擬內(nèi)存的構(gòu)成來(lái)分析下軟件開發(fā)中的內(nèi)存管理。
3. 內(nèi)存管理
程序在虛擬內(nèi)存上被分為棧區(qū)、堆區(qū)、數(shù)據(jù)區(qū)、全局?jǐn)?shù)據(jù)區(qū)、代碼段五個(gè)部分。
而內(nèi)存的管理,就是對(duì)內(nèi)存空間進(jìn)行合理化使用,主要是堆區(qū)(Heap)和棧區(qū)(Stack)這兩個(gè)重要區(qū)域的分配使用。
3.1 堆與棧
虛擬內(nèi)存里有兩塊比較重要的地址空間,分別為堆和??臻g。對(duì)于 C++ 等底層的編程語(yǔ)言,棧上的內(nèi)存空間由編譯器統(tǒng)一管理,而堆上的內(nèi)存空間需要程序員來(lái)手動(dòng)管理進(jìn)行分配和回收。
在 Go 語(yǔ)言中,棧上的內(nèi)存空間也是由編譯器來(lái)統(tǒng)一管理,而堆上的內(nèi)存空間由編譯器和垃圾收集器共同管理進(jìn)行分配和回收,這給我們程序員帶來(lái)了極大的便利性。
在棧上分配和回收內(nèi)存的開銷很低,只需要 2 個(gè)指令:PUSH 和 POP。PUSH 將數(shù)據(jù)壓入棧中,POP 釋放空間,消耗的僅是將數(shù)據(jù)拷貝到內(nèi)存的時(shí)間。
而在堆上分配內(nèi)存時(shí),不僅分配的時(shí)候慢,而且垃圾回收的時(shí)候也比較費(fèi)勁,比如說(shuō) Go 在 1.8 以后就用到了三色標(biāo)記法+混合寫屏障的技術(shù)來(lái)做垃圾回收??傮w來(lái)看,堆內(nèi)存分配比棧內(nèi)存分配導(dǎo)致的開銷要大很多。
3.2 堆棧內(nèi)存分配
1)內(nèi)存分配的挑戰(zhàn)
- 像 C/C++ 這類由用戶程序申請(qǐng)內(nèi)存空間,可能會(huì)頻繁地進(jìn)行內(nèi)存申請(qǐng)和回收,但每次內(nèi)存分配時(shí)都需要進(jìn)行系統(tǒng)調(diào)用(即只有進(jìn)入內(nèi)核態(tài)才可以申請(qǐng)內(nèi)存),就會(huì)導(dǎo)致系統(tǒng)的性能很低。
- 除此之外,還可能會(huì)有多線程(Go語(yǔ)言里面也有協(xié)程)去訪問(wèn)同一個(gè)地址空間的情況,這時(shí)就必定需要對(duì)內(nèi)存進(jìn)行加鎖,帶來(lái)的開銷也會(huì)比較大。
- 初始化時(shí)堆內(nèi)存是一整塊連續(xù)的內(nèi)存,但隨著系統(tǒng)運(yùn)行過(guò)程中不斷申請(qǐng)回收內(nèi)存,可能會(huì)產(chǎn)生許多的內(nèi)存碎片,導(dǎo)致內(nèi)存的使用效率降低。
程序進(jìn)行內(nèi)存分配時(shí),為了應(yīng)對(duì)以上最常見(jiàn)的三種問(wèn)題,Go 語(yǔ)言結(jié)合谷歌的 TCMalloc(ThreadCacheMalloc) 內(nèi)存回收方法,做了一些改進(jìn)。
同時(shí),TCMalloc 和 Go 進(jìn)行內(nèi)存分配時(shí)都會(huì)引入線程緩存(mcentral of P)、中心緩存(mcentral)和頁(yè)堆(mheap)三個(gè)組件進(jìn)行分級(jí)管理內(nèi)存。如圖所示:
圖片
線程緩存屬于每一個(gè)獨(dú)立的線程或協(xié)程,里面存儲(chǔ)了每個(gè)線程所用的內(nèi)存塊 span,由于內(nèi)存塊的大小不一,所以有上百個(gè)內(nèi)存塊類別 span class,這些內(nèi)存塊里面分別管理不同大小的內(nèi)存空間(比如 8KB、16KB、32KB...)。由于不涉及多線程,所以不需要使用互斥鎖來(lái)保護(hù)內(nèi)存,以減少鎖競(jìng)爭(zhēng)帶來(lái)的性能損耗。
當(dāng)線程緩存的空間不夠時(shí),會(huì)使用中心緩存作為小對(duì)象內(nèi)存的分配,中心緩存和線程緩存的每個(gè) span class 一一對(duì)應(yīng),并且中心緩存的每個(gè) span class 中有兩個(gè)內(nèi)存塊,分別存儲(chǔ)了分配過(guò)內(nèi)存的空間和滿內(nèi)存空間,以提升內(nèi)存分配的效率。如果中心緩存還不滿足,就向頁(yè)堆進(jìn)行空間申請(qǐng)。
為了提升空間的利用率,當(dāng)遇到中大對(duì)象(>=32KB)分配時(shí),內(nèi)存分配器會(huì)選擇頁(yè)堆直接進(jìn)行分配。
Go 語(yǔ)言內(nèi)存分配的核心是使用多級(jí)緩存將對(duì)象根據(jù)大小分類,并按照類別來(lái)實(shí)施不同的分配策略。如上圖所示,應(yīng)用程序在申請(qǐng)內(nèi)存時(shí)會(huì)根據(jù)對(duì)象的大?。═iny 小對(duì)象或者 Large and medium 中大對(duì)象),向不同的組件去申請(qǐng)內(nèi)存空間。
2)棧內(nèi)存分配
棧區(qū)的內(nèi)存一般由編譯器自動(dòng)分配和釋放,一般來(lái)說(shuō),棧區(qū)存儲(chǔ)著函數(shù)入?yún)⒁约熬植孔兞?,這些數(shù)據(jù)會(huì)隨著函數(shù)的創(chuàng)建而創(chuàng)建,函數(shù)的返回而消亡,一般不會(huì)在程序中長(zhǎng)期存在。
這種線性的內(nèi)存分配策略有著極高地效率,但是工程師也往往不能控制棧內(nèi)存的分配,這部分工作基本都是由編譯器完成的。
??臻g在運(yùn)行時(shí)中包含兩個(gè)重要的全局變量,分別是 runtime.stackpool 和 runtime.stackLarge,這兩個(gè)變量分別表示全局的棧緩存和大棧緩存,前者可以分配小于 32KB 的內(nèi)存,后者用來(lái)分配大于 32KB 的??臻g:
圖片
棧分配時(shí),根據(jù)線程緩存和申請(qǐng)棧的大小,Go 語(yǔ)言會(huì)通過(guò)三種不同的方法分配??臻g:
- 如果棧空間較小,使用全局棧緩存或者線程緩存上固定大小的空閑鏈表分配內(nèi)存;
- 如果棧空間較大,從全局的大棧緩存 runtime.stackLarge 中獲取內(nèi)存空間;
- 如果??臻g較大并且 runtime.stackLarge 空間不足,在堆上申請(qǐng)一片大小足夠內(nèi)存空間。
在 Go1.4 以后,最小的棧內(nèi)存大小為 2KB,即一個(gè) goroutine 協(xié)程的大小。所以,當(dāng)程序里的協(xié)程數(shù)量超過(guò)棧內(nèi)存可分配的最大值后,就會(huì)分配在堆空間里面。也就是說(shuō),雖然 Go 語(yǔ)言里面可以用 go 關(guān)鍵字分配不限數(shù)量的 goroutine 協(xié)程,但是在性能上,我們分配的 goroutine 個(gè)數(shù)最好不要超過(guò)棧空間的最大值。
假設(shè),棧內(nèi)存的最大值為 8MB,那分配的 goroutine 數(shù)量最好不要超過(guò) 4000 個(gè)(8MB/2KB)。
4. 逃逸分析
4.1 Go如何做逃逸分析
在 C 語(yǔ)言和 C++ 這類需要手動(dòng)管理內(nèi)存的編程語(yǔ)言中,將對(duì)象或者結(jié)構(gòu)體分配到棧上或者堆上是由工程師來(lái)決定的,這也為工程師的工作帶來(lái)的挑戰(zhàn):如何精準(zhǔn)地為每一個(gè)變量分配合理的空間,提升整個(gè)程序的運(yùn)行效率和內(nèi)存使用效率。但是 C 和 C++ 的這種手動(dòng)分配內(nèi)存會(huì)導(dǎo)致如下的兩個(gè)問(wèn)題:
- 不需要分配到堆上的對(duì)象分配到了堆上 — 浪費(fèi)內(nèi)存空間;
- 需要分配到堆上的對(duì)象分配到了棧上 — 產(chǎn)生野指針、影響內(nèi)存安全;
與野指針相比,浪費(fèi)內(nèi)存空間反而是小問(wèn)題。在 C 語(yǔ)言中,棧上的變量被函數(shù)作為返回值返回給調(diào)用方是一個(gè)常見(jiàn)的錯(cuò)誤,在如下所示的代碼中,棧上的變量 i 被錯(cuò)誤返回:
int *dangling_pointer() {
int i = 2;
return &i;
}當(dāng) dangling_pointer 函數(shù)返回后,它的本地變量會(huì)被編譯器回收(棧上空間的機(jī)制),調(diào)用方獲取的是危險(xiǎn)的野指針。如果程序里面出現(xiàn)大量不合法的指針值,在大型項(xiàng)目中是比較難以發(fā)現(xiàn)和定位的。
當(dāng)所指向的對(duì)象被釋放或者收回,但是對(duì)該指針沒(méi)有作任何的修改,以至于該指針仍舊指向已經(jīng)回收的內(nèi)存地址,此情況下該指針便稱野指針,或稱懸空指針、迷途指針。——wiki百科
那么,在 Go 語(yǔ)言里面,編譯器該如何知道某個(gè)變量需要分配在堆,還是棧上而避免出現(xiàn)這種問(wèn)題呢?
編譯器決定內(nèi)存分配位置的方式,就稱之為逃逸分析。逃逸分析由編譯器完成,作用于編譯階段。在編譯器優(yōu)化中,逃逸分析是用來(lái)決定指針動(dòng)態(tài)作用域的方法。Go 語(yǔ)言的編譯器使用逃逸分析決定哪些變量應(yīng)該在棧上分配,哪些變量應(yīng)該在堆上分配。
其中包括使用 new、make 和字面量等方法隱式分配的內(nèi)存,Go 語(yǔ)言的逃逸分析遵循以下兩個(gè)不變性:
- 指向棧對(duì)象的指針不能存在于堆中;
- 指向棧對(duì)象的指針不能在棧對(duì)象回收后存活。
什么意思呢?我們來(lái)翻譯一下:
- 首先,如果堆的指針指向了棧對(duì)象,那么棧對(duì)象的內(nèi)存就需要分配到堆上;
- 如果棧對(duì)象回收后,指針還存活,那么這個(gè)對(duì)象就只能分配到堆上。
我們?cè)谶M(jìn)行內(nèi)存分配時(shí),編譯器會(huì)遵循上述兩個(gè)原則,對(duì)我們申請(qǐng)的變量或?qū)ο筮M(jìn)行內(nèi)存分配到棧上或者是堆上。
換言之,當(dāng)我們分配內(nèi)存時(shí),違反了上述兩個(gè)原則之一,本來(lái)想分配到棧上的變量可能就會(huì)“逃逸”到堆上,被稱作內(nèi)存逃逸。如果程序中出現(xiàn)大量的內(nèi)存逃逸,勢(shì)必會(huì)帶來(lái)意外的負(fù)面影響:比如垃圾回收緩慢,內(nèi)存溢出等問(wèn)題。
4.2 四種逃逸場(chǎng)景
Go 語(yǔ)言中,由于以下四種情況,棧上的內(nèi)存可能會(huì)發(fā)生逃逸。
1. 指針逃逸
指針逃逸很容易理解,我們?cè)诤瘮?shù)中創(chuàng)建一個(gè)對(duì)象時(shí),對(duì)象的生命周期隨著函數(shù)結(jié)束而結(jié)束,這時(shí)候?qū)ο蟮膬?nèi)存就分配在棧上。
而如果返回了一個(gè)對(duì)象的指針,這種情況下,函數(shù)雖然退出了,但指針還在,對(duì)象的內(nèi)存不能隨著函數(shù)結(jié)束而回收,因此只能分配在堆上。
package main
type User struct {
ID int64
Name string
Avatar string
}
// 要想不發(fā)生逃逸,返回 User 對(duì)象即可。
func GetUserInfo() *User {
return &User{
ID: 666666,
Name: "sim lou",
Avatar: "https://www.baidu.com/avatar/666666",
}
}
func main() {
u := GetUserInfo()
println(u.Name)
}上面例子中,如果返回的是 User 對(duì)象,而非對(duì)象指針 *User,那么它就是一個(gè)局部變量,會(huì)分配在棧上;反之,指針作為引用,在 main 函數(shù)中還會(huì)繼續(xù)使用,因此內(nèi)存只能分配到堆上。
我們可以用編譯器命令 go build -gcflags -m main.go 來(lái)查看變量逃逸的情況:
圖片
&User{...} escapes to heap 即表示對(duì)象逃逸到堆上了。
2. interface{} 動(dòng)態(tài)類型逃逸
在 Go 語(yǔ)言中,空接口即 interface{} 可以表示任意的類型,如果函數(shù)參數(shù)為 interface{},編譯期間很難確定其參數(shù)的具體類型,也會(huì)發(fā)生逃逸。比如 Println 函數(shù),入?yún)⑹且粋€(gè) interface{} 空類型:
func Println(a ...interface{}) (n int, err error)這時(shí),返回的是一個(gè) User 對(duì)象,也會(huì)發(fā)生對(duì)象逃逸,但逃逸節(jié)點(diǎn)是 fmt.Println 函數(shù)使用時(shí):
func GetUserInfo() User {
return User{
ID: 666666,
Name: "sim lou",
Avatar: "https://www.baidu.com/avatar/666666",
}
}
func main() {
u := GetUserInfo()
fmt.Println(u.Name) // 對(duì)象發(fā)生逃逸
}3. ??臻g不足
操作系統(tǒng)對(duì)內(nèi)核線程使用的棧空間是有大小限制的,64 位 Linux 系統(tǒng)上通常是 8 MB??梢允褂?ulimit -a 命令查看機(jī)器上棧允許占用的內(nèi)存的大小。
root@cvm_172_16_10_34:~ # ulimit -a
-s: stack size (kbytes) 8192
-u: processes 655360
-n: file descriptors 655360因?yàn)闂?臻g通常比較小,因此遞歸函數(shù)實(shí)現(xiàn)不當(dāng)時(shí),容易導(dǎo)致棧溢出。
對(duì)于 Go 語(yǔ)言來(lái)說(shuō),運(yùn)行時(shí)(runtime) 嘗試在 goroutine 需要的時(shí)候動(dòng)態(tài)地分配棧空間,goroutine 的初始棧大小為 2 KB。當(dāng) goroutine 被調(diào)度時(shí),會(huì)綁定內(nèi)核線程執(zhí)行,??臻g大小也不會(huì)超過(guò)操作系統(tǒng)的限制。
對(duì) Go 編譯器而言,超過(guò)一定大小的局部變量將逃逸到堆上,不同的 Go 版本的大小限制可能不一樣。我們來(lái)做一個(gè)實(shí)驗(yàn)(注意,分配 int[] 時(shí),int 占 8 字節(jié),所以 8192 個(gè) int 就是 64 KB):
package main
import "math/rand"
func generate8191() {
nums := make([]int, 8192) // <= 64KB
for i := 0; i < 8192; i++ {
nums[i] = rand.Int()
}
}
func generate8192() {
nums := make([]int, 8193) // > 64KB
for i := 0; i < 8193; i++ {
nums[i] = rand.Int()
}
}
func generate(n int) {
nums := make([]int, n) // 不確定大小
for i := 0; i < n; i++ {
nums[i] = rand.Int()
}
}
func main() {
generate8191()
generate8192()
generate(1)
}編譯結(jié)果如下:
圖片
可以發(fā)現(xiàn),make([]int, 8192) 沒(méi)有發(fā)生逃逸,make([]int, 8193) 和 make([]int, n) 逃逸到堆上。也就是說(shuō),當(dāng)切片占用內(nèi)存超過(guò)一定大小,或無(wú)法確定當(dāng)前切片長(zhǎng)度時(shí),對(duì)象占用內(nèi)存將在堆上分配。
4. 閉包
一個(gè)函數(shù)和對(duì)其周圍狀態(tài)(lexical environment,詞法環(huán)境)的引用捆綁在一起(或者說(shuō)函數(shù)被引用包圍),這樣的組合就是閉包(closure)。也就是說(shuō),閉包讓你可以在一個(gè)內(nèi)層函數(shù)中訪問(wèn)到其外層函數(shù)的作用域。
— 閉包
Go 語(yǔ)言中,當(dāng)使用閉包函數(shù)時(shí),也會(huì)發(fā)生內(nèi)存逃逸??匆粍t示例代碼:
package main
func Increase() func() int {
n := 0
return func() int {
n++
return n
}
}
func main() {
in := Increase()
fmt.Println(in()) // 1
fmt.Println(in()) // 2
}Increase() 函數(shù)的返回值是一個(gè)閉包函數(shù),該閉包函數(shù)訪問(wèn)了外部變量 n,那變量 n 將會(huì)一直存在,直到 in 被銷毀。很顯然,變量 n 占用的內(nèi)存不能隨著函數(shù) Increase() 的退出而回收,因此將會(huì)逃逸到堆上。
4.3 利用逃逸分析提升性能
傳值VS傳指針
傳值會(huì)拷貝整個(gè)對(duì)象,而傳指針只會(huì)拷貝指針地址,指向的對(duì)象是同一個(gè)。傳指針可以減少值的拷貝,但是會(huì)導(dǎo)致內(nèi)存分配逃逸到堆中,增加垃圾回收(GC)的負(fù)擔(dān)。在對(duì)象頻繁創(chuàng)建和刪除的場(chǎng)景下,傳遞指針導(dǎo)致的 GC 開銷可能會(huì)嚴(yán)重影響性能。
一般情況下,對(duì)于需要修改原對(duì)象值,或占用內(nèi)存比較大的結(jié)構(gòu)體,選擇傳指針。對(duì)于只讀的占用內(nèi)存較小的結(jié)構(gòu)體,直接傳值能夠獲得更好的性能。
5. 小結(jié)
內(nèi)存分配是程序運(yùn)行時(shí)內(nèi)存管理的核心邏輯,Go 程序運(yùn)行時(shí)的內(nèi)存分配器使用類似 TCMalloc 的分配策略將對(duì)象根據(jù)大小分類,并設(shè)計(jì)多層緩存的組件提高內(nèi)存分配器的性能。
理解 Go 語(yǔ)言內(nèi)存分配器的設(shè)計(jì)與實(shí)現(xiàn)原理,可以幫助我們理解不同編程語(yǔ)言在設(shè)計(jì)內(nèi)存分配器時(shí)做出的不同選擇。
棧內(nèi)存是應(yīng)用程序中重要的內(nèi)存空間,它能夠支持本地的局部變量和函數(shù)調(diào)用,??臻g中的變量會(huì)與棧一同創(chuàng)建和銷毀,這部分內(nèi)存空間不需要工程師過(guò)多的干預(yù)和管理,現(xiàn)代的編程語(yǔ)言通過(guò)逃逸分析減少了我們的工作量,理解棧空間的分配對(duì)于理解 Go 語(yǔ)言的運(yùn)行時(shí)有很大的幫助。

























