说明

分析lua使用的gc算法,如何做到分步gc,以及测试结论

gc算法分析

lua gc采用的是标记-清除算法,即一次gc分两步:

  1. 从根节点开始遍历gc对象,如果可达,则标记
  2. 遍历所有的gc对象,清除没有被标记的对象

二色标记法


lua 5.1之前采用的算法,二色回收法是最简单的标记-清除算法,缺点是gc的时候不能被打断,所以会严重卡住主线程

三色标记法

  1. lua5.1开始采用了一种三色回收的算法

    • 白色:在gc开始阶段,所有对象颜色都为白色,如果遍历了一遍之后,对象还是白色的将被清除
    • 灰色:灰色用在分步遍历阶段,如果一直有对象为灰色,则遍历将不会停止
    • 黑色:确实被引用的对象,将不会被清除,gc完成之后会重置为白色
  2. luajit使用状态机来执行gc算法,共有6中状态:
    • GCSpause:gc开始阶段,初始化一些属性,将一些跟节点(主线程对象,主线程环境对象,全局对象等)push到灰色链表中
    • GCSpropagate:分步进行扫描,每次从灰色链表pop一个对象,遍历该对象的子对象,例如如果该对象为table,并且value没有设置为week,则会遍历table所有table可达的value,如果value为gc对象且为白色,则会被push到灰色链表中,这一步将一直持续到灰色链表为空的时候。
    • GCSatomic:原子操作,因为GCSpropagate是分步的,所以分步过程中可能会有新的对象创建,这时候将再进行一次补充遍历,这遍历是不能被打断的,但因为绝大部分工作被GCSpropagate做了,所以过程会很快。新创建的没有被引用的userdata,如果该userdata自定义了gc元方法,则会加入到全局的userdata链表中,该链表会在最后一步GCSfinalize处理。
    • GCSsweepstring:遍历全局字符串hash表,每次遍历一个hash节点,如果hash冲突严重,会在这里影响gc。如果字符串为白色并且没有被设置为固定不释放,则进行释放
    • GCSsweep:遍历所有全局gc对象,每次遍历40个,如果gc对象为白色,将被释放
    • GCSfinalize:遍历GCSatomic生成的userdata链表,如果该userdata还存在gc元方法,调用该元方法,每次处理一个

什么时候会导致gc?

  1. luajit中有两个判断是否需要gc的宏,如果需要gc,则会直接进行一次gc的step操作

    1
    2
    3
    4
    5
    6
    7
    /* GC check: drive collector forward if the GC threshold has been reached. */
    #define lj_gc_check(L) \
    { if (LJ_UNLIKELY(G(L)->gc.total >= G(L)->gc.threshold)) \
    lj_gc_step(L); }
    #define lj_gc_check_fixtop(L) \
    { if (LJ_UNLIKELY(G(L)->gc.total >= G(L)->gc.threshold)) \
    lj_gc_step_fixtop(L); }
    • gc.total: 代表当前已经申请的内存
    • gc.threshold:代表当前设置gc的阈值
  2. 这两个宏会在各个申请内存的地方进行调用,所以当前申请的内存如果已经达到设置的阈值,则会申请的所有对象都会有gc消耗。

lua gc api

lua可以通过

1
collectgarbage([opt [, arg]])

来进行一些gc操作,其中opt参数可以为:

  • “collect”:执行一个完整的垃圾回收周期,这是一个默认的选项
  • “stop”:停止垃圾收集器(如果它在运行),实现方式其实就是将gc.threshold设置为一个巨大的值,不再触发gc step操作
  • “restart”:将重新启动垃圾收集器(如果它已经停止)。
  • “count”:返回当前使用的的程序内存量(单位是Kbytes),返回gc->total/1024
  • “step”:执行垃圾回收的步骤,这个步骤的大小由参数arg(较大的数值意味着较多的步骤),如果这一步完成了一个回收周期则函数返回true。
  • “setpause”:设置回收器的暂停参数,并返回原来的暂停数值。该值是一个百分比,影响gc.threshold的大小,即影响触发下一次gc的时间,设置代码如下:

    1
    g->gc.threshold = (g->gc.estimate/100) * g->gc.pause;

    g->gc.estimate为当前实际使用的内存的大小,如果gc.pause为200,则该段代码表示,设置gc的阈值为当前实际使用内存的2倍

  • “setstepmul”:设置回收器的步进乘数,并返回原值。该值代表每次自动step的步长倍率,影响每次gc step的速率,具体这么影响可以查看后面小节

