内存自动化管理的概念
内存自动化管理是指在指定内存不再被需要时可以自动被释放。通常有两种方案来实现内存的自动化管理:
-
引用计数 (reference count)
-
垃圾回收 (garbage collection)
Lua 选择的是垃圾回收的方案。不选择引用计数的方式的主要原因是,在动态类型语言中,使用引用计数来管理内存,即使是在没有申请任何内存的情况下也会有额外的开销。这里的额外开销主要是 cpu 开销,例如你只是操作了已分配内存的数据,没有分配新的内存,你也需要更新引用计数。
Lua 中的 gc 对象
Lua 中的所有对象都接受自动管理:tables, userdata, functions, threads, and strings. 在 Lua 运行期间,所有的 Lua 对象都链接到一个内置链表中,以供垃圾回收器可以找到他们。
何时对象需要被回收:当对象不再能从根集(root set)中访问到。
根集包括 Lua 注册表(registry)和共享元表(share metatable):
-
Lua 注册表:
-
全局表 _G
-
主协程(lua 栈)
-
package.loaded (加载的 modules)
-
-
共享元表:
- 被其他对象设置为元表的 table
Lua 中内存分配和释放的底层接口
Lua 不直接操作内存,例如向系统申请一大块内存来进行更细致的管理,而是直接使用标准 c 库的内存接口,例如 malloc、free 和 realloc。这样在一方面的确是有不足,使得 Lua 的内存管理依赖具体操作系统环境的内存管理机制的性能好坏,但是另一方面,Lua 通常是和 c 或其他语言搭配使用,这样开发人员可以为应用使用一套分配机制,而不会导致在应用中存在两种分配机制,一种服务于 Lua,一种服务于另外的语言。
Lua 5.0 gc 算法
基础的标记 + 清扫:
-
标记:从根集开始,遍历对象关联图,标记存活的对象。
-
清扫:遍历链接所有 Lua 对象的内置链表,删除那些没有被标记的对象。
void luaC_collectgarbage (lua_State *L) {
mark(L);
luaC_sweep(L, 0);
checkSizes(L);
luaC_callGCTM(L);
}
收集器步子大小的影响:
-
极端情况一:从不启动收集,在收集器上没有任何 cpu 开销,但是内存压力很大,只会上涨不会下降。
-
极端情况二:始终在运行收集器,cpu 开销巨大,但是内存情况维持得很好。
-
比较合适的设置:当内存增长到上一次垃圾回收后的内存的两倍大小时开始新的一轮回收。
主要的缺陷:
由于 gc 过程是一个连续的不可中断的过程,一旦开始 gc,应用程序必须等到一次完整 gc 执行完毕才可继续运行,这个等待时间对大多数 cpu 密集型应用是不可接受的,这也是 Lua 5.1 新的 gc 机制出现前 Lua 的应用场景并没有之后那么广泛的重要原因。
Lua 5.1 gc 算法
主要的改进:收集器可以增量式地跟应用程序交替运行。
三色标记回收算法 (Tri-color Collector):
-
状态说明:
-
每一个 Lua 对象都处于白色、灰色和黑色三种状态之一。
-
不能访问到的对象被标记成白色。
-
可以访问到但是没有遍历到的对象被标记成灰色。
-
被遍历到的对象被标记成黑色。
-
-
约束条件:
-
在根集中的对象是黑色或者灰色。
-
一个黑色对象不能指向一个白色对象。
-
灰色对象的存在形成了白色对象和黑色对象之间的隔离线,如下图红线所示。
-
收集器通过遍历灰色对象来推进工作,将他们转换成黑色,在这个过程中可能会创建新的灰色对象。
-
当不再存在灰色对象时,收集器结束工作。
-
修改器(Mutator):从垃圾收集器的角度来看,应用程序就像一个修改器一样在不断地改变那些被收集器试图收集的数据,对收集器的工作造成干扰。
屏障(Barriers):向前它可以将白色对象转化成灰色对象,向后它可以将黑色对象转化成灰色对象。
启发式设计(heuristics):
-
从黑色回退到灰色的对象暂存到一个分离出去的链表中,仅供在 atomic 阶段拿来遍历,这样可以避免出现 ping-pong 现象(反复横跳)。
-
栈中对象保持为灰色,避免当往栈中写入的时候触发写屏障。
- 栈中对象通常来说是比较少的,但是赋值操作又比较频繁,所以这样做来避免写操作触发屏障检查是有收益的,代价是在 atomic 阶段栈需要遍历栈中对象。
-
对 table 进行赋值操作,会将黑色的 table 回退到灰色。
- 试想如果不这么处理,在对黑色 table 进行多次赋值时(a[x] = y),赋值的对象 x, y 会被标记成黑色,即使可能在后续某次赋值时 y 被替换掉(a[x] = z),这样就产生了额外开销的标记工作,如果将黑色 table 回退成灰色就可以避免掉这类额外开销。
-
设置元表的操作,Lua 5.4 中已经修改为当被设置元表的对象是黑色并且元表是白色的情况下,将元表颜色向前转换为灰色,这是因为元表通常会被分配给许多不同的对象,并且元表比其他对象更加稳定。
- Lua 5.1 和 Lua 5.2 中是将黑色的 table 回退成灰色。
原子操作阶段(atomic):
-
这是标记流程的最后一步。
-
在这个阶段会遍历 “gray again” 链表和栈对象,这块对象的量级可以认为很小,所以该阶段在整个回收流程的占比很小,可以认为该阶段相比于整个回收流程来说进行地非常迅速。
-
检查并清理弱表。
-
分离出确定复活的对象,复活他们以及关联对象。
-
再次检查并清理弱表。(有一些弱表复活了)
收集器步子大小控制参数:
-
暂停时间:通过检测距离上次回收结束增长了多少内存来触发下一次回收启动。一般设置为固定值 “2”, 当内存增长到上次的两倍时开始新的一轮回收。调用 collectgarbage ([opt [, arg]]) 接口设置,opt = “setpause”,arg = 200 表示两倍内存阈值。
-
回收倍率:控制以分配速度为基准的回收的相对速度,通过 collectgarbage ([opt [, arg]]) 接口设置,opt = “setstepmul”,arg = 200,最好不要设置为小于 100 的值,它会导致垃圾回收器推进的十分缓慢,甚至都无法完成一次完整的 gc。详情参考 Lua 5.2 manual 2.5 节
注意:
- 增量式回收算法虽然缩短了每次暂停主程序运行的时间,但是它并没有减少收集器进行一次完整 gc 带来的开销,恰恰相反它的总开销是更高的。
Lua 5.2 - Lua 5.4 gc 算法
Lua 5.2 在 Lua 5.1 增量回收的基础上提出了一个实验性的 gc 模式,分代模式:
As an experimental feature in Lua 5.2, you can change the collector’s operation mode from incremental to generational. A generational collector assumes that most objects die young, and therefore it traverses only young (recently created) objects. This behavior can reduce the time used by the collector, but also increases memory usage (as old dead objects may accumulate). To mitigate this second problem, from time to time the generational collector performs a full collection. Remember that this is an experimental feature; you are welcome to try it, but check your gains.
Lua 5.4 对 Lua 5.2 中的分代模式进行了一些优化,并且完善了世代模式成为一种正式的 gc 机制:
The garbage collector (GC) in Lua can work in two modes: incremental and generational.
The default GC mode with the default parameters are adequate for most uses. However, programs that waste a large proportion of their time allocating and freeing memory can benefit from other settings. Keep in mind that the GC behavior is non-portable both across platforms and across different Lua releases; therefore, optimal settings are also non-portable.
You can change the GC mode and parameters by calling lua_gc in C or collectgarbage in Lua. You can also use these functions to control the collector directly (e.g., to stop and restart it).
分代模式的 gc 机制在不少脚本语言的 gc 机制中存在,例如 c#,它基于以下假设:
大多数对象在早期就死亡了,所以,收集器可以专注于回收比较年轻的对象。
当这个假设成立的时候,分代模式的 gc 总开销是小于增量式的。 假设不成立的典型情形是批处理程序。
简单介绍:
-
所有对象被划分为年轻的和年老的。刚创建的对象都是年轻的。在 Lua 5.4 中,需要存活超过两个回收周期,对象才会变成年老的,而在 Lua 5.2 中,只需要存活超过一个回收周期。
-
收集器进行频繁的次级回收(minor collection),只遍历和清理年轻的对象。
-
这里简单对比下存活不同周期后变成年老对象的区别:
-
Lua 5.2 年轻对象存活一个周期后变成年老对象
-
简单许多的代码实现。
-
在一次回收周期结束后,所有存活的对象都变成年老的。
-
接触列表(touched list)中的对象可以移除出来。
-
对象在变老之前可能只存活了无穷小的时间间隔,如图:
-
-
Lua 5.4 年轻对象存活两个周期后变成年老对象
-
在一次回收周期结束后,一些对象变成老的,一些没有。
-
接触列表(touched list)中的对象需要被正确调整以能正常进行下次回收。
-
对象在变老之前至少需要存活一个完整的 gc 周期,如图:
-
-
-
约束条件:
- 一个年老对象不能指向一个年轻对象。
-
接触列表(touched list):
-
如果检测到一个老对象正指向一个年轻的对象,这个老对象会被标记为接触者(touched)并且被放到一个特殊的列表中(touched list)。
-
在两次回收周期之后,一个接触者对象会恢复到正常的年老状态,除非它又再次被接触(指向一个年轻对象)。
-
选择哪个模式:文章来源:https://www.toymoban.com/news/detail-602073.html
正如官方手册所讲,带有默认参数的默认GC模式适用于大多数用途。然而,浪费大量时间分配和释放内存的程序可以从其他设置中受益。请记住,GC行为是不可移植的,无论是跨平台还是跨不同的Lua版本;因此,最佳设置也是不可移植的。需要开发人员根据自己的应用特性和机器特性进行不断的调整和测试来寻求合适的解决方案。文章来源地址https://www.toymoban.com/news/detail-602073.html
参考内容
- Lua 各版本手册
- Roberto Ierusalimschy 演讲 ppt
- Roberto Ierusalimschy 演讲 video
到了这里,关于Lua gc 机制版本迭代过程简述的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!