ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Память JavaScript: достижимость, утечки и проверка в DevTools

Автор: · Обновлено

Автоматическая сборка мусора освобождает недостижимые данные, но не знает, какие доступные объекты больше не нужны приложению. Поэтому поиск утечки начинается с владельцев ссылок и повторяемого сценария, а не с попытки вручную вызвать сборщик памяти.

Среда и проверяемая часть задачи

Основной пример сохраните как memory.mjs и выполните в Node.js 22 или новее. Он использует стандартный EventTarget и также работает в консоли современного браузера. Набор данных небольшой: упражнение проверяет очистку подписки, а не намеренно исчерпывает память компьютера. Для профилирования во второй части потребуется Chrome DevTools и отдельная учебная страница.

Управление памятью включает создание значений, использование и прекращение доступа к ним. Движок определяет достижимость через корни и цепочки ссылок. Переменная может исчезнуть из текущей области текста, но объект останется доступным через обработчик, коллекцию или замыкание. Цикл ссылок между объектами сам по себе не является утечкой, если весь этот граф больше недостижим.

Полный пример: подписка с явным владельцем

const bus = new EventTarget();
function attachReport(target) {
  let rows = Array.from({ length: 1000 }, (_, id) => ({ id }));
  let disposed = false;
  function onReport() {
    console.log('rows', rows.length);
  }
  target.addEventListener('report', onReport);
  return function dispose() {
    if (disposed) return;
    disposed = true;
    target.removeEventListener('report', onReport);
    rows = [];
  };
}
const dispose = attachReport(bus);
bus.dispatchEvent(new Event('report'));
dispose();
dispose();
bus.dispatchEvent(new Event('report'));
console.log('finished');

Ожидается rows 1000, затем finished. Второе событие не вызывает отчёт, а повторная очистка безопасна. Здесь наблюдается именно контракт освобождения ресурса: подписка удалена, сохранённый массив заменён пустым. Пример не утверждает, что сборщик уже вернул определённое число байтов операционной системе. Такое обещание нельзя вывести из одной строки rows = [].

Пока bus хранит обработчик, обработчик может обращаться к rows. Поэтому долгоживущий источник событий продлевает жизнь данных отчёта. Удаление обработчика разрывает важную связь. Мы также очищаем ссылку на массив, потому что сам disposer в примере остаётся доступным. В реальном коде нужно проверять все пути удержания, а не только самую заметную переменную.

Рост памяти имеет несколько причин

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

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

Размер JavaScript heap не равен всей памяти вкладки или процесса. Двоичные буферы, изображения, DOM, компилированный код и внутренние структуры браузера учитываются по-разному. Если счётчик JS стабилен, но вкладка тяжелеет, расширьте измерение по конкретным ресурсам. Нельзя объяснить любой рост одним общим термином «мусор не убирается».

Частые пути удержания

Неограниченные Map, массивы истории и кэши удерживают свои элементы, пока записи не удалены. Для кэша задайте максимальный размер, срок жизни или явное вытеснение. Замена Map на WeakMap не является универсальным решением: слабая коллекция имеет другой контракт и подходит не для любого ключа или сценария перечисления.

Таймеры, подписки на глобальные события и наблюдатели часто переживают экран, который их создал. Очистка должна отменять таймер, удалять обработчик или вызывать соответствующее завершение наблюдения. Для AbortSignal пользовательская подписка с once удалится только после отмены; если операция закончилась успешно без отмены, её всё равно нужно снять, когда она больше не нужна.

DOM-узел может быть удалён из документа, но оставаться доступным из JavaScript. Даже ссылка на один вложенный элемент способна удерживать связи с более крупным поддеревом. Само отсоединение не всегда ошибка: библиотека может временно хранить элемент для повторного использования. Утечкой это становится, когда владение потеряно, а дальнейшее использование не предусмотрено.

Что действительно даёт WeakMap

WeakMap полезен для метаданных объекта, которые нужны только пока сам объект доступен по обычным сильным ссылкам. Коллекция не должна самостоятельно продлевать жизнь такого ключа. Но она не отменяет обработчики, не закрывает соединения и не ограничивает другую сильную коллекцию, которая хранит те же объекты.

const metadata = new WeakMap();
const lesson = { title: 'Память' };
metadata.set(lesson, { measuredAt: 1 });
console.log(metadata.get(lesson).measuredAt);
console.log(metadata.has(lesson));
metadata.delete(lesson);
console.log(metadata.has(lesson));

Вывод: 1, true, false. Это самостоятельный пример операций коллекции, а не тест сборки мусора. Перечислить все её ключи или получить обычный size нельзя: интерфейс не предоставляет наблюдение за моментами исчезновения ключей. Если приложению нужен полный список активных сущностей, выберите другую структуру с явным удалением.

Практика: сравнить два цикла жизни в DevTools

На отдельной учебной странице создайте кнопку открытия панели и кнопку закрытия. При открытии добавляйте небольшое DOM-поддерево и подписку на window; callback должен действительно читать поле панели. В первой версии закрытие только удаляет элемент. Во второй — также снимает подписку и очищает ненужные сохранённые ссылки. Не запускайте сотни тысяч повторов: нескольких одинаковых циклов достаточно для изучения механизма.

Снимите Heap Snapshot исходного состояния, выполните несколько открытий и закрытий, затем снимите следующий. В сравнении найдите продолжающие жить экземпляры панели или отсоединённые узлы. В разделе Retainers проследите путь до обработчика и владельца подписки. После исправления повторите тот же сценарий; ожидается исчезновение накопления экземпляров от каждого нового цикла, а не обязательно точное возвращение общего счётчика к исходному байту.

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

Для интенсивного создания временных значений используйте Allocation sampling или запись выделений. Снимок отвечает, что удерживается сейчас, а профиль выделений помогает понять, какие функции создают объекты во время сценария. Сначала определите, ищете ли вы накопление, большой стабильный набор или частые короткоживущие выделения; выбор инструмента зависит от этого вопроса.

Частые вопросы

Нужно ли присваивать null каждой локальной переменной?

Нет. После завершения области недостижимые данные обычно не требуют ручной очистки всех имён. Явное удаление ссылки полезно там, где долгоживущий владелец продолжает удерживать уже ненужный ресурс. Найдите эту границу, а не добавляйте присваивания повсюду.

Можно ли использовать финализатор для обязательного сохранения?

Нет. Момент сборки и запуска финализации не гарантируется, а процесс может завершиться раньше. Сохранение, отмена запросов и закрытие ресурса должны иметь явный жизненный цикл. Слабые ссылки и финализация не заменяют протокол завершения.

Почему после исправления память процесса не сразу уменьшилась?

Движок может оставить освобождённые области для последующих выделений, а браузер управляет и другими ресурсами. Проверяйте удерживаемые объекты и повторяемый рост после одинаковых действий. Стабилизация рабочего набора часто информативнее мгновенного показания одного общего счётчика.

Источники