Go 接口内存泄漏

问题描述 投票:0回答:2

Go 似乎没有正确释放基于接口的指针。

package main //interface memory leakage

import (
    "fmt"
    "runtime"
)

type Interface interface{ Method(v any) }
type Implementation struct{}

func (i *Implementation) Method(v any) {}

func main() {
    pages := Pages()
    fmt.Printf("First  Memory Usage: %dMB, expected 0\n", good())      // i:=&Implementation{}; i.Method(data)
    fmt.Printf("Second Memory Usage: %dMB, while expected 0\n", bad()) // var i Interface; i=&Implementation{}; i.Method(data)

    var xor byte //do somthing
    for j := 0; j < NUM_OF_PAGES; j++ {
        xor ^= pages[j][PAGE_SIZE-1]
    }
    print("xor: ", xor)
}

var data = "123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_"
var LOOP_SIZE = 3 * 1000 * 1000

const NUM_OF_PAGES = 10
const PAGE_SIZE = 1024 * 1024

func Pages() [][]byte {
    var pages [][]byte
    for j := 0; j < NUM_OF_PAGES; j++ {
        p := make([]byte, PAGE_SIZE)
        for i := 0; i < len(p); i++ {
            p[i] = 'A'
        }
        pages = append(pages, p)
    }
    return pages
}

func good() int64 {
    var m0, m1 runtime.MemStats
    runtime.ReadMemStats(&m0)

    i := &Implementation{} //good line
    for n := 0; n < LOOP_SIZE; n++ {
        i.Method(data)
    }

    //runtime.GC()
    runtime.ReadMemStats(&m1)
    return int64(m1.Alloc-m0.Alloc) / 1024 / 1024
}

func bad() int64 {
    var m0, m1 runtime.MemStats
    runtime.ReadMemStats(&m0)

    var i Interface       //bad line
    i = &Implementation{} //bad line
    for n := 0; n < LOOP_SIZE; n++ {
        i.Method(data)
    }

    //runtime.GC()
    runtime.ReadMemStats(&m1)
    return int64(m1.Alloc-m0.Alloc) / 1024 / 1024
}

https://go.dev/play/p/0zlbPxs6WCV

这里的代码是一样的,第一次 Go 释放指针,但第二次,当我们使用基于接口的指针时,Go 可能无法正确处理内存。 我在代码中展示了好的和坏的语法,这两种语法有什么区别?为什么第二行会浪费内存()?

  • 第一:
    i:=&Implementation{}; i.Method(data) 
  • 第二个:
    var i Interface; i=&Implementation{}; i.Method(data)

输出也显示了问题:

First  Memory Usage: 0MB, expected 0
Second Memory Usage: 27MB, while expected 0  //However the number is variable but almost is different significantly

我希望第一次和第二次尝试的结果相同。

go memory memory-management memory-leaks dynamic-memory-allocation
2个回答
1
投票
i := &Implementation{}

第一个实现是指向直接类型的指针。 不需要做额外的工作,因为在编译时就知道需要在实例上调用哪个方法。

var i Interface
i = &Implementation{}

第二个实现将指针装箱为接口。 因为

i
的值可以是任何满足接口的值,所以不知道需要执行哪个方法,所以需要在该变量中存储更多信息。

当一个指针被装箱为一个接口时,它必须同时存储指针的类型和指针的值。因此,必须为附加的间接层进行附加分配。是在堆上进行分配还是在堆栈上进行优化由逃逸分析确定。


1
投票

让我们首先澄清您的程序中没有内存泄漏 - 与您的

bad
案例相比,您的
good
案例中有额外的堆分配,但在垃圾收集之后,所有内存都被释放。这意味着没有泄漏。

但是肯定有更多的堆分配。

有两种方法可以使您的

bad
案例具有与
good
案例一样多的堆使用率:

  1. 声明

    data
    变量为类型
    any

    var data any = "123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_"
    
  2. 声明方法的参数类型为

    string
    :

    type Interface interface{ Method(v string) }
    type Implementation struct{}
    func (i *Implementation) Method(v string) {}
    

在上述两种情况下,您无需为每次调用方法将字符串转换为接口,因此编译器不会在每次调用时执行堆分配。

bad
案例在堆上分配更多的真正原因是,在
good
案例中,Go 编译器知道确切的方法实现,并对其进行分析。它看到该方法什么都不做,并完全避免调用该方法。在
bad
的情况下,它看到接口上的方法调用,并且静态分析不认为此时唯一可能的实现是结构
Implementation
,因此它无法确定方法调用没有有什么影响。

如果你改变

Method
的实现来对参数做一些事情,比如将它存储在一个包范围的变量中,那么编译器就不能优化
good
中的方法调用,而
good
bad
将两者都在堆上分配相同的数量。

© www.soinside.com 2019 - 2024. All rights reserved.