Garbage Collection
LuaLuaA lightweight, embeddable scripting language used as the primary scripting language in FiveM server development. uses automatic memory management. Two GC modes: incremental (default) and generational (new in 5.4).
GC modes
Incremental (default)
Marks reachable objects, sweeps unreachable ones. Runs in steps interleaved with program execution.
Tuning parameters:
| Parameter | Default | Meaning |
|---|---|---|
pause | 200 | How much memory can grow before new cycle (%) |
stepmul | 100 | Speed of GC relative to allocations (%) |
stepsize | 13 | Bytes between GC steps (~8KB per step) |
collectgarbage("setpause", 100) -- collect more aggressively
collectgarbage("setstepmul", 200) -- faster steps
Generational (new in 5.4)
Assumes most objects die young. Collects young objects frequently, old objects rarely.
collectgarbage("generational") -- switch to generational mode
collectgarbage("setminor", 20) -- minor collection threshold (%)
collectgarbage("setmajor", 100) -- major collection threshold (%)
Switch back:
collectgarbage("incremental")
collectgarbage options
| Command | Description |
|---|---|
"collect" | Full GC cycle |
"stop" | Stop automatic collection |
"restart" | Restart automatic collection |
"count" | Returns memory used (KB), total bytes |
"step" | Run one GC step. Returns true if cycle finished |
"setpause" | Set pause parameter |
"setstepmul" | Set step multiplier |
"incremental" | Switch to incremental mode |
"generational" | Switch to generational mode |
"isrunning" | Returns whether GC is running |
-- Check memory usage
local kb, bytes = collectgarbage("count")
print(string.format("%.1f KB", kb))
-- Force full collection
collectgarbage("collect")
-- Measure before/after
collectgarbage("collect")
local before = collectgarbage("count")
-- ... allocate stuff ...
collectgarbage("collect")
local after = collectgarbage("count")
print(string.format("Used: %.1f KB", after - before))
Finalizers (__gc)
Objects with a __gc metamethod get their finalizer called when collected.
local mt = {
__gc = function(self)
print("Finalizing:", self.name)
end
}
do
local obj = setmetatable({name = "resource"}, mt)
-- use obj
end
obj = nil
collectgarbage() -- prints: Finalizing: resource
Rules
- Finalizer runs once per object
- Order of finalization is undefined
- Finalizers can resurrect objects (by storing reference)
- Set
__gcbefore the object is marked for finalization
-- WRONG: setting __gc after creation
local t = {}
setmetatable(t, {__gc = function() print("done") end})
-- May not work! Set metatable at creation time.
-- CORRECT:
local t = setmetatable({}, {__gc = function() print("done") end})
To-be-closed variables (5.4)
More predictable than __gc:
local function open_file(path)
local f = assert(io.open(path, "r"))
local sentinel = setmetatable({}, {
__close = function()
f:close()
print("File closed: " .. path)
end
})
return f, sentinel
end
do
local f, _ <close> = open_file("test.txt")
-- use f
end -- file closed here, guaranteed
Weak references
Controlled by __mode in the metatable:
| Mode | Meaning |
|---|---|
"k" | Weak keys — entry removed when key is collected |
"v" | Weak values — entry removed when value is collected |
"kv" | Both weak |
-- Weak values (cache pattern)
local cache = setmetatable({}, {__mode = "v"})
function get_data(key)
if not cache[key] then
cache[key] = expensive_compute(key)
end
return cache[key]
end
collectgarbage()
-- Entries with collected values are now gone
-- Weak keys (metadata without preventing GC)
local metadata = setmetatable({}, {__mode = "k"})
function set_meta(obj, data)
metadata[obj] = data
end
-- When obj is collected, its metadata entry is removed
Important notes
- Only tables can have weak entries
- Strings, numbers, booleans, nil are never collected
- Weak tables may have entries disappear at any time after collection
GC phases
1. Marking — mark all reachable objects (from roots)
2. Atomic — handle special cases (weak tables, finalizers)
3. Sweeping — reclaim unreachable objects
4. Finalizing — run __gc on dead objects with finalizers
Performance tips
-
Don't fight the GC — let it run. Calling
collectgarbage("collect")too often wastes CPU. -
Avoid creating garbage in hot loops — reuse tables and strings where possible:
-- BAD: creates new table every framefor i = 1, 1000 dolocal t = {x = i, y = i * 2}process(t)end-- BETTER: reuselocal t = {}for i = 1, 1000 dot.x = it.y = i * 2process(t)end -
Use weak tables for caches — prevents unbounded memory growth.
-
Close resources explicitly — don't rely on
__gcfor deterministic cleanup. Use<close>or explicitclose(). -
Monitor memory —
collectgarbage("count")to detect leaks.
Common pitfalls
| Pitfall | Solution |
|---|---|
| Finalizer runs in arbitrary order | Don't depend on finalization order |
| Finalizer can resurrect object | Set resurrected reference to nil to allow re-collection |
__gc not called on collectcycle you expect | May take 2 cycles; use <close> for deterministic cleanup |
| Weak table entries disappear unexpectedly | That's the point — don't assume weak entries persist |
| Generational mode accumulates old objects | Tune major threshold; switch to incremental for full sweep |