Управление памятью Node.js и механизм сборки мусора V8

Node.js
Управление памятью Node.js и механизм сборки мусора V8

Для студентов, изучающих исследования и разработки серверов Node.js, в отношении сборки мусора и освобождения памяти нет необходимости вручную создавать такие операции, как удаление/освобождение после создания объекта, как это делают студенты C/C++ для выполнения GC (сборки мусора). .Recycling), Node.js, как и Java, автоматически управляется виртуальной машиной, но это не значит, что можно сидеть сложа руки и расслабляться. Проблема, так что делайте Как квалифицированный инженер по исследованиям и разработкам на стороне сервера, все же необходимо понимать, как виртуальная машина использует память, чтобы вы могли спокойно решать проблемы.

Быстрая навигация

  • Сборщик мусора в Nodejs
  • Практика управления памятью для сборки мусора Nodejs
    • Идентификация утечки памяти
    • пример утечки памяти
    • Освобождение памяти для сборки мусора вручную
  • Механизм сборки мусора V8
    • Ограничение памяти кучи V8
    • Новое поколение и старое поколение
    • Кайнозойское пространство и алгоритм очистки
    • Пространство старого поколения и алгоритм Mark-Sweep Mark-Compact
    • Сводка коллекции мусора V8
  • утечка памяти
    • глобальная переменная
    • Закрытие
    • Будьте осторожны с памятью в качестве кеша
    • Память частной переменной модуля постоянна
    • слушатель повторения события
    • Другие соображения
  • инструмент обнаружения памяти

Сборщик мусора в Nodejs

Node.js — это среда выполнения JavaScript, основанная на движке Chrome V8. Это отрывок с официального сайта Node.js, поэтому V8 — это виртуальная машина, используемая в Node.js. в Разговоре о GC V8.

Отношения между Node.js и V8 похожи на отношения между Java и JVM.Кроме того, когда Райан Даль, отец Node.js, выбрал V8 в качестве виртуальной машины Node.js, производительность V8 была впереди всех. другие виртуальные машины JavaScript в то время. , по-прежнему имеет лучшую производительность, поэтому, когда мы проводим оптимизацию Node.js, при обновлении версии производительность будет улучшаться вместе с ней.

Практика управления памятью для сборки мусора Nodejs

Давайте сначала посмотрим на процесс сборки мусора в Node.js через демо?

Идентификация утечки памяти

Предоставляет метод процесса. MemoryUsage в среде Node.js, чтобы увидеть текущее использование памяти процессов, блок байт

  • RSS (размер резидентов): часть памяти, занятой процессом, сохраненным в оперативной памяти, включая сам код, стек и кучу.
  • heapTotal: общий объем памяти, выделенной в куче.
  • heapUsed: объем памяти, используемый в настоящее время в куче. В основном мы используем это поле для оценки утечек памяти.
  • external: память, занимаемая объектами C++ внутри движка V8.
/**
 * 单位为字节格式为 MB 输出
 */
const format = function (bytes) {
    return (bytes / 1024 / 1024).toFixed(2) + ' MB';
};

/**
 * 封装 print 方法输出内存占用信息 
 */
const print = function() {
    const memoryUsage = process.memoryUsage();

    console.log(JSON.stringify({
        rss: format(memoryUsage.rss),
        heapTotal: format(memoryUsage.heapTotal),
        heapUsed: format(memoryUsage.heapUsed),
        external: format(memoryUsage.external),
    }));
}

пример утечки памяти

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

// example.js
function Quantity(num) {
    if (num) {
        return new Array(num * 1024 * 1024);
    }

    return num;
}

function Fruit(name, quantity) {
    this.name = name
    this.quantity = new Quantity(quantity)
}

let apple = new Fruit('apple');
print();
let banana = new Fruit('banana', 20);
print();

Выполняя приведенный выше код, память показана ниже, размер heapUsed объекта apple составляет всего 4,21 МБ, а для банана мы создаем большое пространство массива для его свойства количества, что приводит к увеличению размера heapUsed до 164,24 МБ.

$ node example.js

{"rss":"19.94 MB","heapTotal":"6.83 MB","heapUsed":"4.21 MB","external":"0.01 MB"}
{"rss":"180.04 MB","heapTotal":"166.84 MB","heapUsed":"164.24 MB","external":"0.01 MB"}

Мы посмотрим на использование памяти, root для каждого объекта содержит ссылку, вы не можете отпустить ничего не привести к GC, на следующем рисунке показан положительный

图片描述

Освобождение памяти для сборки мусора вручную

Предположим, мы больше не используем объект-банан, переназначаем ему какое-то новое значение, например, банан = null, и смотрим, что теперь происходит?

