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。让我们看看答案是什么,如下:
看运行结果,答案不是选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,最好手动设置,这样能更好的避免内存泄漏。
2023-05-26:golang关于垃圾回收和析构函数的选择题,多数人会选错。的更多相关文章
- golang的垃圾回收(GC)机制
golang的垃圾回收采用的是 标记-清理(Mark-and-Sweep) 算法 就是先标记出需要回收的内存对象快,然后在清理掉: 在这里不介绍标记和清理的具体策略(可以参考https://lengz ...
- 从golang的垃圾回收说起(上篇)
本文来自网易云社区 1 垃圾回收中的重要概念 1.1 定义 In computer science, garbage collection (GC) is a form of automatic me ...
- Golang GC 垃圾回收机制详解
摘要 在实际使用 go 语言的过程中,碰到了一些看似奇怪的内存占用现象,于是决定对go语言的垃圾回收模型进行一些研究.本文对研究的结果进行一下总结. 什么是垃圾回收? 曾几何时,内存管理是程序员开发应 ...
- Golang之垃圾回收
本篇主要是参考了: http://legendtkl.com/2017/04/28/golang-gc/ 说是参考,但其实基本上是原封不动. GC算法简介: 1. 引用计数 引用计数的思想非常简单:每 ...
- 从golang的垃圾回收说起(下篇)
文章来自网易云社区 4 Golang垃圾回收的相关参数 4.1 触发GC gc触发的时机:2分钟或者内存占用达到一个阈值(当前堆内存占用是上次gc后对内存占用的两倍,当GOGC=100时) # 表示 ...
- golang GC 垃圾回收机制
垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的对象,让出存储器资源,无需程序员手动执行. Golang中的垃圾回收主要应用三色标记法, ...
- 05.python语法入门--垃圾回收机制
# (1)垃圾回收机制GC# 引用计数# x = 10 # 值10引用计数为1# y = x # 值10引用计数为2## y = 1000 # 值10引用计数减少为1# del x # 值 ...
- C#GC垃圾回收和析构函数和IDisposable的使用
一,什么是GC 1,GC是垃圾回收器,一般来说系统会自动检测不会使用的对象或变量进行内存的释放,不需要手动调用,用Collect()就是强制进行垃圾回收,使内存得到及时的释放,让程序效率更高. 2,G ...
- Golang——垃圾回收GC(2)
1 垃圾回收中的重要概念 1.1 定义 In computer science, garbage collection (GC) is a form of automatic memory manag ...
- 【GoLang】golang垃圾回收 & 性能调优
golang垃圾回收 & 性能调优 参考资料: 如何监控 golang 程序的垃圾回收_Go语言_第七城市 golang的垃圾回收(GC)机制 - 两只羊的博客 - 博客频道 - CSDN.N ...
随机推荐
- 如何通过Java更改Word中的页面大小和页面方向
新建的 Word 文档,默认纸张为 A4 纸,大小为 21 厘米 × 29.7 厘米,没特殊要求的文档用 A4 纸即可,但有时文档中的内容比较宽,需要用比 A4 纸更宽的纸张,例如制作一些宽的表格,就 ...
- 学习笔记if
- 发布新版博客备份功能:生成 sqlite 数据库文件,vscode 插件可查看
大家好,最近我们重新开发了园子的博客备份功能,今天发布第一个预览版,欢迎大家试用. 点击博客后台侧边栏的博客备份进入新版博客备份: 点击创建备份按钮创建博客备份任务(目前每天只能创建一次备份),待备份 ...
- IDEA配置JDK版本的地方, 适用于Compilation failed: internal java compiler error
错误原因: 1. 编译版本不匹配 2.当前项目jdk版本不支持 解决方法 查看项目的jdk 查看工程的jdk 查看java编译器版本 讲这些改成自己需要的版本, 一般就可以解决编译版本出现的错误
- TS 基础及在 Vue 中的实践:TypeScript 都发布 5.0 版本啦,现在不学更待何时!
大家好,我是 Kagol,OpenTiny 开源社区运营,TinyVue 跨端.跨框架组件库核心贡献者,专注于前端组件库建设和开源社区运营. 微软于3月16日发布了 TypeScript 5.0 版本 ...
- JVM常用参数说明
内存相关 通过这些参数可以对JVM的内存分配做调整 Xms 英文解释:Initial heap size(in bytes) 中文释义:堆区初始值 使用方法:-Xms2g 或 -XX:InitialH ...
- 使用二进制方式安装Docker
长期使用安装工具进行安装docker,今天用二进制方式手动安装一下docker环境. 二进制包下载地址:https://download.docker.com/linux/static/stable/ ...
- [Linux/CentOS]通过yum获取rpm安装包
1 yum获取rpm安装包 有时候你需要一个软件包在离线linux系统上安装,如果自己找软件包麻烦,可以linux yum下载需要的软件包. 准备工作是找一台能够联网的linux,并准备好了yum及y ...
- Java设计模式 —— 观察者模式
16 观察者模式 16.1 观察者模式概述 Observer Pattern: 定义对象之间的依赖关系(一对多),当一个对象的状态发生改变时,其关联的依赖对象均收到通知并自动更新. 观察者模式又称:发 ...
- kettle从入门到精通 第十一课 kettle javascript 解析json数组
1.json步骤虽然可以解析json数组,但是不够灵活.通过javascript步骤来解析json数组比较灵活,且可以按照需要组装数据流转到下个步骤. 1)步骤名称:可以自定义 2)Transform ...