Category: luaDifficulty: ProPublished:
Memory Management with Tables in Lua
The Problem
Poor table management in Lua can lead to memory leaks, excessive memory usage, and performance degradation, especially in long-running applications.
-- Bug Example 1: Memory leak through circular references local function createNodes() local node1 = {} local node2 = {} node1.next = node2 node2.prev = node1 -- Circular reference return node1 end -- Bug Example 2: Growing tables without cleanup local cache = {} local function processData(data) cache[#cache + 1] = data -- Growing indefinitely end
Why It Happens
Lua's garbage collector can't free memory when:
- Tables contain circular references
- Tables grow indefinitely without cleanup
- Tables hold references to unused large objects
- Weak references are not used when appropriate
How to Fix It
- Using weak tables to break reference cycles:
-- Solution 1: Weak references local function createNodes() local node1 = {} local node2 = {} -- Create weak reference table local refs = setmetatable({}, {__mode = "v"}) node1.next = node2 refs[1] = node2 -- Store weak reference return node1, refs end
- Implementing cache with size limits:
-- Solution 2: Limited cache implementation local LRUCache = {} LRUCache.__index = LRUCache function LRUCache.new(maxSize) return setmetatable({ maxSize = maxSize, items = {}, access = {} }, LRUCache) end function LRUCache:put(key, value) if #self.items >= self.maxSize then -- Remove least recently used item local lru = table.remove(self.access, 1) self.items[lru] = nil end self.items[key] = value table.insert(self.access, key) end
- Table pooling for frequent allocations:
-- Solution 3: Object pool local Pool = {} Pool.__index = Pool function Pool.new() return setmetatable({ free = {}, active = setmetatable({}, {__mode = "v"}) -- Weak values }, Pool) end function Pool:acquire() local obj = table.remove(self.free) or {} self.active[obj] = true return obj end function Pool:release(obj) if self.active[obj] then self.active[obj] = nil table.insert(self.free, obj) for k in pairs(obj) do obj[k] = nil -- Clear object end end end
Best Practices
- Use weak tables for caches and observers
- Implement size limits for growing collections
- Clear table fields when no longer needed
- Use table pools for frequent allocations/deallocations
- Avoid unnecessary table creation in loops
Memory Profiling
-- Memory usage tracking local function getMemoryUsage() return collectgarbage("count") * 1024 -- in bytes end local function trackMemory(fn) local before = getMemoryUsage() fn() local after = getMemoryUsage() print(string.format("Memory delta: %.2f KB", (after - before) / 1024)) end
Common Pitfalls
-- Pitfall 1: Holding references too long local function processLargeData(data) local results = {} for i = 1, #data do results[i] = heavyComputation(data[i]) -- Process results immediately instead of storing processResult(results[i]) results[i] = nil -- Clear reference end end -- Pitfall 2: Not clearing table fields local function reuseTable(t) -- Wrong: just overwriting values t[1] = newValue -- Correct: clear old values first for k in pairs(t) do t[k] = nil end t[1] = newValue end
Weak References Example
-- Implementing an event system with weak references local EventSystem = {} function EventSystem.new() return setmetatable({ listeners = setmetatable({}, {__mode = "k"}) }, {__index = EventSystem}) end function EventSystem:subscribe(observer, callback) self.listeners[observer] = callback end function EventSystem:emit(event) for observer, callback in pairs(self.listeners) do callback(event) end end
Related Concepts
- Garbage collection
- Reference counting
- Weak references
- Object pooling
- Cache invalidation