图片描述

Результат показан на рисунке выше, объект Banana недоступен из корневого объекта, тогда Banana будет освобожден при следующем запуске сборщика мусора.

Давайте смоделируем сборку мусора и посмотрим, как выглядит реальная ситуация?

// example.js
let apple = new Fruit('apple');
print();
let banana = new Fruit('banana', 20);
print();
banana = null;
global.gc();
print();

Параметр --expose-gc в следующем коде указывает, что механизм сборки мусора разрешено выполнять вручную.Объекту банана присваивается значение null, а затем выполняется GC.В результате, напечатанном третьим отпечатком, мы видим, что использование heapUsed сократилось с 164,24 МБ до 3,97 МБ.

$ node --expose-gc example.js
{"rss":"19.95 MB","heapTotal":"6.83 MB","heapUsed":"4.21 MB","external":"0.01 MB"}
{"rss":"180.05 MB","heapTotal":"166.84 MB","heapUsed":"164.24 MB","external":"0.01 MB"}
{"rss":"52.48 MB","heapTotal":"9.33 MB","heapUsed":"3.97 MB","external":"0.01 MB"}

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

图片描述

Механизм сборки мусора V8

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

Ограничение памяти кучи V8

На стороне сервера память — очень ценная вещь: в V8 64-разрядная машина ограничена примерно 1,4 ГБ, а 32-разрядная — около 0,7 ГБ. Поэтому при некоторых операциях с большой памятью следует соблюдать осторожность, иначе превышение лимита памяти V8 приведет к завершению процесса.

Пример чрезмерного переполнения памяти

// overflow.js
const format = function (bytes) {
    return (bytes / 1024 / 1024).toFixed(2) + ' MB';
};

const print = function() {
    const memoryUsage = process.memoryUsage();
    console.log(`heapTotal: ${format(memoryUsage.heapTotal)}, heapUsed: ${format(memoryUsage.heapUsed)}`);
}

const total = [];
setInterval(function() {
    total.push(new Array(20 * 1024 * 1024)); // 大内存占用
    print();
}, 1000)

В приведенном выше примере total — это глобальная переменная, которая каждый раз увеличивается примерно на 160 МБ и не будет переработана.Когда она близка к границе V8, она не может выделять память и вызывает переполнение памяти процесса.

$ node overflow.js
heapTotal: 166.84 MB, heapUsed: 164.23 MB
heapTotal: 326.85 MB, heapUsed: 324.26 MB
heapTotal: 487.36 MB, heapUsed: 484.27 MB
heapTotal: 649.38 MB, heapUsed: 643.98 MB
heapTotal: 809.39 MB, heapUsed: 803.98 MB
heapTotal: 969.40 MB, heapUsed: 963.98 MB
heapTotal: 1129.41 MB, heapUsed: 1123.96 MB
heapTotal: 1289.42 MB, heapUsed: 1283.96 MB

<--- Last few GCs --->

[87581:0x103800000]    11257 ms: Mark-sweep 1283.9 (1290.9) -> 1283.9 (1290.9) MB, 512.1 / 0.0 ms  allocation failure GC in old space requested
[87581:0x103800000]    11768 ms: Mark-sweep 1283.9 (1290.9) -> 1283.9 (1287.9) MB, 510.7 / 0.0 ms  last resort GC in old space requested
[87581:0x103800000]    12263 ms: Mark-sweep 1283.9 (1287.9) -> 1283.9 (1287.9) MB, 495.3 / 0.0 ms  last resort GC in old space requested


<--- JS stacktrace --->

В V8 также предусмотрены два параметра для настройки предельного размера памяти только на этапе запуска.

Это должно настроить пространство старого поколения и нового поколения, а старое поколение и новое поколение будут представлены позже.

  • --max-old-space-size=2048
  • --max-new-space-size=2048

Конечно, памяти не чем больше, тем лучше, с одной стороныресурсы сервераЭто дорого, с другой стороны, говорят V81,5 ГБ динамической памятисделать небольшойСбор мусора занимает около 50 мсСо временем это приведет к приостановке потока JavaScript, что является наиболее важным аспектом.

Новое поколение и старое поколение

Абсолютное большинство объектов приложения будет иметь короткое время жизни, в то время как некоторые объекты будут иметь длительное время жизни.Чтобы воспользоваться этой ситуацией, V8 делит кучу на две категории: молодые и старые.Объекты в новом пространстве очень малы, около 1 -8MB и сборка мусора здесь тоже быстрая. Объекты, пережившие процесс сборки мусора в пространстве молодого поколения, будут переведены в пространство старого поколения.

Кайнозойское пространство