luajit gc速率控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int LJ_FASTCALL lj_gc_step(lua_State *L)
{
global_State *g = G(L);
GCSize lim;
int32_t ostate = g->vmstate;
setvmstate(g, GC);
// 设置此次遍历的限制值,每次调用gc_onestep都会返回此次step的消耗,限制值消耗完毕之后此次step结束;
lim = (GCSTEPSIZE/100) * g->gc.stepmul;
if (lim == 0)
lim = LJ_MAX_MEM;
if (g->gc.total > g->gc.threshold)
g->gc.debt += g->gc.total - g->gc.threshold;
do {
lim -= (GCSize)gc_onestep(L);
if (g->gc.state == GCSpause) {
g->gc.threshold = (g->gc.estimate/100) * g->gc.pause;
g->vmstate = ostate;
return 1; /* Finished a GC cycle. */
}
} while (sizeof(lim) == 8 ? ((int64_t)lim > 0) : ((int32_t)lim > 0));
if (g->gc.debt < GCSTEPSIZE) {
g->gc.threshold = g->gc.total + GCSTEPSIZE;
g->vmstate = ostate;
return -1;
} else {
// 加快内存上涨速度;
g->gc.debt -= GCSTEPSIZE;
g->gc.threshold = g->gc.total;
g->vmstate = ostate;
return 0;
}
}
  • 可以看到最重要的变量为lim,该变量控制着一个lj_gc_step里的循环次数。每次调用gc_onestep都会返回此次的step消耗,例如如果处于GCSpropagate阶段,则返回值为该step遍历的内存大小,所以如果遍历了一个较大的table就会消耗更多的lim值
  • lim大小主要由gc.stepmul控制,所以设置该值的大小会影响每次step的调用时间

测试大table对gc的影响

从luajit gc原理上看,以为每次gc的遍历都会遍历所有的gc对象,所以大的table是会影响gc性能

测试环境

