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
我希望第一次和第二次尝试的结果相同。
i := &Implementation{}
第一个实现是指向直接类型的指针。 不需要做额外的工作,因为在编译时就知道需要在实例上调用哪个方法。
var i Interface
i = &Implementation{}
第二个实现将指针装箱为接口。 因为
i
的值可以是任何满足接口的值,所以不知道需要执行哪个方法,所以需要在该变量中存储更多信息。
当一个指针被装箱为一个接口时,它必须同时存储指针的类型和指针的值。因此,必须为附加的间接层进行附加分配。是在堆上进行分配还是在堆栈上进行优化由逃逸分析确定。
让我们首先澄清您的程序中没有内存泄漏 - 与您的
bad
案例相比,您的 good
案例中有额外的堆分配,但在垃圾收集之后,所有内存都被释放。这意味着没有泄漏。
但是肯定有更多的堆分配。
有两种方法可以使您的
bad
案例具有与 good
案例一样多的堆使用率:
声明
data
变量为类型any
:
var data any = "123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_"
声明方法的参数类型为
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
将两者都在堆上分配相同的数量。