golang的記憶體相關內容

碼農船長發表於2020-12-03

前言

golang是自動記憶體管理和自動gc的,瞭解golang的記憶體細節不是必須的。但是如果明白golang記憶體方面的概念和編譯時、執行時的記憶體管理細節對寫出更高質量的程式碼是很有幫助的。

本文會介紹記憶體塊申請(memory block allocation)的實現和原理,編譯時和執行時的垃圾回收方面的內容。

記憶體塊/儲存塊(memory blocks)

記憶體塊是連續的記憶體段,用於在執行時託管值部分(value parts )。不同的記憶體塊可能有不同的大小,以便寄存不同的value parts。一個記憶體塊可能同時寄存多個的value parts,但是每個value part只能寄存在一個記憶體塊中,不管這個value part有多大。

簡單來說value parts就是一個值存放在不同記憶體塊的部分(Later, we call the parts (being distributed on different memory blocks) of a value as value parts)。比如go channel底層就有3個佇列實現。

一個記憶體塊可能包含多個value parts有很多原因。其中幾個:

  • 一個struct值通常有幾個欄位。所以當申請一個記憶體塊用來給一個struct值用時,這個記憶體塊也會用來存放這些欄位值。

  • 一個陣列通常會有很多的元素。所以當申請一個記憶體塊用來給陣列用時,這個記憶體塊也會用來存放資料元素。

  • 兩個slices的基礎元素序列可以託管在同一儲存塊上,並且元素序列還可以相互交疊。

值-引用-承載其value parts的記憶體塊

(A Value References the Memory Blocks Which Host Its Value Parts)

我們知道一個value part可以引用其它的value part。這裡,我們延申這個定義:一個記憶體塊被它所儲存的所有的value parts引用。所以如果一個value part V 被另外的一個value part引用,則另一個值也將間接引用存放v的記憶體塊。

記憶體塊什麼時候被申請的

下列是部分申請情形:

  • 顯式地呼叫new和make內建函式。一個new呼叫總是隻分配一個記憶體塊。一個make呼叫將會分配多於一個的記憶體塊,用來存放建立的slice,map或channel的直接部分和間接部分。

  • 建立maps, slices和匿名函式過程中會分配多於一個的記憶體塊。

  • 宣告變數(declare variables)

  • 將非介面值賦給介面值(assign non-interface values to interface values )。當非介面值不是一個指標值時。

  • 連線 非-常量字串(concatenate non-constant strings)。

  • 將字串轉換為位元組片或符文片,反之亦然,除了一些特殊的編譯器優化情況。(convert strings to byte or rune slices, and vice versa)

  • 將整形轉換成字串。convert integers to strings。

  • 呼叫內建函式append (當對應的slice的容量(capacity)不夠時)。

  • 給map插入新的鍵值對時(底下的hash table需要呼叫大小時)

在哪裡分配記憶體塊

對於由官方標準Go編譯器編譯的每個Go程式,在執行時,每個goroutine將維護一個堆疊,這是一個記憶體段。它充當一些記憶體塊的儲存池,以便從中分配記憶體。每個goroutine的初始堆疊大小很小(在64位系統上約為2k位元組)。堆疊大小將在goroutine執行中根據需要增大和縮小。

(請注意,對於標準的Go編譯器,每個goroutine可以擁有的堆疊大小是有限制的,標準的Go compiler 1.11其預設值,64位系統是1GB,32位系統是250MB。可以通過標準runtime/debug包中的SetMaxStack函式修改其值)

記憶體塊可以在堆疊中分配。在一個goroutine中的堆疊分配的記憶體塊只能被goroutine內部使用。它們是goroutine的本地資源。跨goroutines引用是不安全的。一個goroutine可以訪問或修改託管在這個goroutine堆疊上分配的儲存塊上的value parts,而無需使用任何資料同步技術。

堆(Heap)是每個程式中的一個單例。這是一個虛擬的概念。如果一個記憶體塊沒有在任何的goroutine的堆疊上分配,然後我們說它是在堆上分配的。Value parts存放在堆上的記憶體塊時是可以被多個goroutines使用的。換言之,是可以用於併發的。必要時應同步使用。

堆是一個候選的分配記憶體塊的地方。如果編譯器檢測到一個記憶體塊需要被跨goroutines引用或不好確定記憶體塊在一個goroutine的堆疊中是否安全時,在執行時就會在堆上分配這個記憶體塊。也就是說,有些值的堆疊上安全的,在堆上也是安全的。

實際上,堆疊對於Go程式不是必不可少的。Go編譯器/執行時可以在堆上分配所有記憶體塊。支援堆疊只是為了使Go程式更有效地執行:

  • 在堆疊上分配記憶體塊比堆更快。

  • 在堆疊上的記憶體塊不需要垃圾回收。

  • 堆疊記憶體塊比堆記憶體塊對CPU快取更友好。

