一文搞懂Go語言堆內(nèi)存原理小結(jié)
一、基本概念理解
1.1 什么是堆內(nèi)存?
堆內(nèi)存是程序內(nèi)存中用于動態(tài)內(nèi)存分配的部分。堆內(nèi)存不是在編譯過程中預(yù)先確定的,而是在程序運行過程中動態(tài)管理的。程序在執(zhí)行過程中可以根據(jù)需要從堆中申請、釋放內(nèi)存。
在繼續(xù)介紹之前,試著了解一下進程的內(nèi)存布局,如下圖所示,可以簡單了解大致的內(nèi)存布局。
+ - - - - - - - - - - - - - - - +
| Stack | ←- 棧,靜態(tài)分配
| - - - - - - - - - - - - - - - |
| Heap | ←- 堆,動態(tài)分配
| - - - - - - - - - - - - - - - |
| Uninitialized Data | ←- 未初始化數(shù)據(jù)
| - - - - - - - - - - - - - - - |
| Initialized Data | ←- 初始化數(shù)據(jù)
| - - - - - - - - - - - - - - - |
| Code | ←- 代碼(文本段)
+ - - - - - - - - - - - - - - - +
進程內(nèi)存布局
我們來分解一下進程的內(nèi)存布局,看看它們是如何協(xié)同工作的:
- 棧(Stack):這部分內(nèi)存用于靜態(tài)內(nèi)存分配,是存儲局部變量和函數(shù)調(diào)用信息的地方,會隨著函數(shù)的調(diào)用和返回而自動增大和縮小。
- 堆(Heap):這是動態(tài)內(nèi)存分配區(qū)域。當程序需要申請未預(yù)先定義的內(nèi)存時,就會向堆申請空間。這里的內(nèi)存可以在運行時分配和釋放,為程序提供了處理數(shù)組、鏈表等動態(tài)數(shù)據(jù)結(jié)構(gòu)所需的靈活性。
- 未初始化數(shù)據(jù)(BSS 段):該段存放開發(fā)者已聲明但并未初始化的全局變量和靜態(tài)變量。程序啟動時,操作系統(tǒng)會將這些變量初始化為零。
- 初始化數(shù)據(jù):該區(qū)域包含開發(fā)者已初始化的全局變量和靜態(tài)變量。程序一開始運行,這些變量就可以立即使用。
- 代碼(文本段):該段存儲程序的可執(zhí)行指令。通常這部分內(nèi)存是只讀的,以防止意外修改程序指令。
1.2 堆內(nèi)存的特點
動態(tài)分配:內(nèi)存在運行時申請、釋放。
可變大?。悍峙涞膬?nèi)存大小可以變化。
基于指針的管理:使用指針訪問和控制內(nèi)存。
+ - - - - - - - - - - -+
| Heap Memory. | ←- 堆內(nèi)存
| - - - - - - - - - - -|
| Free Block | ←- 空閑塊
| - - - - - - - - - - -|
| Allocated Block 1 | ←- 已分配塊1
| [Pointer -> Data] |
| - - - - - - - - - - -|
| Free Block | ←- 空閑塊
| - - - - - - - - - - -|
| Allocated Block 2 | ←- 已分配塊2
| [Pointer -> Data] |
| - - - - - - - - - - -|
| Free Block. | ←- 空閑塊
+ - - - - - - - - - - -+
動態(tài)分配
- 空閑塊(Free Blocks):這些是當前未分配的內(nèi)存塊,可供將來使用。當程序請求內(nèi)存時,可以從這些空閑塊中獲取。
- 已分配塊(Allocated Blocks):這些部分已分配給程序并儲存了數(shù)據(jù)。每個已分配塊通常都包含一個指向其所含數(shù)據(jù)的指針。
多個空閑塊和已分配塊的存在表明,內(nèi)存的分配和釋放在程序運行過程中不斷發(fā)生。由于內(nèi)存分配和釋放的時間不同,導(dǎo)致空閑內(nèi)存段和已用內(nèi)存段交替出現(xiàn),堆就會出現(xiàn)這種碎片化現(xiàn)象。
1.3 前置知識:棧與堆的根本區(qū)別
要理解堆,必先理解棧。在 Go 程序中,每個 Goroutine 都有一個獨立的棧,而所有 Goroutine 共享一個堆。
| 特性 | 棧 | 堆 |
|---|---|---|
| 所有權(quán) | Goroutine 獨有 | 進程內(nèi)所有 Goroutine 共享 |
| 分配與釋放 | 編譯器/運行時自動管理,函數(shù)入棧時分配,出棧時釋放,速度極快 | 垃圾回收器管理,分配相對較慢,釋放時機不確定(GC時) |
| 大小 | 小而固定(初始幾KB,可動態(tài)增長,但有上限) | 非常大(可達 TB 級別,受限于系統(tǒng)內(nèi)存) |
| 存儲數(shù)據(jù) | 函數(shù)參數(shù)、局部變量、返回地址等生命周期明確的數(shù)據(jù) | 生命周期不確定的數(shù)據(jù),比如函數(shù)返回后仍需被訪問的對象 |
| 訪問速度 | 快(連續(xù)內(nèi)存,CPU緩存友好) | 慢(內(nèi)存不連續(xù),可能涉及系統(tǒng)調(diào)用) |
1.4 堆內(nèi)存如何工作?
堆內(nèi)存由操作系統(tǒng)管理。當程序請求內(nèi)存時,操作系統(tǒng)會從進程的堆內(nèi)存段中分配內(nèi)存。這一過程涉及多個關(guān)鍵組件和功能:
主要組成部分:
- 堆內(nèi)存段:進程內(nèi)存中保留用于動態(tài)分配的部分
- mmap:調(diào)整數(shù)據(jù)段末尾以增加或減少堆大小的系統(tǒng)調(diào)用
- malloc 和 free:C 庫提供的函數(shù),用于分配和釋放堆上的內(nèi)存
- 內(nèi)存管理器:C 庫的一個組件,用于管理堆,跟蹤已分配和已釋放的內(nèi)存塊。
1.5 核心原理:Go 對象如何分配到堆上?
當一個 Goroutine 需要在堆上分配內(nèi)存時,流程如下:
- 獲取 P:該 Goroutine 綁定到某個 P(邏輯處理器)上。
- 查找 mcache:從 P 中獲取其專屬的內(nèi)存緩存
mcache。 - 大小分級:根據(jù)要分配的對象大小,選擇合適的規(guī)格:
- 微小對象 (< 16 bytes):直接在
mcache中用一個專門的tiny對象來處理,避免浪費。 - 小對象 (< 32KB):在
mcache中尋找對應(yīng)大小規(guī)格的mspan(內(nèi)存跨度)來分配。 - 大對象 (>= 32KB):直接跳過
mcache和mcentral,向全局的mheap申請內(nèi)存。
關(guān)鍵點:絕大多數(shù)對象都是小對象,它們的分配都可以在mcache中無鎖完成,速度極快。
- 微小對象 (< 16 bytes):直接在
核心問題:編譯器如何決定一個對象放棧上還是堆上?
答案是:逃逸分析。
Go 編譯器會分析代碼,如果一個局部變量的作用域超出了它所在的函數(shù)(即“逃逸”了),那么它就必須被分配在堆上。如:
// 情況一:不逃逸,分配在棧上
func stackExample() int {
x := 10 // x 的生命周期只在 stackExample 函數(shù)內(nèi)
return x
}
// 情況二:逃逸,分配在堆上
func heapExample() *int {
x := 10 // x 的指針被返回,作用域超出了函數(shù),x 逃逸到堆上
return &x
}
可以使用 go build -gcflags="-m" 命令來查看逃逸分析的結(jié)果:
$ go build -gcflags="-m" main.go # command-line-arguments ./main.go:10:6: can inline heapExample ./main.go:11:2: leaking param: x ./main.go:11:2: moved to heap: x
moved to heap: x 明確告訴我們變量 x 被分配到了堆上。
1.6 Go 內(nèi)存分配器:TCMalloc 的繼承與演進
Go 的內(nèi)存分配器高度借鑒了 Google 的 TCMalloc。其核心思想是:避免多線程競爭,將內(nèi)存管理工作分攤到每個處理器(P)上。
這帶來了兩個核心設(shè)計:
- 無鎖化:每個 P 都有自己的本地內(nèi)存緩存,大部分分配操作都在 P 內(nèi)部完成,無需加鎖。
- 分級管理:將內(nèi)存按大小分為不同級別,用不同策略管理,提高分配效率。
二、Go 如何管理堆內(nèi)存
Go 為堆內(nèi)存管理提供了內(nèi)置函數(shù)和數(shù)據(jù)結(jié)構(gòu),如 new、make、slices、maps 和 channels。這些函數(shù)和數(shù)據(jù)結(jié)構(gòu)抽象掉了底層細節(jié),在內(nèi)部與操作系統(tǒng)的內(nèi)存管理機制進行了交互。
2.1 案例
我們通過一個簡單的 Go 程序來理解,該程序為整數(shù)片段分配內(nèi)存、初始化數(shù)值并打印。(main.go)
package main
import (
"fmt"
"runtime"
)
func main() {
// 為包含10個整數(shù)的切片分配內(nèi)存(動態(tài)數(shù)組)
memorySize := 10
slice := make([]int, memorySize)
// 初始化并使用分配的內(nèi)存
for i := 0; i < len(slice); i++ {
slice[i] = 5 // 為每個元素賦值
}
// 打印值
for i := 0; i < len(slice); i++ {
fmt.Printf("%d ", slice[i])
}
fmt.Println()
// 通過強制垃圾收集演示內(nèi)存釋放
runtime.GC()
}
為了了解 Go 如何與 Linux 內(nèi)存管理庫交互,可以使用 strace(centos系統(tǒng)可通過:yum install strace安裝)來跟蹤 Go 程序進行的系統(tǒng)調(diào)用。
2.2 內(nèi)存分配中的系統(tǒng)調(diào)用
$ go build -o memory_allocation main.go $ strace -f -e trace=mmap,munmap ./memory_allocation
執(zhí)行結(jié)果如下:
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50caa6b000 mmap(NULL, 131072, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50caa4b000 mmap(NULL, 1048576, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50ca94b000 mmap(NULL, 8388608, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50ca14b000 mmap(NULL, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50c614b000 mmap(NULL, 536870912, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50a614b000 mmap(NULL, 536870912, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f508614b000 mmap(0xc000000000, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc000000000 mmap(NULL, 33554432, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f508414b000 mmap(NULL, 69648, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5084139000 mmap(0xc000000000, 4194304, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xc000000000 mmap(0x7f50caa4b000, 131072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50caa4b000 mmap(0x7f50ca9cb000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50ca9cb000 mmap(0x7f50ca551000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50ca551000 mmap(0x7f50c817b000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50c817b000 mmap(0x7f50b62cb000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50b62cb000 mmap(0x7f50962cb000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50962cb000 mmap(NULL, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5084039000 mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5084029000 mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5084019000 mmap(NULL, 1439992, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5083eb9000 strace: Process 1425438 attached [pid 1425437] mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5083e79000 strace: Process 1425439 attached strace: Process 1425440 attached [pid 1425437] mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5083e39000 strace: Process 1425441 attached 5 5 5 5 5 5 5 5 5 5 [pid 1425437] mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5083e29000 [pid 1425440] +++ exited with 0 +++ [pid 1425439] +++ exited with 0 +++ [pid 1425438] +++ exited with 0 +++ [pid 1425441] +++ exited with 0 +++ +++ exited with 0 +++
+ - - - - - - - - - - -+
| Go Program | ←- Go 程序
| - - - - - - - - - - -|
| Calls Go Runtime | ←- 調(diào)用 Go 運行時
| - - - - - - - - - - -|
| Uses syscalls: | ←- 系統(tǒng)調(diào)用:mmap,munmap
| mmap, munmap |
| - - - - - - - - - - -|
| Interacts with OS | ←- 與操作系統(tǒng)內(nèi)存管理器交互
| Memory Manager |
+ - - - - - - - - - - -+
系統(tǒng)調(diào)用的簡化示例
strace 輸出解釋
- mmap 調(diào)用:mmap 系統(tǒng)調(diào)用用于分配內(nèi)存頁。輸出中的每個 mmap 調(diào)用都是請求操作系統(tǒng)分配特定數(shù)量(用 size 參數(shù)指定,例如 262144、131072 字節(jié))的內(nèi)存,。
- 內(nèi)存保護(Memory Protections):參數(shù) PROT_READ|PROT_WRITE 表示分配的內(nèi)存應(yīng)是可讀和可寫的。
- 匿名映射(Anonymous Mapping):MAP_PRIVATE|MAP_ANONYMOUS 標記表示內(nèi)存沒有任何文件支持,所做更改對進程來說是私有的。
- 固定地址映射(Fixed Address Mapping):有些 mmap 調(diào)用使用 MAP_FIXED 標記,指定內(nèi)存應(yīng)映射到特定地址,通常用于直接管理特定內(nèi)存區(qū)域。
2.3 內(nèi)存分配過程的各個階段:
+ - - - - - - - - - - -+ | Initialize Slice | ←- 初始化切片 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] | | - - - - - - - - - - -| | Set Values | ←- 設(shè)置值 | [5, 5, 5, 5, 5, 5, 5, 5, 5, 5] | | - - - - - - - - - - -| | Print Values | ←- 打印值 | 5 5 5 5 5 5 5 5 5 5 | | - - - - - - - - - - -| | Force GC | ←- 強制垃圾回收 | - - - - - - - - - - -|
上圖說明了 Go 動態(tài)內(nèi)存分配和管理的逐步過程。
- 1、初始化切片:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],切片(動態(tài)數(shù)組)的初始狀態(tài)為 10 個元素,全部設(shè)置為 0。這一步展示了 Go 如何為切片分配內(nèi)存。 - 2、設(shè)置值:
[5, 5, 5, 5, 5, 5, 5, 5, 5, 5],然后,在切片的每個元素中填入值 5。這一步演示了如何初始化和使用分配的內(nèi)存。 - 3、打印值:
5 5 5 5 5 5 5 5 5 5,打印切片的值,確認內(nèi)存分配和初始化成功。這一步驗證程序是否正確訪問和使用了分配的內(nèi)存。 - 4、強制 GC(垃圾回收):手動觸發(fā)垃圾回收器,釋放不再使用的內(nèi)存。這一步強調(diào) Go 的自動內(nèi)存管理和清理過程,確保了資源的有效利用。
三、 堆內(nèi)存管理:三級結(jié)構(gòu)
Go 的堆內(nèi)存管理是一個精密的三級(或四級)結(jié)構(gòu),理解它就理解了 Go 內(nèi)存管理的核心。
3.1 第一級:mcache (Per-P Cache)
- 是什么:每個 P 都有一個獨立的
mcache。 - 作用:存儲各種大小規(guī)格的
mspan的空閑列表。 - 特點:無鎖分配。因為 P 同一時間只能被一個 Goroutine 占用,所以從
mcache分配內(nèi)存是線程安全的,無需加鎖。這是 Go 高并發(fā)內(nèi)存分配性能高的關(guān)鍵。
3.2 第二級:mcentral (Central Cache)
- 是什么:全局的內(nèi)存中心,所有 P 共享。它按
spanclass(大小規(guī)格)分為多個mcentral。 - 作用:當
mcache中的mspan不夠用時,P 會向?qū)?yīng)的mcentral申請新的mspan。 - 特點:需要加鎖。因為多個 P 可能同時向同一個
mcentral申請內(nèi)存,所以需要加鎖保證線程安全。
3.3 第三級:mheap (Heap Manager)
- 是什么:全局唯一的堆內(nèi)存管理器,掌管著所有從操作系統(tǒng)申請來的大塊內(nèi)存。
- 作用:
- 管理
mcentral,當mcentral的mspan不足時,向mheap申請。 - 直接處理大對象(>= 32KB)的分配請求。
- 當內(nèi)存不足時,向操作系統(tǒng)申請更多內(nèi)存(調(diào)用
mmap)。
- 管理
3.4 基礎(chǔ)單元:mspan (Memory Span)
- 是什么:
mcache、mcentral、mheap之間流轉(zhuǎn)的基本單位。它是一段連續(xù)的內(nèi)存地址,由多個頁組成。 - 作用:
mspan會被劃分為特定大小的塊,用于存儲同一種規(guī)格的對象。例如,一個mspan可能專門用來存放所有 16 字節(jié)大小的對象。
流程串聯(lián): Goroutine -> mcache (無鎖) -> mcentral (加鎖) -> mheap (全局鎖) -> OS
到此這篇關(guān)于一文搞懂Go語言堆內(nèi)存原理小結(jié)的文章就介紹到這了,更多相關(guān)Go語言 堆內(nèi)存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
linux下通過go語言獲得系統(tǒng)進程cpu使用情況的方法
這篇文章主要介紹了linux下通過go語言獲得系統(tǒng)進程cpu使用情況的方法,實例分析了Go語言使用linux的系統(tǒng)命令ps來分析cpu使用情況的技巧,需要的朋友可以參考下2015-03-03
Golang基于JWT與Casbin身份驗證授權(quán)實例詳解
這篇文章主要為大家介紹了Golang基于JWT與Casbin實現(xiàn)身份驗證授權(quán)實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08