操作系统:Debian GNU/Linux 8
CPU:Intel(R) Xeon(R) CPU E5-2640 v2 @ 2.00GHz
内存:64G
lua环境:LuaJIT-2.1.0-beta3 (测试的时候关闭jit)

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
-- 关闭jit
if jit then
jit.off()
end local data = {} -- 一个大的table,用来模拟常驻内存的table,测试的时候使用的是drop_data.lua里面的数据,该data有8655个table元素(在gc的时候产生消耗),60810个元素(包括table元素,会在遍历的时候产生消耗) function deepCopyTable(t)
local ret = {}
for k, v in pairs(t) do
if type(v) == "table" then
ret[k] = deepCopyTable(v)
else
ret[k] = v
end
end
return ret
end datas = {} -- 循环产生更多的常驻内存的table,可以看到总共会有865W+的table元素和总共6000W+的元素
for i = 1, 1000 do
datas[#datas+1] = deepCopyTable(data)
end print("begin")
local time = os.clock()
for i = 1, 2000000 do
-- 模拟产生临时变量
local temp = deepCopyTable(data) -- 每10次计算一次时间和内存
if i % 10 == 0 then
local time_temp = os.clock()
print(collectgarbage("count"), time_temp-time)
time = time_temp
end
end

测试结果(第一个列为当前内存,第二列为当前内存阈值,第三列为当前gc状态,第四列为循环10次的时间)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- gc没有介入阶段,平均时间大概在0.059s,这时候代表着内存的分配速度
3345733.2617188 4136590.0390625 0 0.058304
3366347.6367188 4136590.0390625 0 0.058013000000003
3386962.0117188 4136590.0390625 0 0.058147999999996
3407576.3867188 4136590.0390625 0 0.059978000000001
3428190.7617188 4136590.0390625 0 0.059843999999998
3448805.1367188 4136590.0390625 0 0.058331000000003
3469419.5117188 4136590.0390625 0 0.058205000000001
3490033.8867188 4136590.0390625 0 0.058352999999997
3510648.2617188 4136590.0390625 0 0.058503000000002
3531262.6367188 4136590.0390625 0 0.058151000000002
3551877.0117188 4136590.0390625 0 0.058059999999998 -- gc进入sweep阶段,删除内存,峰值时间在0.78s左右,后面时间变少应该是因为那一块都是常驻内存的gc对象,很少会去调用free函数
5056726.3867188 5056726.3242188 1 0.076171000000002
5077340.7617188 5077340.9492188 1 0.076453999999998
4955367.8554688 4955368.0429688 4 0.140509
3994134.0820313 3994134.0195313 4 0.679567
3032849.7617188 3032850.1992188 4 0.786561
2133608.0117188 2133608.7617188 4 0.788004
2154222.3867188 2154222.3242188 4 0.255904
2174836.7617188 2174837.1992188 4 0.254212
full sweep time: 2.850359
2195451.1367188 4137406.4453125 0 0.066203999999999
  • 火焰图分析(gc处于sweep状态):

    主要时间消耗在gc_sweep(51.34%):该步骤会遍历所有的gc对象,如果可回收,就进行free操作,所以gc_sweep里面最耗时的就是free函数(34%左右)

gc优化

从火焰图上看到,gc_sweep函数耗时严重,其主要工作是遍历所有gc对象,如果为白色,则free它,所以优化方案有两点:

  1. 内存分配算法优化
  2. 减少gc遍历的对象,即减少那些明确常驻内存的gc对象遍历

    内存分配算法优化

    luajit默认使用的是自己的内存分配算法,现在尝试分别使用glibc自带的内存分配和第三方高性能jemalloc(选择的版本是jemalloc-stable-4),tcmalloc(选择的是gperftools-2.7)的分配算法进行分析

    测试结果



结果分析

  • 申请内存的速率跟常驻内存的table大小关系不大,luajit自带的分配算法最快,但是总体相差不大
  • 随着常驻内存的table大小变大,会影响gc释放速度,这将会卡主主线程
  • 释放内存速率jemalloc最好,并且随着常驻内存的table大小变大,效率体现的越明显

table缓存优化

思路

自己写一个table缓冲池,缓冲一定数量、一定大小的table在c++内存,避免每次反复申请内存及rehash,reszie table操作
TODO: 需要具体修改luajit源码进行测试

减少gc遍历的对象

思路

对于那些常驻内存的table,可以主动加一个标记,在gc时候遍历到这个table,将对其以及所有子gc对象从全局gc链表删除,并加入到一个全局const gc对象链表中。
源代码可以查看github

测试结果

对比结果

火焰图(jemalloc-4G内存)

    • gc_sweep在总的采样占比上已经变得很少,这点从打log上面就能看出
    • free占比gc_sweep的时间比重增加,说明减少了遍历的时间消耗

      注意点

    • 从给table设置constant之后完整的一次gc之前,不能主动调用full gc否则会导致table子元素没有被标记,这样就会被误删除,导致访问的时候出现内存问题
    • table不能设置weak
    • table元素只能是table、string、number,不能有function,线程

      结论

      可以看到优化在常驻内存table大的时候很明显,主要提升了两个方面的速度:

    • 在GCSpropagate阶段减少不必要的遍历,加快遍历速度,同时减少了新临时变量的生成
    • 在GCSweep阶段,减少不必要的遍历,同时因为加快遍历速度,需要free的临时变量变少,所以减少了GCSweep的时间

Lua GC机制的更多相关文章

  1. 引用计数gc机制使用不当导致内存泄漏

    上一篇文章找同事review了一下,收到的反馈是铺垫太长了,我尽量直入正题,哈哈 最近dbd压测时发现内存泄漏,其实这个问题去年已经暴露了,参见这篇博客[压测周].当时排查不够仔细,在此检讨下.关于d ...

  2. 开源基于lua gc管理c++对象的cocos2dx lua绑定方案

    cocos2dx目前lua对应的c++对象的生命周期管理,是基于c++析构函数的,也就是生命周期可能存在不一致,比如c++对象已经释放,而lua对象还存在,如果这时候再使用,会有宕机的风险,为此我开发 ...

  3. Java 内存区域和GC机制分析

    目录 Java垃圾回收概况 Java内存区域 Java对象的访问方式 Java内存分配机制 Java GC机制 垃圾收集器 Java垃圾回收概况 Java GC(Garbage Collection, ...

  4. 从C#垃圾回收(GC)机制中挖掘性能优化方案

    GC,Garbage Collect,中文意思就是垃圾回收,指的是系统中的内存的分配和回收管理.其对系统性能的影响是不可小觑的.今天就来说一下关于GC优化的东西,这里并不着重说概念和理论,主要说一些实 ...

  5. 浅谈你感兴趣的 C# GC 机制底层

    本文内容是学习CLR.via C#的21章后个人整理,有不足之处欢迎指导. 昨天是1024,coder的节日,我为自己coder之路定下一句准则--保持学习,保持自信,保持谦逊,保持分享,越走越远. ...

  6. GC基本算法及C++GC机制

    前言 垃圾收集器是一种动态存储分配器,它自动释放程序不再需要的已分配的块,这些块也称为垃圾.在程序员看来,垃圾就是不再被引用的对象.自动回收垃圾的过程则称为垃圾收集(garbage collectio ...

  7. Java 内存区域和GC机制

    目录 Java垃圾回收概况 Java内存区域 Java对象的访问方式 Java内存分配机制 Java GC机制 垃圾收集器 Java垃圾回收概况 Java GC(Garbage Collection, ...

  8. Java系列笔记(3) - Java 内存区域和GC机制

    目录 Java垃圾回收概况 Java内存区域 Java对象的访问方式 Java内存分配机制 Java GC机制 垃圾收集器 Java垃圾回收概况 Java GC(Garbage Collection, ...

  9. .NET GC机制学习笔记

    学习笔记内容来自网络资料摘录http://www.cnblogs.com/springyangwc/archive/2011/06/13/2080149.html 1.GC介绍 Garbage Col ...

随机推荐

  1. 编译原理 First集和Follow集的求法

    转载地址 https://blog.csdn.net/Alexander_Frank/article/details/51280798 自上而下分析: FIRST集求法 First集合最终是对产生式右 ...

  2. Linux学习笔记 一 第三章 Linux常用命令

    第三章Linux常用命令 一.文件处理命令 1.命令格式 2.目录处理命令:ls 3.目录处理命令:mkdir 4.文件处理命令: touch

  3. .netcore tif文件转jpg,并通过webapi返回

    网上能搜到很多关于c# tif转jpg的代码.最简单的就是下面这段获得转换后的图片图片对象.在netcore 中使用这段代码时由于netcore框架已经不自带System.Drawing命名空间了所有 ...

  4. 汇编in和out实例解析

    直接看例子: IN AL,21H 从21H端口读取一字节数据到AL IN AX,21H 从端口地址21H读取1字节数据到AL,从端口地址22H读取1字节到AH MOV DX,379HIN AL,DX ...

  5. 《Python 测试开发技术栈—巴哥职场进化记》—每日站会的意义

    上文<Python测试开发技术栈-巴哥职场进化记>-一道作业题我们讲到华哥给巴哥出了一道作业题,让巴哥用Python实现记录日志的功能,巴哥历经"千辛万苦",终于做出了 ...

  6. SpringBoot--- Shiro(拦截,认证)、Thymeleaf(模板引擎)

    SpringBoot--- Shiro(拦截,认证).Thymeleaf(模板引擎) 环境 IDEA :2020.1 SpringBoot: 2.3.3 Java : 8 版本依赖: ​ shiro- ...

  7. 曹工改bug:cpu狂飙,old gc频繁,线程神秘死亡连环案件调查报告

    曹工改bug:cpu狂飙,old gc频繁,线程神秘死亡连环案件调查报告 前言 前两天,访问开发环境上一个java服务,发现一直转圈圈,因为我开着fiddler,可以看到的现象是,接口一直没返回:本来 ...

  8. 快醒醒,C# 9 中又来了一堆关键词 init,record,with

    一:背景 1. 讲故事 .NET5 终于在 2020-08-25 也就是前天发布了第八个预览版,这么多的预览版搞得我都麻木了,接踵而来的就是更多的新特性加入到了 C# 9 中,既然还想呆在这条船上,得 ...

  9. Labview学习之路(五)按钮的机械动作

    布尔类型中有一个按钮是非常重要的控件,他不是只是表示一个确定,输出0或1,下边我们共同探讨一下他的机械动作 单击时转换 释放时转换 保持转换直到释放 单击时触发 释放时触发 保持触发直到释放 单击时转 ...

  10. eclipse android程序运行报错:Conversion to Dalvik format failed: Unable to execute dex:

    [2013-06-19 16:59:01 - Dex Loader] Unable to execute dex: Multiple dex files define Landroid/support ...