lua实现了简单的incremental mark-and-sweep collector,有两个参数会影响GC的工作方式:pausestep multiplier

概念

GC完整地清理一次内存的过程(“full gc”或”cycle”)由”标记内存是否垃圾”(mark)和”释放垃圾内存”(sweep“)两个阶段构成。一个cycle的开销通常比较大,理想情况下应该分割成多个开销较小的step。lua的GC引入一个变量debt,用以分割step,单位和内存是等比例的。如果debt是无穷大,一个step就会完成一个cycle的工作。debt<=0时,GC停止工作。分配内存时debt会等比例的增大。在mark过程中,每标记一个正在使用的object,就会根据这个object的大小从debt减去一个等比例的数值。在sweep过程中,每释放一个object,就会根据这个object的大小从debt减去一个等比例的数值。在一个cycle完成后,GC会根据当前使用的内存和pause的值计算一个新的debt。

数量关系

声明以下变量:

  • TotalMemory: Lua占用的内存。
  • TotalMemoryInUse : 正在被使用的对象的内存总和。
  • MemoryAllocatedInMark : Mark过程中分配的内存。
  • MemoryMarked : Mark过程中已标记的内存
  • MemoryAllocatedInSweep: Sweep过程中分配的内存。
  • MemoryFreed: 释放的对象的内存总和。

在Mark过程中,TotalMemory增加MemoryAllocatedInMark,debt的变化量是MemoryAllocatedInMark - MemoryMarked ;在Sweep过程中,TotalMemory的变化量是MemoryAllocatedInSweep - MemoryFreed,debt的变化量是 MemoryAllocatedInSweep - MemoryFreed。 在一个cycle完成之后,GC将debt的值重新设置为 TotalMemory * (1 - pause) * stepmul 内存增加时,debt也会相应的增加,step multiplier是两者的比例。

示例分析:

现象

在青柳谷中什么都不做,TotalMemory会有规律的波动,缓慢地增长到一个最大值(MemoryMax)后,快速地降低到一个最小值(MemoryBase)。设置不同的pause和set multiplier,统计数据如下:

pause(%) stepmul(下面的值/200) MemoryBase(M) MemoryMax(M) 内存增长时间(s) 内存减少时间(s)
10 200 34 37 25.4 4.6
20 200 35 41 51.4 6.5
90 200 38 64 235.8 28.7
10 100 44 70 246 73

原因

在游戏中什么都不做,所以TotalMemoryInUse是基本固定的,测试数据33M。内存缓慢增长是因为每个Tick都会分配少量(大约3~4k)垃圾内存。一个cycle跑完时内存达到最小值 MemoryBase = TotalMemoryInUse + MemoryAllocatedInSweep;Mark过程中内存会持续增长,所以Mark完成时达到最大值 MemoryMax = MemoryBase + MemoryAllocatedInMark。debt累积超过34M才能完成Mark,完成前一个cycle时会设置初始debt = TotalMemory * (1 - pause) * stepmul,如果debt >= TotalMemoryInUse,GC会在下一个step完成Mark,进入Sweep;否则GC会停留在Mark阶段,等待内存分配。 以第二行数据为例,初始debt = 34 * (1 - 0.2) 即27.2M,不足以完成Mark,还缺少6.8M。内存增长到41M后,完成Mark。进入Sweep阶段后,开始释放内存,时长6.5秒,Tick分配的内存大约为 6.5* (41-35)/51.4约等于0.76M。