詳解Go中如何進行進行內(nèi)存優(yōu)化和垃圾收集器管理
Go 中的棧和堆
這篇文章不會深入研究垃圾收集器的內(nèi)部工作原理,因為大量文章和官方文檔已經(jīng)涵蓋了這個主題。除此之外,我將介紹關(guān)鍵概念來闡明本文探討的主題。
在Go中,數(shù)據(jù)可以分為兩種主要的內(nèi)存存儲:棧和堆。
一般來說,堆棧中存放的數(shù)據(jù)的大小和生命周期是 Go 編譯器可以預(yù)期的。這包括局部函數(shù)變量、函數(shù)參數(shù)、返回值等等。
堆棧是自動管理的,遵循后進先出 (LIFO) 原則。當調(diào)用函數(shù)時,所有關(guān)聯(lián)的數(shù)據(jù)都放置在堆棧頂部,函數(shù)完成后,這些數(shù)據(jù)將被刪除。堆棧高效運行,將內(nèi)存管理的開銷降至最低。在堆棧上檢索和存儲數(shù)據(jù)的過程很快。
盡管如此,并非所有程序數(shù)據(jù)都可以駐留在堆棧中。在執(zhí)行過程中動態(tài)變化的數(shù)據(jù)或需要超出函數(shù)范圍的訪問的數(shù)據(jù)不能容納在堆棧中,因為編譯器無法預(yù)測其使用情況。這些數(shù)據(jù)在堆中找到了自己的家。
與堆棧相比,從堆中檢索數(shù)據(jù)及其管理是更消耗資源的過程。
堆棧與堆分配
正如前面提到的,堆棧提供可預(yù)測大小和壽命的值。此類值的示例包括在函數(shù)內(nèi)聲明的局部變量,如基本數(shù)據(jù)類型(例如數(shù)字和布爾值)、函數(shù)參數(shù)和函數(shù)返回值(如果它們在從函數(shù)返回后不再找到引用)。
Go 編譯器在決定是在堆棧還是堆上分配數(shù)據(jù)時會采用各種細微差別。例如,大小最大為 64 KB 的預(yù)分配切片將分配給堆棧,而超過 64 KB 的切片將指定給堆。同樣的標準適用于數(shù)組:超過 10 MB 的數(shù)組將分派到堆。
為了確定特定變量的分配位置,可以采用逃逸分析。為此,您可以通過使用以下標志從命令行編譯應(yīng)用程序來仔細檢查應(yīng)用程序-gcflags=-m
:
go build -gcflags=-m main.go
main.go
當使用以下標志編譯以下應(yīng)用程序時-gcflags=-m
:
package main func main() { var arrayBefore10Mb [1310720]int arrayBefore10Mb[0] = 1 var arrayAfter10Mb [1310721]int arrayAfter10Mb[0] = 1 sliceBefore64 := make([]int, 8192) sliceOver64 := make([]int, 8193) sliceOver64[0] = sliceBefore64[0] }
結(jié)果表明,arrayAfter10Mb
由于數(shù)組大小超過 10 MB,因此該數(shù)組被重新定位到堆中。相反,arrayBefore10Mb
保留在堆棧上。此外,sliceBefore64
由于其大小小于 64 KB,因此不遷移到堆,而sliceOver64
存儲在堆中。
要更深入地了解堆分配,請參閱文檔。
垃圾收集器:管理堆
處理堆的一種有效方法是避免使用它。但是,如果數(shù)據(jù)已經(jīng)進入堆,該怎么辦?
與堆棧相反,堆擁有無限的大小和持續(xù)增長。堆是動態(tài)生成的對象的所在地,例如結(jié)構(gòu)、切片、映射和無法適應(yīng)堆棧約束的大量內(nèi)存塊。
垃圾收集器是回收堆內(nèi)存并防止其完全阻塞的唯一工具。
了解垃圾收集器
垃圾收集器(通常稱為 GC)是一個專用系統(tǒng),旨在識別和釋放動態(tài)分配的內(nèi)存。
Go 采用了一種植根于跟蹤和標記和清除方法的垃圾收集算法。在標記階段,垃圾收集器將應(yīng)用程序主動使用的數(shù)據(jù)指定為活動堆。隨后,在清理階段,GC 會遍歷未標記的內(nèi)存,使其可供重用。
然而,垃圾收集器的操作是有代價的,消耗兩個重要的系統(tǒng)資源:CPU 時間和物理內(nèi)存。
垃圾收集器內(nèi)的內(nèi)存包括:
- 活動堆內(nèi)存(在上一個垃圾收集周期中標記為“活動”的內(nèi)存)。
- 新的堆內(nèi)存(尚未被垃圾收集器分析的堆內(nèi)存)。
- 元數(shù)據(jù)存儲,與前兩個實體相比通常微不足道。
垃圾收集器消耗的 CPU 時間取決于其操作模式。某些垃圾收集器實現(xiàn)(標記為“stop-the-world”)會在垃圾收集期間完全暫停程序執(zhí)行,從而導(dǎo)致 CPU 時間浪費在非生產(chǎn)性任務(wù)上。
在 Go 的上下文中,垃圾收集器并不完全是“停止世界”,它的大部分工作(包括堆標記)與應(yīng)用程序的執(zhí)行并行。然而,它確實需要一些限制,并在一個周期內(nèi)定期停止活動代碼的執(zhí)行。
到現(xiàn)在為止,讓我們更進一步。
管理垃圾收集器
控制 Go 中的垃圾收集器可以通過特定參數(shù)來實現(xiàn):GOGC 環(huán)境變量或其功能等效項 SetGCPercent,可在運行時/調(diào)試包中找到。
GOGC 參數(shù)指示與垃圾收集啟動時的活動內(nèi)存相關(guān)的新的、未分配的堆內(nèi)存的百分比。
默認情況下,GOGC 設(shè)置為 100,表示當新內(nèi)存量達到活動堆內(nèi)存的 100% 時觸發(fā)垃圾回收。
考慮一個示例程序,并通過 go 工具跟蹤跟蹤堆大小的變化。我們將使用 Go 版本 1.20.1 來執(zhí)行該程序。
在此示例中,該performMemoryIntensiveTask
函數(shù)消耗了堆中分配的大量內(nèi)存。該函數(shù)啟動一個隊列大小為NumWorker
且任務(wù)數(shù)量等于 的工作池NumTasks
。
package main import ( "fmt" "os" "runtime/debug" "runtime/trace" "sync" ) const ( NumWorkers = 4 // Number of workers. NumTasks = 500 // Number of tasks. MemoryIntense = 10000 // Size of memory-intensive task (number of elements). ) func main() { // Write to the trace file. f, _ := os.Create("trace.out") trace.Start(f) defer trace.Stop() // Set the target percentage for the garbage collector. Default is 100%. debug.SetGCPercent(100) // Task queue and result queue. taskQueue := make(chan int, NumTasks) resultQueue := make(chan int, NumTasks) // Start workers. var wg sync.WaitGroup wg.Add(NumWorkers) for i := 0; i < NumWorkers; i++ { go worker(taskQueue, resultQueue, &wg) } // Send tasks to the queue. for i := 0; i < NumTasks; i++ { taskQueue <- i } close(taskQueue) // Retrieve results from the queue. go func() { wg.Wait() close(resultQueue) }() // Process the results. for result := range resultQueue { fmt.Println("Result:", result) } fmt.Println("Done!") } // Worker function. func worker(tasks <-chan int, results chan<- int, wg *sync.WaitGroup) { defer wg.Done() for task := range tasks { result := performMemoryIntensiveTask(task) results <- result } } // performMemoryIntensiveTask is a memory-intensive function. func performMemoryIntensiveTask(task int) int { // Create a large-sized slice. data := make([]int, MemoryIntense) for i := 0; i < MemoryIntense; i++ { data[i] = i + task } // Imitation of latency. time.Sleep(10 * time.Millisecond) // Calculate the result. result := 0 for eachValue := range data { result += eachValue } return result }
為了跟蹤程序的執(zhí)行,結(jié)果被寫入文件trace.out
:
// Writing to the trace file. f, _ := os.Create("trace.out") trace.Start(f) defer trace.Stop()
通過利用go tool trace
,我們可以觀察堆大小的波動并分析程序內(nèi)垃圾收集器的行為。
請注意,不同 Go 版本的具體細節(jié)和功能go tool trace
可能有所不同,因此建議查閱官方文檔以獲取特定于版本的信息。
GOGC的默認值
debug.SetGCPercent
GOGC參數(shù)可以通過包中的函數(shù)設(shè)置runtime/debug
。默認情況下,GOGC 配置為 100%。
要運行我們的程序,請使用以下命令:
go run main.go
程序執(zhí)行后,trace.out
會生成一個文件。要對其進行分析,請執(zhí)行以下命令:
go tool trace trace.out
當 GOGC 值為 100 時,垃圾收集器被觸發(fā) 16 次,在我們的示例中總共消耗了 14 毫秒。
增加 GC 頻率
如果我們在設(shè)置為 10% 后運行代碼debug.SetGCPercent(10)
,垃圾收集器會被更頻繁地調(diào)用。在這種情況下,當當前堆大小達到活動堆大小的 10% 時,垃圾收集器將激活。
換句話說,如果活動堆大小為 10 MB,則當當前堆大小達到 1 MB 時,垃圾收集器將啟動。
當 GOGC 值為 10 時,垃圾收集器被調(diào)用 38 次,總垃圾收集時間為 28 ms。
降低 GC 頻率
以 1000% 運行相同的程序debug.SetGCPercent(1000)
會導(dǎo)致垃圾收集器在當前堆大小達到活動堆大小的 1000% 時觸發(fā)。
在這種情況下,垃圾收集器被激活一次,執(zhí)行 2 毫秒。
禁用GC
您還可以通過設(shè)置 GOGC=off 或使用來禁用垃圾收集器debug.SetGCPercent(-1).
關(guān)閉 GC 后,應(yīng)用程序中的堆大小會持續(xù)增長,直到程序執(zhí)行為止。
堆內(nèi)存占用
在實時堆的實際內(nèi)存分配中,該過程不會像跟蹤中看到的那樣定期且可預(yù)測地發(fā)生。
活動堆可以隨著每個垃圾收集周期動態(tài)變化,并且在某些條件下,其絕對值可能會出現(xiàn)峰值。
為了模擬這種情況,在具有內(nèi)存限制的容器中運行程序可能會導(dǎo)致內(nèi)存不足 (OOM) 錯誤。
在此示例中,程序在內(nèi)存限制為 10 MB 的容器中運行以進行測試。Dockerfile說明如下:
FROM golang:latest as builder WORKDIR /src COPY . RUN go env -w GO111MODULE=on RUN go mod vendor RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -a -installsuffix cgo -o app ./cmd/ FROM golang:latest WORKDIR /root/ COPY --from=builder /src/app . EXPOSE 8080 CMD ["./app"]
Docker-compose 的描述是:
version: '3' services: my-app: build: context: . dockerfile: Dockerfile ports: - 8080:8080 deploy: resources: limits: memory: 10M
啟動容器會debug.SetGCPercent(1000%)
導(dǎo)致 OOM 錯誤:
docker-compose build docker-compose up
容器崩潰并顯示錯誤代碼 137,指示內(nèi)存不足的情況。
避免 OOM 錯誤
從 Go 1.19 版本開始,Golang 引入了帶有 GOMEMLIMIT 選項的“軟內(nèi)存管理”。此功能使用 GOMEMLIMIT 環(huán)境變量來設(shè)置 Go 運行時可以使用的總體內(nèi)存限制。例如,GOMEMLIMIT = 8MiB
,其中 8 MB 是內(nèi)存大小。
該機制旨在解決 OOM 問題。啟用 GOMEMLIMIT 后,會定期調(diào)用垃圾收集器以將堆大小保持在一定限制內(nèi),避免內(nèi)存過載。
成本性能問題
GOMEMLIMIT 是一個功能強大但也是雙刃劍的工具。它可能導(dǎo)致一種稱為“死亡螺旋”的情況。當由于實時堆增長或持續(xù)的 Goroutine 泄漏而導(dǎo)致整體內(nèi)存大小接近 GOMEMLIMIT 時,垃圾收集器將根據(jù)限制不斷調(diào)用。
頻繁的垃圾收集器調(diào)用可能會導(dǎo)致 CPU 使用率增加和程序性能下降。與 OOM 錯誤不同,死亡螺旋很難檢測和修復(fù)。
GOMEMLIMIT 不提供 100% 保證嚴格執(zhí)行內(nèi)存限制,從而允許內(nèi)存利用率超出限制。它還設(shè)置了 CPU 使用率限制,以防止過多的資源消耗。
在哪里申請 GOMEMLIMIT 和 GOGC
GOMEMLIMIT 在多種情況下都具有優(yōu)勢:
- 在內(nèi)存有限的容器中運行應(yīng)用程序,留下 5-10% 的可用內(nèi)存是一個很好的做法。
- 在處理資源密集型代碼時,GOMEMLIMIT 的實時管理可能會很有用。
- 當應(yīng)用程序作為容器中的腳本運行時,禁用垃圾收集器但設(shè)置 GOMEMLIMIT 可以提高性能并防止超出容器的資源限制。
在以下情況下避免使用 GOMEMLIMIT:
- 當您的程序已經(jīng)接近其操作環(huán)境的內(nèi)存限制時,不要定義內(nèi)存限制。
- 在您不監(jiān)督的執(zhí)行環(huán)境中部署程序時,請避免實施內(nèi)存限制,特別是當程序的內(nèi)存消耗與其輸入數(shù)據(jù)直接相關(guān)時。這對于命令行界面或桌面應(yīng)用程序等工具尤其重要。
顯然,通過采取故意的方法,我們可以有效地控制特定的程序設(shè)置,包括垃圾收集器和 GOMEMLIMIT。盡管如此,徹底評估實施這些設(shè)置的方法至關(guān)重要。
以上就是詳解Go中如何進行進行內(nèi)存優(yōu)化和垃圾收集器管理的詳細內(nèi)容,更多關(guān)于Go垃圾收集器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang基礎(chǔ)之waitgroup用法以及使用要點
WaitGroup是Golang并發(fā)的兩種方式之一,一個是Channel,另一個是WaitGroup,下面這篇文章主要給大家介紹了關(guān)于golang基礎(chǔ)之waitgroup用法以及使用要點的相關(guān)資料,需要的朋友可以參考下2023-01-01go語言中切片與內(nèi)存復(fù)制 memcpy 的實現(xiàn)操作
這篇文章主要介紹了go語言中切片與內(nèi)存復(fù)制 memcpy 的實現(xiàn)操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04