Поскольку сборка мусора в новом пространстве происходит часто, ее нужно обрабатывать очень быстро, используя алгоритм очистки, который был написан Ч. Дж. Чейни в 1970 году в статьеA nonrecursive list compacting algorithmпредложить.

Scavenge — это алгоритм репликации.Пространство нового поколения будет разделено на два равных по размеру из-пространства и в-пространство. Это работает путем копирования объектов, которые находятся в пространстве from, затем перемещаются в пространство to или перемещаются в пространство старого поколения, а объекты, которые не являются живыми в пространстве from, будут освобождены. Поменяйтесь местами из космоса и в космос после создания этих копий.

图片描述

Алгоритм Scavenge очень быстр и подходит для сборки мусора небольшого объема памяти, но имеет большие накладные расходы, что приемлемо для небольшого объема памяти в молодом поколении.

пространство старого поколения

Пространство нового поколения будет переведено в пространство старого поколения, когда сборка мусора соответствует определенным условиям (происходит ли восстановление очистки, отношение памяти к пространству), а объекты в пространстве старого поколения испытали хотя бы одно или несколько переработка, поэтому у них больше шансов на выживание. Алгоритм Scavenge имеет два основных недостатка: во-первых, он будет дублировать уцелевшие объекты, что делает эффективность неэффективной, а во-вторых, это пустая трата пространственных ресурсов. используются в пространстве старого поколения.(Tag Collation) Алгоритм.

Mark-Sweep

Обработка Mark-Sweep делится на два этапа: пометка и очистка.В отличие от алгоритма Scavenge, который копирует только живые объекты, в пространстве старого поколения, поскольку живые объекты составляют большинство, Mark-Sweep проходит все объекты в куче. в фазе маркировки и маркирует только живые объекты Немаркированные мертвые объекты удаляются, после чего маркировка завершается.

图片描述

Вроде бы все отлично, но проблема все же есть: очищенные объекты разбросаны по адресам памяти, в результате получается много фрагментов памяти.

Mark-Compact

В пространстве старого поколения для решения проблемы фрагментации памяти алгоритма Mark-Sweep вводится Mark-Compact (алгоритм сортировки по меткам), который перемещает живой объект в один конец в процессе работы. пространство памяти компактно и движение завершено.После этого непосредственно очистить память за пределами.

图片描述

Сводка по сборке мусора V8

Почему вывоз мусора стоит дорого? V8 использует другой алгоритм сборки мусора Scavenge, Mark-Sweep, Mark-Compact. Этих трех алгоритмов сбора мусора нельзя избежать в приложениях для отказа, необходимо приостановить переработку, сбор мусора до тех пор, пока после завершения восстановления логики приложения для нового поколения пространства не будет так мало эффекта, но из-за старого пространства поколения больше живого объекты, пауза или будут затронуты, и, следовательно, V8 и добавляет новые способы уменьшить время задержки добавочной метки.

Об этом автор сборщик мусора V8 сказал очень поверхностно, просто подведите итоги в процессе обучения, если вы хотите узнать больше принципа, на непрофессиональном языке Node.js. Эта книга является хорошим выбором, также обратитесь к этим двум статьям.A tour of V8: Garbage Collection,Memory Management Reference..

утечка памяти

Утечка памяти (Memory Leak) относится к тому факту, что динамически выделенная куча памяти в программе не освобождается или не может быть освобождена по какой-либо причине, что приводит к пустой трате системной памяти, что приводит к серьезным последствиям, таким как замедление скорости работы программы или даже сбой системы.

глобальная переменная

Необъявленные переменные или переменные, висящие под глобальным глобальным, не будут автоматически восстановлены и останутся в памяти до завершения процесса и не будут освобождены, если ссылочное отношение между ними не будет разрешено путем удаления или переназначения на undefined/null. Есть также несколько примеров глобальных переменных, упомянутых выше.

Закрытие

Это также общая ситуация с утечкой памяти. Закрытие будет относиться к переменной в родительской функции. Если закрытие не выпущено, родительская переменная, упомянутая закрытие, не будет выпущена, что приводит к утечке памяти.

Реальный случай — The Meteor Case-Study, в 2013 году создатели Meteor объявили результаты своего расследования обнаруженной ими утечки памяти. Рассматриваемый фрагмент кода выглядит следующим образом

var theThing = null
var replaceThing = function () {
  var originalThing = theThing
  var unused = function () {
    if (originalThing)
      console.log("hi")
  }
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage)
    }
  };
};
setInterval(replaceThing, 1000)

