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

这篇具有很好参考价值的文章主要介绍了2023-05-26:golang关于垃圾回收和析构函数的选择题,多数人会选错。。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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。让我们看看答案是什么,如下:

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

看运行结果,答案不是选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()

}

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

根据运行结果,内存大小明显变小,说明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()

}

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

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

总结

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

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

3.在写代码的时候,能手动设置引用为nil,最好手动设置,这样能更好的避免内存泄漏。文章来源地址https://www.toymoban.com/news/detail-460221.html

到了这里,关于2023-05-26:golang关于垃圾回收和析构函数的选择题,多数人会选错。的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++ 构造函数和析构函数 详解

    C++中用构造函数和析构函数来初始化和清理对象,这两个函数将会被编译器自动调用。对象的初始化和清理是非常重要的,如果我们不提供构造函数与析构函数,编译器会自动提供两个函数的空实现。 构造函数:主要作用于创建函数时对对象成员的属性赋值。 析构函数:主

    2024年02月02日
    浏览(52)
  • 【C++初阶】构造函数和析构函数

    📖 默认成员函数 用户没有显式实现,编译器会自动生成的成员函数,称为默认成员函数。 构造函数 :完成对象的初始化工作。 析构函数 :完成对象空间的清理工作。 拷贝构造 :使用同类对象初始化创建对象。 赋值重载 :把一个对象赋值给另外一个对象(该对象已存在

    2024年02月17日
    浏览(51)
  • 『C++成长记』构造函数和析构函数

    🔥 博客主页: 小王又困了 📚 系列专栏: C++ 🌟 人之为学,不日近则日退  ❤️ 感谢大家点赞👍收藏⭐评论✍️ 目录 一、类的六个个默认成员函数 📒1.1认识默认成员函数  二、构造函数 📒2.1为什么要有构造函数 📒2.2构造函数的概念 📒2.3构造函数的特性 📒2.4编译器

    2024年02月05日
    浏览(46)
  • C++初阶——构造和析构函数

    目录 1. 我们在经常忘记写或者调用初始化函数使得栈类中的变量是随机值,易出错有时忘记调用销毁函数,导致内存泄漏 2.构造函数 2.2 构造函数的特性 3. 析构函数 3.1 概念 3.2 特性 3.3 深入了解  这里就引出了 构造和析构函数 2.1 特性: 函数名与类名相同 无返回值 对象实例化

    2024年02月14日
    浏览(39)
  • C++构造函数和析构函数(附示例代码)

    构造函数和析构函数是面向对象编程中的重要概念,用于创建和销毁对象。 构造函数 是一种特殊的函数,它在对象被创建时自动调用。它的作用是初始化对象的状态,例如设置成员变量的默认值、分配资源等操作。构造函数与类同名且没有返回值,可以有多个重载版本,同

    2024年02月03日
    浏览(64)
  • C语言大师(5)构造函数和析构函数

    引言 在C++的面向对象编程中,构造函数和析构函数扮演着至关重要的角色。它们分别管理对象的初始化和销毁过程,确保资源的有效分配和释放。了解这些函数如何工作,对于编写高效和可靠的C++程序至关重要。 1. 构造函数 构造函数在每次创建类的新对象时自动调用。它主

    2024年01月22日
    浏览(90)
  • 【C++杂货铺】构造函数和析构函数

    📖 默认成员函数 用户没有显式实现,编译器会自动生成的成员函数,称为默认成员函数。 构造函数 :完成对象的初始化工作。 析构函数 :完成对象空间的清理工作。 拷贝构造 :使用同类对象初始化创建对象。 赋值重载 :把一个对象赋值给另外一个对象(该对象已存在

    2024年02月16日
    浏览(50)
  • 【c++】类和对象(三)构造函数和析构函数

    🔥 个人主页 :Quitecoder 🔥 专栏 :c++笔记仓 朋友们大家好,本篇文章我们带来类和对象重要的部分, 构造函数和析构函数 如果一个类中什么成员都没有,简称为空类 任何类在什么都不写时,编译器会自动生成以下6个默认成员函数 (用户没有显式实现,编译器会生成的成

    2024年04月12日
    浏览(49)
  • 【C++】类和对象(中)---构造函数和析构函数

    个人主页:平行线也会相交💪 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 平行线也会相交 原创 收录于专栏【C++之路】💌 本专栏旨在记录C++的学习路线,望对大家有所帮助🙇‍ 希望我们一起努力、成长,共同进步。🍓 我们知道类包含成员变量和成员函数,当一个类中

    2024年02月05日
    浏览(104)
  • C++类开发第二篇(浅析构造函数和析构函数)

    构造函数是一种特殊的成员函数,用于创建和初始化类的对象。它的 名称与类的名称相同 ,没有返回值,也不需要显式调用。在C++中,每个类都必须至少有一个构造函数。 当我们创建一个类的对象时,编译器会自动调用构造函数来初始化该对象的成员变量。构造函数可以执

    2024年02月19日
    浏览(58)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包