Есть ли утечки памяти с javascript, вызывающим функцию в обратном вызове рекурсивно?

Скажем, например, вы пишете программу, которая ждет сообщения в очереди, обрабатывает ее, а затем ждет следующего сообщения, и это продолжается вечно. На языке, таком как C или Java, он выглядит примерно так:

void processMessage() {
    while (true) {
        // waitForMessage blocks until the next message is received
       msg = waitForMessage(); 
      // handle msg here
    }
}

В Javascript (я использую node.js, btw), поскольку используются обратные вызовы, он обычно выглядит так:

function processMessage() {
    waitForMessage(function(msg) {
        // handle msg or error here
        processMessage();
    }); 
}

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

Ответ 1

Нет, рекурсивные вызовы функций не вызывают утечки памяти в Javascript.

Единственная память, используемая вызовом функции, - это бит пространства стека (поэтому интерпретатор знает, куда идти, когда возвращается функция) и независимо от того, какая память используется объектом области видимости функции (например, локальные переменные). Эта память стека возвращается в систему при возврате вызова функции. Он не течет.

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

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

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

Если вы связываете/встраиваете асинхронные операции друг в друга, то дополнительные объекты области будут сохраняться на время асинхронной операции, но именно так работает Javascript и предлагает возможности доступа к вашей родительской области. На практике, как правило, это не проблема памяти. Объекты Scope сами по себе являются относительно компактными объектами (один создается почти для каждого вызова функции), как я сказал выше, если вы не ставите гигантские буферы или гигантские строки/массивы в сохраняющуюся область, использование памяти обычно не имеет значения.


Также имейте в виду, что когда вы вызываете processMessage() снова из асинхронного обратного вызова, который не является рекурсией, о которой вы, возможно, обычно думаете, поскольку предыдущий вызов функции processMessage() уже возвращен, и стек имеет полностью разматывается до того, как асинхронное событие вызывает обратный вызов. Таким образом, в этом случае нет наращивания стека. Это связано с тем, что асинхронные операции в Javascript все проходят через очередь событий. Когда асинхронная операция готова к запуску действия, она помещает событие в очередь событий Javascript. Это событие обрабатывается только тогда, когда текущий поток JS-операции завершен и полностью размотан. Только тогда JS-интерпретатор смотрит в очередь событий, чтобы увидеть, есть ли что-то еще. Таким образом, стек всегда полностью разматывается до запуска следующей асинхронной операции.

Подробнее о том, как это работает, и о нескольких справочных статьях в очереди событий JS (которая работает одинаково в node.js, что она делает в браузере), см. в этой статье:

Как JavaScript обрабатывает ответы AJAX в фоновом режиме?

Это одна из причин, по которой Joyent называет node.js a "управляемую событиями, неблокирующую модель ввода/вывода" прямо на node.js главная страница.

Ответ 2

Функции не выделяются в стеке. Функция обратного вызова будет собирать мусор после ее использования, если по какой-либо причине не будет содержаться ссылка. Ваш код должен быть в порядке!