go

go语言中的"内存泄漏"

2019-04-16  本文已影响563人  golang推广大使

go语言中“内存泄漏”案例

当使用一种自动垃圾回收的语言编程时,通常我们不要担心内存泄漏问题,因为运行时系统将经常搜集未使用的内存。然而,我们还是需要留意一些特殊可能导致某种内存泄漏的案例。本文剩下的部分将列出这些案例。

substring导致的“内存泄漏”

Go规范没有指定子字符串表达式中涉及的结果字符串和基本字符串是否应共享相同的底层内存块以承载两个字符串的基础字节序列。标准的go编译器或者运行时不会让他们共享同一块底层内存块。这是一个对内存和cpu占用来说都很好的设计。但是他或许会引起内存泄漏。
例如,下面的例子中,demo函数被调用后,将会有大概1M字节的内存泄漏,直到包级别的变量s0在别的地方呗修改:

var s0 string // a package-level variable

// A demo purpose function.
func f(s1 string) {
    s0 = s1[:50]
    // Now, s0 shares the same underlying memory block
    // with s1. Although s1 is not alive now, but s0
    // is still alive, so the memory block they share
    // couldn't be collected, though there are only 50
    // bytes used in the block and all other bytes in
    // the block become unavailable.
}

func demo() {
    s := createStringWithLengthOnHeap(1 << 20) // 1M bytes
    f(s)
}

为了避免内存泄漏,我们可以将子字符串转换成[]byte类型,然后再转回string类型

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

上面避免内存泄漏的代价是在转换过程中耗费两个50字节的副本,其中一个没用必要。
我们可以使用一种标准go编译器的优化方法来避免这个副本。不过要浪费一点额外的内存

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

上面的方法缺点是编译器的优化可能会在将来失效,并且在其他编译器中不可用。
第三种避免内存泄漏的方法是使用自go1.10以来支持的strings.Builder方法

import "strings"

func f(s1 string) {
    var b strings.Builder
    b.Grow(50)
    b.WriteString(s1[:50])
    s0 = b.String()
}

第三种方法的缺点是有点冗长。好消息是从go1.12开始我们可以调用带值为1的count参数的strings.Repeat函数来克隆一个字符串。从go1.12开始,strings.Repeat函数的底层实现将使用strings.Builder,以避免使用不必要的副本。

子切片引起的“内存泄漏”

与子字符串相似,子切片也能引起内存泄漏。下面的代码中,g函数被调用后,托管s1元素的内存块占用的大部分内存都将泄漏。

var s0 []int

func g(s1 []int) {
    // Assume the length of s1 is much larger than 30.
    s0 = s1[len(s1)-30:]
}

如果我们想避免那种内存泄漏,我们必须复制s0的30个元素,这样s0的存活性将不会阻止GC回收s1的元素所占用的的内存块。

func g(s1 []int) {
    s0 = append([]int(nil), s1[len(s1)-30:]...)
    // Now, the memory block hosting the elements
    // of s1 can be collected if no other values
    // are referencing the memory block.
}

不重置丢失的切片元素中的指针引起的“内存泄漏”

在下面的代码中,在调用h函数之后,为切片s的第一个和最后一个元素分配的内存块将泄漏。

func h() []*int {
    s := []*int{new(int), new(int), new(int), new(int)}
    // do something with s ...

    return s[1:3:3]
}

只要返回的切片仍然存在,它将阻止s的任何元素被收集,这样可以防止为s的第一个和最后一个元素引用的两个int值分配的两个内存块被收集。
如果我们想避免这种类型的内存泄漏,我们必须重置存储在丢失元素中的指针。

func h() []*int {
    s := []*int{new(int), new(int), new(int), new(int)}
    // do something with s ...

    // Reset pointer values.
    s[0], s[len(s)-1] = nil, nil
    return s[1:3:3]
}

被卡住的goroutine引起的“内存泄漏”

有时候,Go程序中的一些goroutines可能永远处于阻塞状态。这种goroutines被称为卡住的goroutines。Go运行时不会杀死挂起的goroutine,因此为挂起的goroutine分配的资源(以及引用的内存块)永远不会被垃圾收集。
Go运行时不会杀死挂goroutines有两个原因。一个是有时Go运行时很难判断阻塞goroutine是否会被永久阻止。另一个有时候我们故意让goroutine悬挂。例如,有时我们可能会让Go程序的主要goroutine挂起以避免程序退出。

不在使用的但是没有stop的time.Ticker导致的“内存泄漏”

当time.Timer值不再使用时,它将在一段时间后被垃圾收集。但是对于一个时间来说这不是真的.Ticker值。我们应该停止一个时间。不再使用时的标签值。

不正确使用终结器导致的“内存泄漏”

为作为循环引用组的成员的值设置终结器可以防止收集为循环引用组分配的所有存储器块。这是真正的内存泄漏。
例如,在调用以下函数并退出之后,为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

    // The SetFinalizer call makes x escape to heap.
    runtime.SetFinalizer(&x, finalizer)

    // The following line forms a cyclic reference
    // group with two members, x and y.
    // This causes x and y are not collectable.
    x.t, y.t = &y, &x // y also escapes to heap.
}

因此,请避免为循环引用组中的值设置终结器。
顺便说一句,我们不应该使用终结器作为对象析构函数。

上一篇下一篇

猜你喜欢

热点阅读