如果在某處分配了記憶體塊,我們也可以說這個記憶體塊中的value parts是在同一個地方。(If a memory block is allocated somewhere, we can also say the value parts hosted on the memory block are allocated on the same place.)

如果函式中部分的本地變數的value parts在堆上分配,我們可以說值部分(和變數)轉儲到堆中。(If some value parts of a local variable declared in a function is allocated on heap, we can say the value parts (and the variable) escape to heap. )

藉助Go工具鏈,我們可以通過執行

go build -gcflags -m

來檢查哪些本地變數(value parts)會在執行時逃逸到椎上。如上所述,當前標準的Go編譯器的逃逸分析器(escape analyzer)還不很完美,很多local value parts本可以儲存在堆疊上即存到堆上。

一個在使用中的並在堆上分配的value part,必須至少被一個在堆疊上分配的value part所引用(An active value part allocated on heap still in use must be referenced by at least one value part allocated on a stack)。如果一個宣告為本地變數的值逃逸到椎上,同時假設它的型別是T,Go在執行時將在當前goroutine的堆疊上建立一個型別為型別*T的隱含指標。指標的值儲存變數在堆上的記憶體塊地址(也就是型別T的區域性變數的地址)。Go編譯器將在執行時用指標指向的值替換所有使用到這個變數的值。從稍後開始,堆疊上的* T指標值可能會被標記為無效,因此從它到堆上T值的引用關係將消失。從堆疊上的* T值到堆上的T值的引用關係在垃圾收集過程中起著重要作用,下面將對此進行描述。

簡單來說,我們可以視package-level變數是在堆上的,被一個隱式指標引用的值是在全域性記憶體區分配的。實際上,隱式指標引用了package-level變數的直接部分,而變數的直接部分則引用了其他一些value parts。

分配在堆上的記憶體塊可以被同時分配在不同堆疊上的多個值部分引用。

  • 如果一個struct值中的一個欄位逃逸到堆上,那整個struct值都移到堆上。

  • 如果陣列中一個元素逃逸到堆上,那整個陣列也逃逸到堆上。slice也一樣。

  • 如果值v被另外一個引用的值逃逸到堆上,它也將逃逸到堆上。

new函式建立的記憶體塊可能在堆疊或堆上。

什麼時候記憶體塊被回收?

由package-level的變數申請的記憶體塊永遠不會被回收。

一個goroutine的堆疊在這個goroutine退出時回收。所以在stack上分配的記憶體塊不用回收。stacks不是由垃圾回收器回收的。

heap上的記憶體塊,只有在goroutine堆疊和全域性儲存區上分配的所有值部分都不再(直接或間接)引用它時,才可以安全地回收它。這種情況下,這些記憶體塊也稱為不再使用的記憶體塊(unused memory blocks)。

這是一個示例,顯示何時可以收集一些記憶體塊:

package main
​
var p *int
​
func main() {
    done := make(chan bool)
    // "done" will be used in main and the following
    // new goroutine, so it will be allocated on heap.
​
    go func() {
        x, y, z := 123, 456, 789
        _ = z  // z can be allocated on stack safely.
        p = &x // For x and y are both ever referenced
        p = &y // by the global p, so they will be both
               // allocated on heap.
​
        // Now, x is not referenced by anyone, so
        // its memory block can be collected now.
​
        p = nil
        // Now, y is als not referenced by anyone,
        // so its memory block can be collected now.
​
        done <- true
    }()
​
    <-done
    // Now the above goroutine exits, the done channel
    // is not used any more, a smart compiler may
    // think it can be collected now.
​
    // ...
}
package main
​
import "fmt"
​
func main() {
    // Assume the length of the slice is so large
    // that its elements must be allocated on heap.
    bs := make([]byte, 1 << 31)
​
    // 編譯器機智地提前回收
    // A smart compiler can detect that the
    // underlying part of the slice bs will never be
    // used later, so that the underlying part of the
    // slice bs can be garbage collected safely now.
​
    fmt.Println(len(bs))
}

上面示例中,如果我們希望bs在fmt.Println呼叫前不被回收的話,可以呼叫runtime.KeepAlive告訴垃圾回收器不回收先。

package main
​
import "fmt"
import "runtime"
​
func main() {
    bs := make([]int, 1000000)
​
    fmt.Println(len(bs))
​
    // A runtime.KeepAlive(bs) call is also
    // okay for this specified example.
    runtime.KeepAlive(&bs)
}

不再使用的記憶體塊是如何檢測的?

當前的標準Go編譯器(版本1.15)使用併發的三色標記清除垃圾收集器。

不再使用的記憶體塊什麼時候被檢測?

垃圾回收不是一直進行的,是在一定的閾值下才觸發。可以通過runtime/debug.SetGCPercent 函式來設定這個環境變數。小的閾值頻率更高,負數是禁用自動垃圾回收。

也可以 runtime.GC 顯式開啟回收。

 

 

參考,好吧是部分翻譯。。。

https://go101.org/article/memory-block.html

相關文章