写点什么

2023-05-26:golang 关于垃圾回收和析构函数的选择题,多数人会选错。

  • 2023-05-26
    北京
  • 本文字数:6724 字

    阅读完需:约 22 分钟

2023-05-26:golang 关于垃圾回收和析构的选择题,代码如下:


package main
import ( "fmt" "runtime" "time")
type ListNode struct { Val int Next *ListNode}
func main0() { a := &ListNode{Val: 1} b := &ListNode{Val: 2} runtime.SetFinalizer(a, func(obj *ListNode) { fmt.Printf("a被回收--") }) runtime.SetFinalizer(b, func(obj *ListNode) { fmt.Printf("b被回收--") }) a.Next = b b.Next = a}
func main() { main0() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() fmt.Print("结束")}
复制代码


代码的运行结果是什么?并说明原因。注意析构是无序的。


A. 结束


B. a 被回收--b 被回收--结束


C. b 被回收--a 被回收--结束


D. B 和 C 都有可能

答案 2023-05-26:

golang 的垃圾回收算法跟 java 一样,都是根可达算法。代码中 main0 函数里 a 和 b 是互相引用,但是 a 和 b 没有外部引用。因此 a 和 b 会被当成垃圾被回收掉。而析构函数的调用不是有序的,所以 B 和 C 都有可能,答案选 D。让我们看看答案是什么,如下:



看运行结果,答案不是选 D,而是选 A。这肯定会出乎很多人意料,golang 的垃圾回收算法是根可达算法难不成是假的,大家公认的八股文难道是错的?有这个疑问是好事,但不能全盘否定。让我们看看析构函数的源码吧。代码在 src/runtime/mfinal.go 中,如下:


// SetFinalizer sets the finalizer associated with obj to the provided// finalizer function. When the garbage collector finds an unreachable block// with an associated finalizer, it clears the association and runs// finalizer(obj) in a separate goroutine. This makes obj reachable again,// but now without an associated finalizer. Assuming that SetFinalizer// is not called again, the next time the garbage collector sees// that obj is unreachable, it will free obj.//// SetFinalizer(obj, nil) clears any finalizer associated with obj.//// The argument obj must be a pointer to an object allocated by calling// new, by taking the address of a composite literal, or by taking the// address of a local variable.// The argument finalizer must be a function that takes a single argument// to which obj's type can be assigned, and can have arbitrary ignored return// values. If either of these is not true, SetFinalizer may abort the// program.//// Finalizers are run in dependency order: if A points at B, both have// finalizers, and they are otherwise unreachable, only the finalizer// for A runs; once A is freed, the finalizer for B can run.// If a cyclic structure includes a block with a finalizer, that// cycle is not guaranteed to be garbage collected and the finalizer// is not guaranteed to run, because there is no ordering that// respects the dependencies.//// The finalizer is scheduled to run at some arbitrary time after the// program can no longer reach the object to which obj points.// There is no guarantee that finalizers will run before a program exits,// so typically they are useful only for releasing non-memory resources// associated with an object during a long-running program.// For example, an os.File object could use a finalizer to close the// associated operating system file descriptor when a program discards// an os.File without calling Close, but it would be a mistake// to depend on a finalizer to flush an in-memory I/O buffer such as a// bufio.Writer, because the buffer would not be flushed at program exit.//// It is not guaranteed that a finalizer will run if the size of *obj is// zero bytes, because it may share same address with other zero-size// objects in memory. See https://go.dev/ref/spec#Size_and_alignment_guarantees.//// It is not guaranteed that a finalizer will run for objects allocated// in initializers for package-level variables. Such objects may be// linker-allocated, not heap-allocated.//// Note that because finalizers may execute arbitrarily far into the future// after an object is no longer referenced, the runtime is allowed to perform// a space-saving optimization that batches objects together in a single// allocation slot. The finalizer for an unreferenced object in such an// allocation may never run if it always exists in the same batch as a// referenced object. Typically, this batching only happens for tiny// (on the order of 16 bytes or less) and pointer-free objects.//// A finalizer may run as soon as an object becomes unreachable.// In order to use finalizers correctly, the program must ensure that// the object is reachable until it is no longer required.// Objects stored in global variables, or that can be found by tracing// pointers from a global variable, are reachable. For other objects,// pass the object to a call of the KeepAlive function to mark the// last point in the function where the object must be reachable.//// For example, if p points to a struct, such as os.File, that contains// a file descriptor d, and p has a finalizer that closes that file// descriptor, and if the last use of p in a function is a call to// syscall.Write(p.d, buf, size), then p may be unreachable as soon as// the program enters syscall.Write. The finalizer may run at that moment,// closing p.d, causing syscall.Write to fail because it is writing to// a closed file descriptor (or, worse, to an entirely different// file descriptor opened by a different goroutine). To avoid this problem,// call KeepAlive(p) after the call to syscall.Write.//// A single goroutine runs all finalizers for a program, sequentially.// If a finalizer must run for a long time, it should do so by starting// a new goroutine.//// In the terminology of the Go memory model, a call// SetFinalizer(x, f) “synchronizes before” the finalization call f(x).// However, there is no guarantee that KeepAlive(x) or any other use of x// “synchronizes before” f(x), so in general a finalizer should use a mutex// or other synchronization mechanism if it needs to access mutable state in x.// For example, consider a finalizer that inspects a mutable field in x// that is modified from time to time in the main program before x// becomes unreachable and the finalizer is invoked.// The modifications in the main program and the inspection in the finalizer// need to use appropriate synchronization, such as mutexes or atomic updates,// to avoid read-write races.func SetFinalizer(obj any, finalizer any) {  if debug.sbrk != 0 {    // debug.sbrk never frees memory, so no finalizers run    // (and we don't have the data structures to record them).    return  }  e := efaceOf(&obj)  etyp := e._type  if etyp == nil {    throw("runtime.SetFinalizer: first argument is nil")  }  if etyp.kind&kindMask != kindPtr {    throw("runtime.SetFinalizer: first argument is " + etyp.string() + ", not pointer")  }  ot := (*ptrtype)(unsafe.Pointer(etyp))  if ot.elem == nil {    throw("nil elem type!")  }
if inUserArenaChunk(uintptr(e.data)) { // Arena-allocated objects are not eligible for finalizers. throw("runtime.SetFinalizer: first argument was allocated into an arena") }
// find the containing object base, _, _ := findObject(uintptr(e.data), 0, 0)
if base == 0 { // 0-length objects are okay. if e.data == unsafe.Pointer(&zerobase) { return }
// Global initializers might be linker-allocated. // var Foo = &Object{} // func main() { // runtime.SetFinalizer(Foo, nil) // } // The relevant segments are: noptrdata, data, bss, noptrbss. // We cannot assume they are in any order or even contiguous, // due to external linking. for datap := &firstmoduledata; datap != nil; datap = datap.next { if datap.noptrdata <= uintptr(e.data) && uintptr(e.data) < datap.enoptrdata || datap.data <= uintptr(e.data) && uintptr(e.data) < datap.edata || datap.bss <= uintptr(e.data) && uintptr(e.data) < datap.ebss || datap.noptrbss <= uintptr(e.data) && uintptr(e.data) < datap.enoptrbss { return } } throw("runtime.SetFinalizer: pointer not in allocated block") }
if uintptr(e.data) != base { // As an implementation detail we allow to set finalizers for an inner byte // of an object if it could come from tiny alloc (see mallocgc for details). if ot.elem == nil || ot.elem.ptrdata != 0 || ot.elem.size >= maxTinySize { throw("runtime.SetFinalizer: pointer not at beginning of allocated block") } }
f := efaceOf(&finalizer) ftyp := f._type if ftyp == nil { // switch to system stack and remove finalizer systemstack(func() { removefinalizer(e.data) }) return }
if ftyp.kind&kindMask != kindFunc { throw("runtime.SetFinalizer: second argument is " + ftyp.string() + ", not a function") } ft := (*functype)(unsafe.Pointer(ftyp)) if ft.dotdotdot() { throw("runtime.SetFinalizer: cannot pass " + etyp.string() + " to finalizer " + ftyp.string() + " because dotdotdot") } if ft.inCount != 1 { throw("runtime.SetFinalizer: cannot pass " + etyp.string() + " to finalizer " + ftyp.string()) } fint := ft.in()[0] switch { case fint == etyp: // ok - same type goto okarg case fint.kind&kindMask == kindPtr: if (fint.uncommon() == nil || etyp.uncommon() == nil) && (*ptrtype)(unsafe.Pointer(fint)).elem == ot.elem { // ok - not same type, but both pointers, // one or the other is unnamed, and same element type, so assignable. goto okarg } case fint.kind&kindMask == kindInterface: ityp := (*interfacetype)(unsafe.Pointer(fint)) if len(ityp.mhdr) == 0 { // ok - satisfies empty interface goto okarg } if iface := assertE2I2(ityp, *efaceOf(&obj)); iface.tab != nil { goto okarg } } throw("runtime.SetFinalizer: cannot pass " + etyp.string() + " to finalizer " + ftyp.string())okarg: // compute size needed for return parameters nret := uintptr(0) for _, t := range ft.out() { nret = alignUp(nret, uintptr(t.align)) + uintptr(t.size) } nret = alignUp(nret, goarch.PtrSize)
// make sure we have a finalizer goroutine createfing()
systemstack(func() { if !addfinalizer(e.data, (*funcval)(f.data), nret, fint, ot) { throw("runtime.SetFinalizer: finalizer already set") } })}
复制代码


看代码,看不出什么。其端倪在注释中。注意如下注释:


// Finalizers are run in dependency order: if A points at B, both have


// finalizers, and they are otherwise unreachable, only the finalizer


// for A runs; once A is freed, the finalizer for B can run.


// If a cyclic structure includes a block with a finalizer, that


// cycle is not guaranteed to be garbage collected and the finalizer


// is not guaranteed to run, because there is no ordering that


// respects the dependencies.


这段英文翻译成中文如下:


Finalizers(终结器)按照依赖顺序运行:如果 A 指向 B,两者都有终结器,并且它们除此之外不可达,则仅运行 A 的终结器;一旦 A 被释放,可以运行 B 的终结器。如果一个循环结构包含一个具有终结器的块,则该循环体不能保证被垃圾回收并且终结器不能保证运行,因为没有符合依赖关系的排序方式。


这意思很明显了,析构函数会检查当前对象 A 是否有外部对象指向当前对象 A。如果有外部对象指向当前对象 A 时,A 的析构是无法执行的;如果有外部对象指向当前对象 A 时,A 的析构才能执行。


代码中的 a 和 b 是循环依赖,当析构判断 a 和 b 时,都会有外部对象指向 a 和 b,析构函数无法执行。析构无法执行,内存也无法回收。因此答案选 A。


去掉析构函数后,a 和 b 肯定会被释放的。不用析构函数去证明,那如何证明呢?用以下代码就可以证明,代码如下:


package main
import ( "fmt" "runtime" "time")
type ListNode struct { Val [1024 * 1024]bool Next *ListNode}
func printAlloc() { var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("%d KB\n", m.Alloc/1024)}
func main0() { printAlloc() a := &ListNode{Val: [1024 * 1024]bool{true}} b := &ListNode{Val: [1024 * 1024]bool{false}}
a.Next = b b.Next = a
// runtime.SetFinalizer(a, func(obj *ListNode) { // fmt.Printf("a被删除--") // })
printAlloc()
}
func main() { fmt.Print("开始") main0() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() fmt.Print("结束") printAlloc()
}
复制代码



根据运行结果,内存大小明显变小,说明 a 和 b 已经被回收了。


让我们再看看有析构函数的情况,运行结果是咋样的,如下:


package main
import ( "fmt" "runtime" "time")
type ListNode struct { Val [1024 * 1024]bool Next *ListNode}
func printAlloc() { var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("%d KB\n", m.Alloc/1024)}
func main0() { printAlloc() a := &ListNode{Val: [1024 * 1024]bool{true}} b := &ListNode{Val: [1024 * 1024]bool{false}}
a.Next = b b.Next = a
runtime.SetFinalizer(a, func(obj *ListNode) { fmt.Printf("a被删除--") })
printAlloc()
}
func main() { fmt.Print("开始") main0() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() time.Sleep(1 * time.Second) runtime.GC() fmt.Print("结束") printAlloc()
}
复制代码



根据运行结果,有析构函数的情况下,a 和 b 确实是无法被回收。

总结

1.不要怀疑八股文的正确性,golang 的垃圾回收确实是根可达算法。


2.不要用析构函数去测试无用对象被回收的情况,上面的例子也看到了,两对象的循环引用,析构函数的测试结果就是错误的。只能根据内存变化,看无用对象是否被回收。


3.在写代码的时候,能手动设置引用为 nil,最好手动设置,这样能更好的避免内存泄漏。

发布于: 刚刚阅读数: 5
用户头像

公众号:福大大架构师每日一题 2021-02-15 加入

公众号:福大大架构师每日一题

评论

发布
暂无评论
2023-05-26:golang关于垃圾回收和析构函数的选择题,多数人会选错。_golang_福大大架构师每日一题_InfoQ写作社区