Перейти к основному содержимому

Работа с памятью и сборка мусора

Разработчику Архитектору

Работа с памятью и сборка мусора

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: изменение через одну ссылку видно через другую

Разбор:

  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: print().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Таким образом, Lua реализует ссылочную семантику для составных типов, аналогично Python, Java или JavaScript.


Жизненный цикл объекта

Жизненный цикл объекта в куче включает несколько этапов:

  • Выделение — при создании ({}, function(), coroutine.create()).
  • Использование — объект доступен через одну или несколько ссылок.
  • Недоступность — все прямые и косвенные ссылки на объект утеряны.
  • Сборка мусора — GC помечает объект как "мертвый" и освобождает память.
  • Финализация (опционально) — если у объекта есть метод __gc, он вызывается перед освобождением.

Интерактивная модель — фазы mark-and-sweep и достижимость объектов (языконезависимо). Подробнее: автоматическое управление памятью.

Play ITЗагрузка интерактивного демо…


Управление сборщиком мусора

Сборщик мусора работает невидимо для большинства программ, но его поведение можно анализировать, настраивать и даже принудительно запускать.

Lua использует incremental mark-and-sweep garbage collector — это означает, что процесс сборки разбит на этапы и выполняется по частям между шагами исполнения программы.

GC работает в два этапа:

  1. Mark (пометка). GC начинает с корней — глобальных переменных, локальных переменных в активных стеках, регистров VM, рекурсивно помечает все объекты, достижимые по ссылкам. Каждый достижимый объект получает флаг "жив".
  2. Sweep (очистка). Проход по всем объектам в куче, непомеченные объекты (недостижимые) освобождаются, а память возвращается системе или пулу. Для объектов с метаметодом __gc вызывается финализатор на следующем цикле GC, чтобы избежать проблем с порядком удаления.

Этапы mark и sweep выполняются порциями, чтобы минимизировать паузы. Это критично для реального времени (например, игры), и называется инкрементальность.

GC запускается автоматически, когда объём выделённой памяти превышает порог, рассчитываемый на основе предыдущего объёма живых объектов и коэффициентов сборки.

Нужно ли вызывать GC вручную? Функция collectgarbage() позволяет взаимодействовать с GC явно:

collectgarbage("collect") -- Принудительный запуск полного цикла GC
collectgarbage("count") -- Возвращает текущий объём памяти в КБ
collectgarbage("step", step) -- Выполнить один шаг сборки
collectgarbage("stop") -- Остановить GC
collectgarbage("restart") -- Возобновить GC

Разбор:

  • collectgarbage управляет сборщиком мусора: можно запросить сбор, узнать объём памяти или настроить паузу.
  • Ключевые вызовы в фрагменте: collectgarbage().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Когда стоит использовать принудительный вызов?

  • После загрузки ресурсов (например, уровня в игре), чтобы очистить временные данные.
  • Перед критическими участками (анимация, ввод), чтобы минимизировать паузы GC.
  • Для профилирования — отслеживание роста памяти.

И соответственно, не нужно в обычном потоке выполнения (GC и так работает эффективно), а частые вызовы collectgarbage("collect") могут ухудшить производительность, так как нарушают адаптивность GC. Используйте ручной GC целенаправленно и редко, как инструмент управления пиковыми нагрузками.


Ссылки

Все составные типы в Lua передаются и хранятся по ссылке.

Это означает, что:

  1. Присваивание таблицы не создаёт копию:
local a = { x = 1 }
local b = a

Разбор:

  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Теперь a и b указывают на один объект. Удаление a не освободит память, пока b существует.

  1. Замыкания захватывают переменные по ссылке:
function make_counter()
local count = 0
return function() count = count + 1; return count end
end

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • return завершает функцию и может вернуть несколько значений через запятую.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Объект 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" }) -- слабые ключи и значения

Разбор:

  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: setmetatable().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Как работают слабые ссылки? Слабая ссылка не предотвращает сборку мусора объекта. Если объект достигается только через слабые ссылки, он считается недостижимым и будет собран.

Пример - кэш объектов:

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

Разбор:

  • function объявляет функцию; end закрывает тело. Имя после function становится ссылкой на вызываемый объект.
  • if … then … end выбирает ветку по truthiness: ложными считаются только nil и false.
  • Логика записывается словами and/or/not, а не символами &&/||/! как в C-подобных языках.
  • return завершает функцию и может вернуть несколько значений через запятую.
  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: setmetatable().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Здесь значения (объекты) будут автоматически удаляться GC, если на них нет других ссылок. Это позволяет строить автоматически очищающиеся кэши.

Слабые ссылки работают только для таблиц и userdata. Строки и числа не участвуют в GC (строки — иммутируемые, часто interned).


Финализаторы

Объекты типа userdata (и таблицы, если включено __gc) могут иметь финализатор — код, выполняемый перед освобождением.

-- newproxy есть в Lua 5.1; в 5.2+ используйте userdata + метатаблицу с __gc
local obj = setmetatable({}, {
__gc = function(self)
print("Объект уничтожается")
end
})

Разбор:

  • Точка с запятой в Lua необязательна; здесь она просто разделяет несколько выражений в одной строке.
  • setmetatable связывает таблицу с метатаблицей; поля __… задают перехват операций (индекс, арифметика, вызов).
  • Фигурные скобки {} создают таблицу: можно задать поля key = value и/или элементы-массив с индексами с 1.
  • Ключевые вызовы в фрагменте: print(), setmetatable().
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Финализаторы вызываются не сразу после потери доступности, а на следующем цикле GC. Не стоит полагаться на них для освобождения внешних ресурсов (файлы, сокеты) — лучше делать это явно. Финализаторы могут создавать новые ссылки на объект — тогда он воскрешается (resurrection) и не будет удалён.

Lua предоставляет простые, но эффективные средства для мониторинга использования памяти.

  1. collectgarbage("count") возвращает текущий объём используемой памяти в килобайтах (с плавающей точкой):
print(collectgarbage("count")) -- например, 123.456

Разбор:

  • collectgarbage управляет сборщиком мусора: можно запросить сбор, узнать объём памяти или настроить паузу.
  • Ключевые вызовы в фрагменте: collectgarbage(), print().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.

Можно использовать для измерения потребления памяти до/после операции и для поиска утечек: если значение постоянно растёт без плато.

  1. Настройка поведения GC:
collectgarbage("count") -- объём памяти (КБ)
collectgarbage("setpause", 110) -- порог до следующего цикла, %
collectgarbage("setstepmul", 200) -- скорость шагов относительно аллокации
-- collectgarbage("stat") — только в отдельных форках (например, LuaJIT)

Разбор:

  • collectgarbage управляет сборщиком мусора: можно запросить сбор, узнать объём памяти или настроить паузу.
  • Ключевые вызовы в фрагменте: collectgarbage().
  • Комментарии после -- поясняют ожидаемый результат; при запуске интерпретатор их игнорирует.
  • При переносе в свой проект сохраните порядок шагов и проверьте результат через print или отладчик.
  1. Внешние инструменты:
    • luatrace — трассировка выделений.
    • glue.gccount (в наборах расширений) — детальный подсчёт.
    • Интеграция с debug-версиями движков (например, в Love2D или Nginx + OpenResty).