5.15. Работа с памятью и сборка мусора
Работа с памятью и сборка мусора
Работа с памятью и сборка мусора
Автоматическая сборка мусора (GC) на основе mark-and-sweep.
Слабые таблицы (__mode = "k" или "v") — контроль за ссылками.
Профилирование: collectgarbage("count"), принудительный вызов GC.
Нет указателей, но есть ссылки на таблицы и функции.
Lua — язык с автоматическим управлением памятью. Программист не отвечает за выделение и освобождение объектов вручную, как в C или C++. Вместо этого Lua использует сборщик мусора (Garbage Collector, GC), который автоматически определяет и освобождает память, занятую объектами, более недоступными для программы.
Этот механизм позволяет сосредоточиться на логике приложения, но требует понимания его принципов: ведь даже автоматическая система может привести к утечкам, паузам или непредсказуемому поведению, если использоваться без учёта её особенностей.
Lua использует гибридную модель управления памятью, сочетающую стек и кучу (heap).
- Стек вызовов (call stack) хранит локальные переменные, параметры функций и информацию о возврате, управляется интерпретатором строго по принципу LIFO. Объекты на стеке существуют только во время выполнения соответствующей функции. При выходе из функции локальные ссылки удаляются — но сами объекты (если они находятся в куче) могут сохраняться. Важно: стек хранит значения примитивных типов (числа, булевы, nil) и указатели на объекты в куче (таблицы, функции, строки).
- Куча (heap), централизованная область памяти, где размещаются все составные объекты: таблицы, функции (включая замыкания), потоки (coroutines), строки (неизменяемые, часто кэшируются). Управление памятью в куче полностью делегировано сборщику мусора. Все объекты в куче управляются по ссылкам: переменные содержат не сами объекты, а ссылки на них.
local a = { x = 1 }
local b = a -- b ссылается на тот же объект в куче
b.x = 2
print(a.x) -- 2: изменение через одну ссылку видно через другую
Таким образом, Lua реализует ссылочную семантику для составных типов, аналогично Python, Java или JavaScript.
Жизненный цикл объекта в куче включает несколько этапов:
- Выделение — при создании (
{}, function(), coroutine.create()). - Использование — объект доступен через одну или несколько ссылок.
- Недоступность — все прямые и косвенные ссылки на объект утеряны.
- Сборка мусора — GC помечает объект как "мертвый" и освобождает память.
- Финализация (опционально) — если у объекта есть метод
__gc, он вызывается перед освобождением.
Сборщик мусора работает невидимо для большинства программ, но его поведение можно анализировать, настраивать и даже принудительно запускать.
Lua использует incremental mark-and-sweep garbage collector — это означает, что процесс сборки разбит на этапы и выполняется по частям между шагами исполнения программы.
GC работает в два этапа:
- Mark (пометка). GC начинает с корней — глобальных переменных, локальных переменных в активных стеках, регистров VM, рекурсивно помечает все объекты, достижимые по ссылкам. Каждый достижимый объект получает флаг «жив».
- Sweep (очистка). Проход по всем объектам в куче, непомеченные объекты (недостижимые) освобождаются, а память возвращается системе или пулу. Для объектов с метаметодом
__gcвызывается финализатор на следующем цикле GC, чтобы избежать проблем с порядком удаления.
Этапы mark и sweep выполняются порциями, чтобы минимизировать паузы. Это критично для реального времени (например, игры), и называется инкрементальность.
GC запускается автоматически, когда объём выделённой памяти превышает порог, рассчитываемый на основе предыдущего объёма живых объектов и коэффициентов сборки.
Нужно ли вызывать GC вручную? Функция collectgarbage() позволяет взаимодействовать с GC явно:
collectgarbage("collect") -- Принудительный запуск полного цикла GC
collectgarbage("count") -- Возвращает текущий объём памяти в КБ
collectgarbage("step", step) -- Выполнить один шаг сборки
collectgarbage("stop") -- Остановить GC
collectgarbage("restart") -- Возобновить GC
Когда стоит использовать принудительный вызов?
- После загрузки ресурсов (например, уровня в игре), чтобы очистить временные данные.
- Перед критическими участками (анимация, ввод), чтобы минимизировать паузы GC.
- Для профилирования — отслеживание роста памяти.
И соответственно, не нужно в обычном потоке выполнения (GC и так работает эффективно), а частые вызовы collectgarbage("collect") могут ухудшить производительность, так как нарушают адаптивность GC. Используйте ручной GC целенаправленно и редко, как инструмент управления пиковыми нагрузками.
Все составные типы в Lua передаются и хранятся по ссылке.
Это означает, что:
- Присваивание таблицы не создаёт копию:
local a = { x = 1 }
local b = a
Теперь a и b указывают на один объект. Удаление a не освободит память, пока b существует.
- Замыкания захватывают переменные по ссылке:
function make_counter()
local count = 0
return function() count = count + 1; return count end
end
Объект count будет жить до тех пор, пока живёт замыкание. Таким образом, главная причина утечек памяти в Lua — непреднамеренное удержание ссылок.
Lua предоставляет механизм слабых ссылок через слабые таблицы, создаваемые с помощью метаметода __mode.
local weak_k = {}
setmetatable(weak_k, { __mode = "k" }) -- слабые ключи
local weak_v = {}
setmetatable(weak_v, { __mode = "v" }) -- слабые значения
local weak_kv = {}
setmetatable(weak_kv, { __mode = "kv" }) -- слабые ключи и значения
Как работают слабые ссылки? Слабая ссылка не предотвращает сборку мусора объекта. Если объект достигается только через слабые ссылки, он считается недостижимым и будет собран.
Пример - кэш объектов:
local cache = setmetatable({}, { __mode = "v" })
function get_or_create(key)
if not cache[key] then
cache[key] = expensive_creation(key)
end
return cache[key]
end
Здесь значения (объекты) будут автоматически удаляться GC, если на них нет других ссылок. Это позволяет строить автоматически очищающиеся кэши.
Слабые ссылки работают только для таблиц и userdata. Строки и числа не участвуют в GC (строки — иммутируемые, часто interned).
Объекты типа userdata (и таблицы, если включено __gc) могут иметь финализатор — код, выполняемый перед освобождением.
local obj = newproxy(true) -- userdata-like object
getmetatable(obj).__gc = function(self)
print("Объект уничтожается")
end
Финализаторы вызываются не сразу после потери доступности, а на следующем цикле GC. Не стоит полагаться на них для освобождения внешних ресурсов (файлы, сокеты) — лучше делать это явно. Финализаторы могут создавать новые ссылки на объект — тогда он воскрешается (resurrection) и не будет удалён.
Lua предоставляет простые, но эффективные средства для мониторинга использования памяти.
- collectgarbage("count") возвращает текущий объём используемой памяти в килобайтах (с плавающей точкой):
print(collectgarbage("count")) -- например, 123.456
Можно использовать для измерения потребления памяти до/после операции и для поиска утечек: если значение постоянно растёт без плато.
- Дополнительные метрики:
-- Общий объём памяти (в КБ)
collectgarbage("count")
-- Количество вызовов GC
collectgarbage("stat") -- в некоторых реализациях (например, LuaJIT)
-- Настройка поведения GC
collectgarbage("setpause", 110) -- % использования до следующего цикла
collectgarbage("setstepmul", 200) -- скорость сборки относительно аллокации
- Внешние инструменты:
- luatrace — трассировка выделений.
- glue.gccount (в наборах расширений) — детальный подсчёт.
- Интеграция с debug-версиями движков (например, в Love2D или Nginx + OpenResty).