【Lua学习笔记】Lua进阶——垃圾回收

在这里插入图片描述
按照唐老师的课程本来要讲自带库的,但是想想这东西能看文档,ctrl+左键还能看注解,并且最重要的许多自带库的方法基本大部分语言都有,其实看看就能懂了。所以还是重点讲讲垃圾回收

以下大部分内容摘抄自[Lua]垃圾回收详解lua源码解析——gc实现机制[详细版](一),请各位阅读链接中文章


GC

如果你是Unity接触Lua的话,应当知道C#中也存在GC机制,在面向对象的大部分语言中都使用到了虚拟机,而Unity中也使用了Mono 虚拟机。而我们的代码则是在虚拟机的虚拟内存中,在虚拟机中代码要转换成指令集,编译成字节码,最后在各个平台上使用字节码转译的指令集来实现跨平台。

由于对象所占用的内存空间都是存放在虚拟内存中的,因此就不会影响到物理内存。当物理内存需要调用对象的时候,虚拟内存块就会被指派给物理内存。如果一个虚拟内存块长时间不被物理内存所引用,那么它所占的内存应当被释放,这就是GC(garage collect)

Luc官方doc
根据官方的描述,通过GC机制Lua可以自动的管理内存,剔除出那些dead objects,这些object的类型包括:string,tables,userdata,function,threads,internal structures等等

而那些被认为是dead的对象将不会再程序中被访问(但finalizers可以复活这些死亡的物体)。GC认为的已死和程序员认为的有所不同,GC认为长时间不活动就是死了,而如果一个对象被认为死了,那么也就无法再正常访问。

通过使用函数collectgarbage或者定义元方法__gc,我们可以直接使用或者重写gc机制。


辅助垃圾回收

虽然自动垃圾回收在大多数时候都适用,但在一些特殊的情况下还是要我们自己确定垃圾回收的对象和时机。为此,Lua语言提供了一下方式来辅助进行垃圾回收

  • collectgarbage函数:允许控制垃圾回收器的步长
  • 析构器 (最近查询C#的GC条目,微软官方取的中文名叫做终结器)(finalizer):允许收集不在垃圾回收器直接控制下的外部对象
  • 弱引用表(weak table):允许收集Lua中还可以程序访问的对象(尤其是table中的空键值)

collectgarbage

---@alias gcoptions
---|>"collect"      # 做一次完整的垃圾收集循环。
---| "stop"         # 停止垃圾收集器的运行。
---| "restart"      # 重启垃圾收集器的自动运行。
---| "count"        # 以 K 字节数为单位返回 Lua 使用的总内存数。
---| "step"         # 单步运行垃圾收集器。 步长“大小”由 `arg` 控制。
---| "isrunning"    # 返回表示收集器是否在工作的布尔值。
---| "incremental"  # 改变收集器模式为增量模式。
---| "generational" # 改变收集器模式为分代模式。

---
---这个函数是垃圾收集器的通用接口。 通过参数 opt 它提供了一组不同的功能。
---
function collectgarbage(opt, ...) end

// 使用方法collectgarbage("加内关键字")
collectgarbage("collect")  --主动进行一次垃圾回收(垃圾回收会回收nil的垃圾)

每次垃圾回收占用内存还是挺多的,所以能不用尽量少用,能手动就别自动。

collectgarbage提供了两种回收模式,分别是增量模式和分代模式

增量模式

在增量模式下,每次gc循环使用mark-and-sweep方法来逐步进行垃圾标记和收集,GC与解释器一起交替运行(Lua5.1及之后,不需要停止主程序的运行),每当解释器分配了一定数量的内存时,垃圾回收器也执行一步。每个GC周期由四个阶段组成,分别是标记(mark)、清理(cleaning)、清除(sweep)和析构(finalization):

collectgarbage("incremental",200,200,13)
1)、garbage-collector pause, 
    什么时间执行,比上次回收后内增加的比例 默认200% 最大1000%
2)、garbage-collector step multiplier, 
相对说内存分配而言的一个比例,也就是是以什么速度收集 默认 200% 最大 1000%
3)、 the garbage-collector step size
控制每次回收的步幅?解释器分配的字节数 默认是 213次 约 8K

以下大部分内容摘自lua源码解析——gc实现机制[详细版](一)

上述参数将控制collectgarbage在增量模式下的一些参数,增量模式使用mark-and-sweep方法,也就是先标记后处理,简单地来说使用的是三色标记法,把所有的对象都放在树形结构,而父子关系可以用一个链表,用头插法进行元素增加。
在这里插入图片描述
在这里插入图片描述

在起始状态,所有的节点颜色都是白色。白色代表了未引用
在这里插入图片描述
现在引用关系如上图所示,那么从根节点开始遍历引用关系,并给链表中的节点上色在这里插入图片描述
1号节点有引用,所以上色灰色,将其头插到链表gray中
在这里插入图片描述
接着1号的孩子4号有引用,将1号上色黑色并出链表,再把四号上色灰色并进链表

接着就重复上述过程,4号上色黑色,出链表,789上色灰色入链表。依次执行知道gray链表中再无节点为止,那么所有引用的节点都被上了黑色。

最后就是清理环节,sweep节点会顺序遍历rootgc链表,所有的白色节点都会被提出,如果在清除前有白色节点突然被引用了,那么该节点会被上色保护色白色,不删除它。整理完毕后再把所有黑色节点置为白色,方便下次清理。

