五分鐘搞懂 Golang 堆內存
你想過為什么堆內存被稱為 "堆" 嗎?想象一下雜亂堆放的對象,與此類似,在計算機中,堆內存是動態(tài)分配和釋放內存的空間,通常會導致內存塊的無序排列。我們可以利用這種相似性和無序排列來理解堆內存,并探討堆內存的概念及其在計算中的意義。
一、什么是堆內存?
堆內存是程序內存中用于動態(tài)內存分配的部分。堆內存不是在編譯過程中預先確定的,而是在程序運行過程中動態(tài)管理的。程序在執(zhí)行過程中可以根據(jù)需要從堆中申請、釋放內存。
1. 進程的內存布局
在繼續(xù)介紹之前,我們先退一步,試著了解一下進程的內存布局,如下圖所示,可以簡單了解大致的內存布局。
+ - - - - - - - - - - - - - - - +
| Stack | ←- 棧,靜態(tài)分配
| - - - - - - - - - - - - - - - |
| Heap | ←- 堆,動態(tài)分配
| - - - - - - - - - - - - - - - |
| Uninitialized Data | ←- 未初始化數(shù)據(jù)
| - - - - - - - - - - - - - - - |
| Initialized Data | ←- 初始化數(shù)據(jù)
| - - - - - - - - - - - - - - - |
| Code | ←- 代碼(文本段)
+ - - - - - - - - - - - - - - - +
進程內存布局
我們來分解一下進程的內存布局,看看它們是如何協(xié)同工作的:
- 棧(Stack):這部分內存用于靜態(tài)內存分配,是存儲局部變量和函數(shù)調用信息的地方,會隨著函數(shù)的調用和返回而自動增大和縮小。
- 堆(Heap):這是動態(tài)內存分配區(qū)域。當程序需要申請未預先定義的內存時,就會向堆申請空間。這里的內存可以在運行時分配和釋放,為程序提供了處理數(shù)組、鏈表等動態(tài)數(shù)據(jù)結構所需的靈活性。
- 未初始化數(shù)據(jù)(BSS 段):該段存放開發(fā)者已聲明但并未初始化的全局變量和靜態(tài)變量。程序啟動時,操作系統(tǒng)會將這些變量初始化為零。
- 初始化數(shù)據(jù):該區(qū)域包含開發(fā)者已初始化的全局變量和靜態(tài)變量。程序一開始運行,這些變量就可以立即使用。
- 代碼(文本段):該段存儲程序的可執(zhí)行指令。通常這部分內存是只讀的,以防止意外修改程序指令。
通過簡單介紹,可以看到內存是如何有效組織,以滿足運行進程的靜態(tài)和動態(tài)需求。堆的作用對于動態(tài)內存分配尤為重要,從而允許程序靈活高效的管理內存。
2. 堆內存的特點
- 動態(tài)分配:內存在運行時申請、釋放。可變大小:分配的內存大小可以變化。基于指針的管理:使用指針訪問和控制內存。
下圖演示了如何通過將堆內存劃分為多個空閑塊和已分配塊來動態(tài)管理堆內存:
+ - - - - - - - - - - -+
| Heap Memory. | ←- 堆內存
| - - - - - - - - - - -|
| Free Block | ←- 空閑塊
| - - - - - - - - - - -|
| Allocated Block 1 | ←- 已分配塊1
| [Pointer -> Data] |
| - - - - - - - - - - -|
| Free Block | ←- 空閑塊
| - - - - - - - - - - -|
| Allocated Block 2 | ←- 已分配塊2
| [Pointer -> Data] |
| - - - - - - - - - - -|
| Free Block. | ←- 空閑塊
+ - - - - - - - - - - -+
動態(tài)分配
- 空閑塊(Free Blocks):這些是當前未分配的內存塊,可供將來使用。當程序請求內存時,可以從這些空閑塊中獲取。
- 已分配塊(Allocated Blocks):這些部分已分配給程序并儲存了數(shù)據(jù)。每個已分配塊通常都包含一個指向其所含數(shù)據(jù)的指針。
多個空閑塊和已分配塊的存在表明,內存的分配和釋放在程序運行過程中不斷發(fā)生。由于內存分配和釋放的時間不同,導致空閑內存段和已用內存段交替出現(xiàn),堆就會出現(xiàn)這種碎片化現(xiàn)象。
二、堆內存如何工作?
堆內存由操作系統(tǒng)管理。當程序請求內存時,操作系統(tǒng)會從進程的堆內存段中分配內存。這一過程涉及多個關鍵組件和功能:
主要組成部分:
- 堆內存段:進程內存中保留用于動態(tài)分配的部分
- mmap:調整數(shù)據(jù)段末尾以增加或減少堆大小的系統(tǒng)調用
- malloc 和 free:C 庫提供的函數(shù),用于分配和釋放堆上的內存
- 內存管理器:C 庫的一個組件,用于管理堆,跟蹤已分配和已釋放的內存塊。
三、Go 如何管理堆內存
Go 為堆內存管理提供了內置函數(shù)和數(shù)據(jù)結構,如 new、make、slices、maps 和 channels。這些函數(shù)和數(shù)據(jù)結構抽象掉了底層細節(jié),在內部與操作系統(tǒng)的內存管理機制進行了交互。
1. 實例
我們通過一個簡單的 Go 程序來理解,該程序為整數(shù)片段分配內存、初始化數(shù)值并打印。
package main
import (
"fmt"
"runtime"
)
func main() {
// 為包含10個整數(shù)的切片分配內存(動態(tài)數(shù)組)
memorySize := 10
slice := make([]int, memorySize)
// 初始化并使用分配的內存
for i := 0; i < len(slice); i++ {
slice[i] = 5 // 為每個元素賦值
}
// 打印值
for i := 0; i < len(slice); i++ {
fmt.Printf("%d ", slice[i])
}
fmt.Println()
// 通過強制垃圾收集演示內存釋放
runtime.GC()
}
為了了解 Go 如何與 Linux 內存管理庫交互,可以使用 strace(我最喜歡的工具)來跟蹤 Go 程序進行的系統(tǒng)調用。
2. 內存分配中的系統(tǒng)調用
$ go build -o memory_allocation main.go
$ strace -f -e trace=mmap,munmap ./memory_allocation
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff94da0000
mmap(NULL, 131072, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff94d80000
mmap(NULL, 1048576, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff94c80000
mmap(NULL, 8388608, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff94400000
mmap(NULL, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff90400000
mmap(NULL, 536870912, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff70400000
mmap(NULL, 536870912, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff50400000
mmap(0x4000000000, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4000000000
mmap(NULL, 33554432, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff4e400000
mmap(NULL, 68624, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff94c6f000
mmap(0x4000000000, 4194304, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x4000000000
mmap(0xffff94d80000, 131072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xffff94d80000
mmap(0xffff94c80000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xffff94c80000
mmap(0xffff94402000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xffff94402000
mmap(0xffff90410000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xffff90410000
mmap(0xffff70480000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xffff70480000
mmap(0xffff50480000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xffff50480000
mmap(NULL, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff4e300000
mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff94c5f000
mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff94c4f000
strace: Process 1141999 attached
strace: Process 1142000 attached
strace: Process 1142001 attached
[pid 1141998] --- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=1141998, si_uid=0} ---
[pid 1142000] mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff94c0f000
strace: Process 1142002 attached
5 5 5 5 5 5 5 5 5 5
[pid 1142001] mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff4e2c0000
[pid 1141998] --- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=1141998, si_uid=0} ---
[pid 1142000] mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff4e2b0000
[pid 1141998] mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff4e270000
[pid 1142002] +++ exited with 0 +++
[pid 1142001] +++ exited with 0 +++
[pid 1142000] +++ exited with 0 +++
[pid 1141999] +++ exited with 0 +++
+++ exited with 0 +++
+ - - - - - - - - - - -+
| Go Program | ←- Go 程序
| - - - - - - - - - - -|
| Calls Go Runtime | ←- 調用 Go 運行時
| - - - - - - - - - - -|
| Uses syscalls: | ←- 系統(tǒng)調用:mmap,munmap
| mmap, munmap |
| - - - - - - - - - - -|
| Interacts with OS | ←- 與操作系統(tǒng)內存管理器交互
| Memory Manager |
+ - - - - - - - - - - -+
系統(tǒng)調用的簡化示例
3. strace 輸出解釋
- mmap 調用:mmap 系統(tǒng)調用用于分配內存頁。輸出中的每個 mmap 調用都是請求操作系統(tǒng)分配特定數(shù)量(用 size 參數(shù)指定,例如 262144、131072 字節(jié))的內存,。
- 內存保護(Memory Protections):參數(shù) PROT_READ|PROT_WRITE 表示分配的內存應是可讀和可寫的。
- 匿名映射(Anonymous Mapping):MAP_PRIVATE|MAP_ANONYMOUS 標記表示內存沒有任何文件支持,所做更改對進程來說是私有的。
- 固定地址映射(Fixed Address Mapping):有些 mmap 調用使用 MAP_FIXED 標記,指定內存應映射到特定地址,通常用于直接管理特定內存區(qū)域。
4. 內存分配過程的各個階段
+ - - - - - - - - - - -+
| Initialize Slice | ←- 初始化切片
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
| - - - - - - - - - - -|
| Set Values | ←- 設置值
| [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)內存分配和管理的逐步過程。
(1) 初始化切片:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
切片(動態(tài)數(shù)組)的初始狀態(tài)為 10 個元素,全部設置為 0。這一步展示了 Go 如何為切片分配內存。
(2) 設置值:
[5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
然后,在切片的每個元素中填入值 5。這一步演示了如何初始化和使用分配的內存。
(3) 打印值:
5 5 5 5 5 5 5 5 5 5
打印切片的值,確認內存分配和初始化成功。這一步驗證程序是否正確訪問和使用了分配的內存。
(4) 強制 GC(垃圾回收)
手動觸發(fā)垃圾回收器,釋放不再使用的內存。這一步強調 Go 的自動內存管理和清理過程,確保了資源的有效利用。
四、總結
堆內存是現(xiàn)代計算的重要方面,它實現(xiàn)了動態(tài)內存分配,使程序能在運行時有效管理內存。這種靈活性對于處理鏈表、樹、圖等動態(tài)數(shù)據(jù)結構至關重要,因為這些結構無法在編譯時預先確定。了解堆內存對于開發(fā)人員編寫高效、穩(wěn)健的應用至關重要,可確保有效使用內存,并在不再需要時釋放資源。
通過探討堆內存在 Linux 中的工作原理以及 Go 如何管理動態(tài)內存分配,希望本文能為你提供有關內存管理內部運作的寶貴見解。掌握這些概念不僅有助于編寫更好的代碼,還有助于調試和優(yōu)化應用程序。