При выполнении приведенного выше кода каждый раз при выполнении метода replaceThing будет создаваться новый объект, но предыдущий объект не освобождается, что приводит к утечке памяти. Эта часть включает в себя концепцию закрытия“同一个作用域生成的闭包对象是被该作用域中所有下一级作用域共同持有的”Поскольку определение unused использует переменную originalThing области, объект замыкания (someMethod) в области видимости функции уровня replaceThing также содержит переменную originalThing (重点:someMethod 的闭包作用域和 unused 的作用域是共享的), эталонное отношение между нимиtheThing 引用了 longStr 和 someMethod,someMethod 引用了 originalThing,originalThing 又引用了上次的 theThing, таким образом, образуя привязанную ссылку.

Приведенный выше код взят из блога Meteor.An interesting kind of JavaScript memory leak, для большего понимания, пожалуйста, обратитесь кОбсуждение вопросов Node-Interview #7

Будьте осторожны с памятью в качестве кеша

Кэширование через память может быть самым быстрым методом реализации, о котором мы только можем подумать.Кроме того, кэширование все еще очень распространено в бизнесе, но после понимания модели памяти и механизма сборки мусора в Node.js мы должны быть осторожны при его использовании.почему? Чем больше ключей хранится в кеше, тем больше долгоживущих объектов, которые будут бесполезны при сборке мусора.

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

const memoryStore = new Map();

exports.getUserToken = function (key) {
    const token = memoryStore.get(key);

    if (token && Date.now() - token.now > 2 * 60) {
        return token;
    }

    const dbToken = db.get(key);
    memoryStore.set(key, {
        now: Date.now(),
        val: dbToken,
    });
    return token;
}

Память частной переменной модуля постоянна

Перед загрузкой модуля кода следующая функция Node.js использует оболочку для упаковки, чтобы убедиться, что верхняя часть переменной (var, const, let) находится в пределах диапазона модуля, а не глобального объекта.

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

(function(exports, require, module, __filename, __dirname) {
    // 模块的代码实际上在这里
});

Небольшое предложение: рекомендуется использовать const для кэширования ссылки на модуль только после первой загрузки заголовка, а не загружать его каждый раз, когда он используется (для каждого требования требуется анализ пути и оценка кеша)

пример 1:

const a = require('a.js') // 推荐

function test() { 
    a.run()
}

Пример 2:

function test(){ // 不推荐
  require('a.js').run()
}

слушатель повторения события

Node.js в случае повторяющейся ошибки будет сообщен следующим образом: прослушиватель, фактически используемый класс EventEmitter, который содержит массив слушателей, прослушиватель 10 по умолчанию превышает это число, для обнаружения утечки памяти выдается следующее предупреждение: Также можно использовать метод emitter.setMaxListeners () для изменения указанного экземпляра EventEmitter предела.

(node:23992) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 connect listeners added. Use emitter.setMaxListeners() to increase limit

На поле существуют статью CNODE, анализируют утечку памяти, вызванную переподключением сокета, ссылкаУтечка, вызванная собственной стратегией усиления сокетов, а также утечка памяти, вызванная HTTP-модулем Node.js Keep-Alive, см.Github Node Issues #714

Другие соображения

При использовании таймера setInterval используйте соответствующий clearInterval для его очистки, потому что setInterval вернет значение после его выполнения и не будет автоматически освобожден. Кроме того, есть карта, фильтр и т. д. для работы с массивами.После каждой операции будет создаваться новый массив, который будет занимать память.Если вы просто проходите, например по карте, вы можете вместо этого использовать forEach.Это некоторые Детали находятся в разработке, но часто Детали определяют успех или неудачу, и каждая утечка памяти снова и снова вызывается непреднамеренными действиями. Поэтому эти моменты также требуют нашего внимания.

console.log(setInterval(function(){}, 1000)) // 返回一个 id 值
[1, 2, 3].filter(item => item % 2 === 0) // [2]
[1, 2, 3].map(item => item % 2 === 0) // [false, true, false]

инструмент обнаружения памяти

node-heapdump

heapdump — это инструмент для вывода информации о куче V8,node-heapdump

node-profiler

node-profiler — аналогичный захват моментального снимка памяти кучи с помощью инструмента node-heapdump, созданного командой alinode,node-profiler

Easy-Monitor

Облегченный мониторинг производительности ядра проекта Node.js + инструмент анализа,GitHub.com/Huayan Jing1991/EAS…

Node.js-Troubleshooting-Guide

Сбои приложений Node.js онлайн/офлайн, проблемы стресс-тестирования и руководство по настройке производительности,Node.js-Troubleshooting-Guide

alinode

Платформа производительности Node.js (платформа производительности Node.js) — это комплексное решение, обеспечивающее мониторинг производительности, напоминания о безопасности, устранение неполадок, оптимизацию производительности и другие услуги для средних и крупных приложений Node.js.alinode

Рекомендации по чтению