分代模式

collectgarbage("generational",20,200)
1,minor
    比例数,相对于上次major回收增长的比例,达到即执行minor , 默认20% 最大200%
2), major 
    比例数,内存使用增长比例达到就执行回收,默认100,最大1000

上述参数将控制collectgarbage在迭代模式下的一些参数,在分代GC模式中,垃圾收集器频繁的执行小型垃圾回收,每次都从最近创建的对象中进行扫描,清理其中的垃圾,而不是扫描所有对象。如果这种小型GC后内存仍然超出限制,它将暂停程序的运行,遍历所有对象进行GC。


__gc

在元表中同样为gc提供了一种元方法 __gc,元方法所定义的函数被官方称为finalizer,暂且用知乎上的称呼“析构器” (最近查询C#的GC条目,微软官方取的中文名叫做终结器)。当这个定义了元方法的对象被gc回收的时候,它会执行终结器中的函数。使用这个元方法,我们可以在某些对象被清理的时候调用终结器的函数,或者为了避免某些对象被清理而将它复活。

例子1:

t = {
    
    name = "zhangsan"}
setmetatable(t,{
    
    __gc = function (t)
    print(t.name)
end})
t = nil
--调用t的析构函数,打印zhangsan

在例子1中,打印了zhangsan,但是t被gc清理了,实际过程是:gc开始清理对象->使用终结器,打印t.name(尽管t=nil,但是被终结器短暂的复活了,而执行完终结器之后又会死去)->gc清理

如果一个对象在设置元表时,没有为它添加 __gc 元方法,而是在元表创建完成后添加,那么这个对象在回收时将无法触发 __gc元方法。

t = {
    
    name = "zhangsan"}
mt = {
    
    }
setmetatable(t,mt)
--先设置元表,再为元表添加__gc元方法
mt.__gc = function (t)
    print(t.name)
end
t = nil
--不会输出任何值(未执行终结器)

因为终结器要访问被回收的对象,因此Lua需要将这个对象复活(resurrection)。通常这种复活是短暂的,这个对象占用的内存将在下次GC时被释放。但是,如果终结器将这个对象存储在某个全局位置(比如全局变量),那么这种复活就会变成永久的。

t = {
    
    name = "zhangsan"}
setmetatable(t,{
    
    __gc = function (t)
    print(t.name)
    a = t   --在析构函数中将它赋值给全局变量
end})
t = nil
collectgarbage()    --在此处要手动垃圾回收,否则由于下方还有语句不会执行gc,而a也就不会被赋值了,打印zhangsan
print(a.name)       --t引用的对象并未被回收(永久复活),打印zhangsan

weak table弱引用表

如果要保存一些活跃对象,该怎么做呢?我们只需要将它放入数组,但一个对象一旦加入数组,将再也不能被回收,因为就算没有其他地方引用它,但依然包含在数组中!不过我们可以通过**弱引用表(weak table)**显式告诉Lua,这个数组中的引用不应该影响此对象的回收。

弱引用(weak reference)指的是不在垃圾回收器考虑范围内的引用,如果一个对象的引用全是弱引用,那么垃圾回收器会回收这个对象,并删除这些弱引用,弱引用表就是Lua实现弱引用的方式。

一个表是否为弱引用表是由其元表的__mode字段决定的,它有三种情况,分别是:

  • 键弱引用:通过设置 __mode = "k",允许垃圾回收器回收它的键,但不允许回收值
  • 值弱引用:通过设置 __mode = "v",允许垃圾回收器回收它的值,但不允许回收键。也称为瞬表(ephemeron table),只有它的键可访问,值才能被访问。因为当它的键无法访问时,值也会被垃圾回收器从table中移除。
  • 键值都为弱引用:通过设置 __mode = "kv",键值都允许回收

需要强调的时,在任何情况下,只要table的键或值被回收,整个键值对都会从table中移除。

以下内容摘自Lua基础之弱引用

Lua采用垃圾自动回收的内存管理机制,但有时候Lua并不能正确判断对象是否需要被销毁,导致某些需要被销毁的对象一直存在,造成内存泄漏。

a = {
    
    }
key = {
    
    }
print(a[key])
a[key] = 1
print(a[key])
key = {
    
    }
print(a[key])
a[key] = 2
collectgarbage()
for k,v in pairs(a) do
    print(k, v)
end
输出:
nil
1
nil
table: 00000000006da000	1
table: 00000000006da380	2

本该销毁的1却没有被销毁,尽管key={}之后a[key]是nil,但是当我们遍历的时候还是得到了1,说明它并没有被销毁。这就是因为这个a[key]是存于一个数组当中的,而数组中的键值对即使有空也不允许GC删除,而这样的情况就会导致内存泄漏。因此为了避免这种情况,我们可以使用弱引用来告诉GC机制:虽然它是数组,但里面的空键值都是可以删除的!

a = {
    
    }
b = {
    
    __mode = "k"}
setmetatable(a,b)
key = {
    
    }
a[key] = 1
key = {
    
    }
a[key] = 2
collectgarbage()
for k,v in pairs(a) do
    print(v)
end
输出:
2

猜你喜欢

转载自blog.csdn.net/milu_ELK/article/details/131984125