Утечки памяти в Go

Go

Эта статья переведена с (Memory Leaking) Авторские права @ принадлежат оригинальному тексту.

При программировании на языке со сборщиком мусора (GC), как правило, нам не нужно заботиться об утечках памяти, поскольку среда выполнения языка периодически собирает неиспользуемую память. Или частный случай реальной утечки памяти.В оставшейся части этой статьи будет перечислено несколько таких случаев.

Незначительная утечка памяти из-за подстрок

Спецификация Go не указывает, должны ли результирующая строка и базовая строка, участвующие в выражении подстроки, совместно использовать один и тот же базовый блок памяти, в котором размещается базовая последовательность байтов для обеих строк.Стандартный компилятор/среда выполнения Go заставляет их совместно использовать один и тот же базовый блок памяти , Это хороший дизайн, и это мудрый шаг для потребления памяти и процессора, но это может привести к утечкам памяти.

Например, вызов следующей функцииf, произойдет утечка памяти объемом 1 Мбайт (незначительная), пока переменные уровня пакета не будут изменены в другом месте.s0.

var s0 string // package level variable

func f(s1 string) {
// 假设 s1 是一个长度大于 50 的字符串.
s0 = s1[:50]
// 现在, s0 和 s1 共享相同的底层内存块.
// s1 现在不存活了, 但是 s0 依然存活.
// 尽管仅有 50 个字节在内存块中,
// s0 仍旧存活的事实阻止了这 1M 字节的内存块被回收.
}

Чтобы избежать этой небольшой утечки памяти, мы можем преобразовать подстроку в[]byteзначение, то[]byteзначение преобразовано обратноstring.

func f(s1 string) {
s0 = string([]byte(s1[:50]))
}

Недостатком описанного выше метода предотвращения этой небольшой утечки памяти является то, что при преобразовании происходит копирование 50 байтов, один из которых не нужен.

Что мы можем сделать со стандартным компилятором Goоптимизациячтобы избежать единственной копии с небольшими дополнительными затратами на трату байта.

func f(s1 string) {
s0 = (" " + s1[:50])[1:]
}

Недостатком описанного выше подхода является то, что оптимизации компилятора могут впоследствии дать сбой, а оптимизации могут не работать с другими компиляторами.

Третий способ избежать утечек памяти типов — использовать то, что не поддерживалось до версии Go 1.10.strings.Builder.

import "strings"

func f(s1 string) {
var b strings.Builder
b.Grow(50)
b.WriteString(s1[:50])
s0 = b.String()
// b.Reset() // 如果 b 在其他地方会用到, 那么它必须在这里重置掉.
}

Недостаток третьего способа в том, что он немного многословен (по сравнению с первыми двумя).

Незначительная утечка памяти, вызванная субсрезами

Подобно нахождению подстроки, нахождение подводных классов также может вызвать легкую утечку памяти. В следующем ниже, вызываяgПосле функции, несяs1Большая часть памяти, занимаемой блоком памяти элемента, будет потеряна (если больше нет значений, ссылающихся на блок памяти).

var s0 []int

func g(s1 []int) {
// 假设 s1 的长度远远大于 30.
s0 = s1[len(s1)-30:]
}

Если мы хотим избежать этой небольшой утечки памяти, мы должны скопироватьs030 элементов , так чтоs0выживание не мешаетs1Блок памяти элемента освобождается.

func g(s1 []int) {
s0 = append([]int(nil), s1[len(s1)-30:]...)
}

Незначительная утечка памяти, вызванная сбросом указателей на уцелевшие элементы среза.

В приведенном ниже коде вызовитеgПосле функции присвойте срезуsБлок памяти для первого элемента теряется.Если последний элемент никогда не используется в качестве элемента какого-либо среза после этого, блок памяти, выделенный для последнего элемента, также теряется.

func g() []*int {
s := []*int{new(int), new(int), new(int), new(int)}
return s[1:3]
}

Если возвращенный фрагмент все еще жив, это предотвратит сборsосновной блок памяти элемента, тем самым предотвращаяsДва блока памяти, выделенные от первого до последнего элемента, собираются, даже если эти два элемента больше не активны.

Если мы хотим избежать этой небольшой утечки памяти, мы должны сбросить указатели в неживых элементах (здесь, в функцииhПосле вызова первый и последний элементы считаются мертвыми).

func h() []*int {
s := []*int{new(int), new(int), new(int), new(int)}
s1 := s[1:3]
s[0] = nil; s[len(s)-1] = nil
return s1
}

Нам часто приходится сбрасыватьОперация удаления элемента срезаУказатель на невыживаемый элемент в .

Утечки памяти, вызванные потерянными горутинами

Иногда из-за какой-то логической ошибки в дизайне кода одна или несколько горутин блокируются навсегда, что приводит к тому, что многие блоки кода, используемые в этих горутинах, никогда не удаляются сборщиком мусора Это настоящая утечка памяти.

Например, если следующие функции используются в качестве функции запуска GOROUTINE и передают параметр канала Nil для него, Goroutine всегда будет блокировать. Go Runtime думает, что Goroutine все еще живет, такsВыделенный блок памяти никогда не будет собран.

func k(c <-chan bool) {
s := make([]int64, 1e6)
if <-c { // 如果 c 为 nil, 这里将永远阻塞
_ = s
// 使用 s, ...
}
}

Мы должны избегать этой логической ошибки.

Финализаторы

Установите финализаторы для участников в циклической справочной группеможет препятствовать сбору всех блоков памяти, выделенных для этой циклической группы ссылок., Это не мелкая, а настоящая утечка памяти.

После того, как следующие функции будут вызваны и завершены,xа такжеyНе гарантируется, что выделенный блок памяти будет собран сборщиком мусора в будущем.

func memoryLeaking() {
type T struct {
v [1<<20]int
t *T
}

var finalizer = func(t *T) {
fmt.Println("finalizer called")
}

var x, y T

// SetFinalizer 会使 x 逃逸到堆上.
runtime.SetFinalizer(&x, finalizer)

// 以下语句将导致 x 和 y 变得无法收集.
x.t, y.t = &y, &x // y 也逃逸到了 堆上.
}

Поэтому избегайте установки финализаторов для значений в круговых ссылочных группах.