0.1、索引
https://blog.waterflow.link/articles/1663406367769
1、內存管理
內存管理是管理計算機內存的過程,在主存和磁盤之間移動進程以提高系統的整體性能。內存管理的基本要求是提供方法來根據程序的請求動態的將部分內存分配給程序,並在不需要時釋放它以供重用。
程序通過將他們的內存劃分為執行特定任務的不同部分來管理他們。棧和堆就是這部分中的倆個,他們管理程序的未使用的內存並將其分配給不同類型的數據。當程序不再需要這些內存的時候就會釋放他們,供後續使用。
2、經典的內存模型
正在運行的程序將其數據保存在這些邏輯區域之中。操作系統在將邏輯加載到內存時為全局變量和靜態變量分配內存,並且在程序結束之前不會釋放它。這些值不會被修改。另外倆個區域,堆和棧,更多的是動態分配的變量。在這些區域中,程序根據需要去分配和釋放內存。這兩個區域之間的區別下面會説到。
文本段
文本段是目標文件或內存中程序的一部分,其中包含可執行指令。文本段一般放在堆或棧的下方,以防止堆棧溢出被覆蓋。
初始化數據和未初始化數據段
數據段是程序虛擬地址空間的一部分,其中包含由程序員初始化的全局變量和靜態變量。
棧
棧用於靜態內存分配,就像數據結構中的棧,遵循後進先出。通常函數和局部變量會在棧上分配,當數據被分配到棧上或是從棧上彈出時,實際上沒有任何物理移動,只有保存在棧中的值會被修改。這使得從棧中存儲和查詢數據的過程非常快,因為不需要查找。 我們可以從它最上面的塊中存儲和查詢數據。 存儲在棧上的任何數據都必須是有限且靜態的。 這意味着數據的大小在編譯時是已知的。 堆的內存管理簡單明瞭,由操作系統完成。 因為棧的大小是有限的,我們可能會在這裏遇到堆棧溢出錯誤。
堆
這裏的堆和數據結構中的堆是沒有關係的。堆用於動態內存分配。棧只允許在頂部進行分配和釋放,而程序可以在堆中的任何位置分配或釋放內存。程序必須以與其分配相反的順序將內存返回到棧。但是程序可以以任何順序將內存返回到堆中。這意味着堆比棧更靈活。指針、數組和大數據結構通常存儲在堆中。
存儲在堆上的數據必須形成一個足夠大的連續塊,以使用單個內存塊滿足請求。此屬性增加了堆的複雜性。首先,執行分配操作的代碼必須掃描堆,直到找到足夠大的連續內存塊來滿足請求。其次,當內存返回堆時,必須合併相鄰的已釋放塊,以更好地適應未來對大塊內存的請求。這種增加的複雜性意味着使用堆管理內存比使用堆棧慢。
堆內存分配方案不提供自動解除分配。我們需要使用垃圾回收器來刪除未使用的對象,以便有效地使用內存。
棧和堆的區別
- 與棧相比,堆更慢,因為查找數據的過程涉及更多。
- 堆比棧可以存儲更多的數據。
- 堆以動態大小存儲數據;棧以靜態大小存儲數據。
- 堆在應用程序的線程之間共享。
- 堆由於其動態特性而更難管理。
- 當我們談論內存管理時,我們主要是在談論管理堆內存。
- 堆內存分配不是線程安全的,因為存儲在此空間中的數據對所有線程都是可訪問或可見的。
內存分配的重要性
內存是有限的。如果一個程序繼續消耗內存而不釋放它,它將耗盡內存並自行崩潰。因此,軟件程序不能隨心所欲地繼續使用內存,它會導致其他程序和進程耗盡內存。由於這一點的重要性,大多數編程語言(包括 Go)都提供了自動內存管理的方法。
3、go的內存模型
Go 支持自動內存管理,例如自動內存分配和自動垃圾回收,避免了很多隱藏的 bug。
在 Go 中,每個線程都有自己的堆棧。當我們啓動一個線程時,我們分配一塊內存用作該線程的堆棧。當這塊內存不夠用時,問題就來了。為了克服這個問題,我們可以增加堆棧的默認大小,但增加堆棧的大小意味着每個線程的大小都會增加。如果我們想使用大量線程,這將非常低效。
另一種選擇是單獨決定每個線程的堆棧大小。同樣,在我們有很多線程的設置中,這將是低效的,因為我們需要弄清楚我們應該為每個堆棧分配多少內存。
Go 的創建者想出的不是給每個 goroutine 一個固定數量的堆棧內存,而是 Go 運行時嘗試根據需要為 goroutine 提供所需的堆棧空間。這樣我們在創建線程時就不需要考慮堆棧大小了。
goroutine 以2 kb的堆棧大小開始,可以根據需要增長和縮小。Go 檢查它即將執行的函數所需的堆棧數量是否可用,如果不夠用,則調用morestack分配一個新幀,然後才執行該函數。當該函數退出時,它的返回參數被複制回原始堆棧幀,並且任何不需要的堆棧空間都被釋放。
堆棧也有上限。如果達到此限制,我們的應用程序將panic並中止。
Go 在兩個地方分配內存:一個用於動態分配的全局堆和一個用於每個 goroutine 的本地堆棧。Go 與許多垃圾收集語言相比的一個主要區別是,許多對象直接分配在程序堆上。Go 更喜歡在棧上分配。棧分配代價更低,因為它只需要兩條 CPU 指令:一條推入棧進行分配,另一條從棧中釋放。
不幸的是,並非所有數據都可以使用棧上分配的內存。棧分配要求可以在編譯時確定變量的生命週期和內存佔用。如果無法確定,則在運行時動態分配到堆上。
Go 編譯器使用一個稱為逃逸分析的過程來查找其生命週期在編譯時已知的對象,並將它們分配到棧上而不是在垃圾回收的堆內存中。基本思想是在編譯時做垃圾回收的工作。編譯器跨代碼區域跟蹤變量的範圍。它使用這些數據來確定哪些變量持有一組檢查,以證明它們的生命週期在運行時是完全可知的。如果變量通過了這些檢查,則可以在棧上分配值。如果不是,就代表逃逸,並且必須進行堆分配。
內存是在棧上分配還是逃到堆上完全取決於你如何使用內存,而不是你如何聲明變量。
可以通過下面的命令查看是否有內存逃逸,go build -gcflags '-m'。
4、垃圾回收
垃圾回收是自動內存管理的一種形式。垃圾回收器嘗試回收由程序分配但不再被引用的內存。
Go 的垃圾回收器是一個非分代併發、三色標記和清理垃圾回收器。
分代垃圾回收器專注於最近分配的對象,因為它假設像臨時變量這樣的短期對象最常被回收。
Go 編譯器更喜歡在棧上分配對象,短期對象通常分配在棧上而不是堆上;這意味着不需要分代GC。
Go 的垃圾回收分為兩個階段,標記階段和清除階段。GC 使用三色算法來分析內存塊的使用情況。該算法首先將仍被引用的對象標記為“活躍”,並在下一階段(掃描)釋放不活躍對象的內存。
不用回收垃圾,但是可以減少垃圾
導致垃圾回收代價高主要因素之一是堆上的對象數量。通過優化我們的代碼以減少堆上長壽命對象的數量,我們可以最大限度地減少花費在 GC 上的資源,並提高我們的系統性能。
重構結構
在讀取數據時,現代計算機 CPU 的內部數據寄存器可以保存和處理 64 位。這稱為字長。它通常是 32 位或 64 位的。
當我們不對齊數據以適應字長時,會添加填充以正確對齊內存中的字段,以便下一個字段可以從一個字長倍數的偏移量開始。
當數據自然對齊時,現代 CPU 硬件最有效地執行對內存的讀取和寫入。Go 編譯器使用所需的對齊來確保並排存儲的內存可以使用公倍數訪問。它的值等於結構中最大字段所需的內存大小。
在 Go 中創建struct時,會為其分配一個連續的內存塊。Go 內存分配器不會針對數據結構對齊進行優化,因此通過重新排列結構的字段,您可以通過降低填充來降低內存使用量。
通常go中的類型對應的字節大小如下
/**
var a bool // 1字節
var b int16 // 2字節
var c int32 // 4字節
var d int64 // 8字節
var e int32 // 4字節
var f int64 // 8字節
var g int // 8字節
var h string // 16字節
var i float32 // 4字節
var j float64 // 8字節
var k interface{} // 16字節
var l time.Time // 24字節,結構體字節數不穩定
var m time.Timer // 80字節,結構體字節不穩定
var n time.Duration // 8字節
var o []byte // 24字節
**/
例如,下面 User
type User1 struct {
Age uint8 // 1字節
Hunger int64 // 8字節
Happy bool // 1字節
}
可以看到10個字節就能保存這些屬性,但是我們可以看下實際佔用了多少字節:
go run struct.go
Size of main.User1 struct: 24 bytes
我們可以修改下User的結構
type User1 struct {
Hunger int64 // 8字節
Age uint8 // 1字節
Happy bool // 1字節
}
看下結果,減少了8個字節的長度
go run struct.go
Size of main.User1 struct: 16 bytes
訣竅就是根據字段的大小降序排列這些字段,後面的Age和Happy因為沒有超過一個機器字,非配了8個字節。所以總共分配了16字節。
完整的代碼如下:
package main
import (
"fmt"
"unsafe"
)
/**
var a bool // 1字節
var b int16 // 2字節
var c int32 // 4字節
var d int64 // 8字節
var e int32 // 4字節
var f int64 // 8字節
var g int // 8字節
var h string // 16字節
var i float32 // 4字節
var j float64 // 8字節
var k interface{} // 16字節
var l time.Time // 24字節,結構體字節數不穩定
var m time.Timer // 80字節,結構體字節不穩定
var n time.Duration // 8字節
var o []byte // 24字節
var p uint8 // 1字節
**/
type User1 struct {
Age uint8 // 1字節
Hunger int64 // 8字節
Happy bool // 1字節
}
type User2 struct {
Hunger int64 // 8字節
Age uint8 // 1字節
Happy bool // 1字節
}
var user1 User1
var user2 User2
func main() {
fmt.Printf("Size of %T struct: %d bytes\n", user1, unsafe.Sizeof(user1))
fmt.Printf("Size of %T struct: %d bytes\n", user2, unsafe.Sizeof(user2))
}
減少長生命週期對象的數量
與其讓對象存在於堆上,不如將它們創建為值而不是按需引用。例如,如果我們需要用户請求中每個項目的一些數據,而不是預先計算並將其存儲在一個長期存在的映射中,我們可以基於每個請求計算它以減少堆上的對象數量。
刪除指針內的指針
如果我們有一個對象的引用,並且對象本身包含更多的指針,那麼這些都被認為是堆上的單個對象,即使它們可能是嵌套的。通過將這些嵌套值更改為非指針,我們可以減少要掃描的對象的數量。
避免不必要的字符串/字節數組分配
由於字符串/字節數組在底層被視為指針,因此每個數組都是堆上的一個對象。如果可能,嘗試將它們表示為其他非指針值,例如整數/浮點數、時間。
原文:
https://medium.com/@ali.can/memory-optimization-in-go-23a56544ccc0