Подтвердить что ты не робот

Метод JavaScript setInterval() вызывает утечку памяти?

В настоящее время разрабатывается проект анимации на основе JavaScript.

Я заметил, что правильное использование setInterval(), setTimeout() и даже requestAnimationFrame выделяет память без моего запроса и вызывает частые вызовы сбора мусора. Дополнительные вызовы GC = мерцание: - (

Например; когда я выполняю следующий простой код, вызвав init() в Google Chrome, выделение памяти + сборка мусора прекрасна в течение первых 20-30 секунд...

function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

function draw()
{
    return true
}

Как-то, через минуту или около того, начинается странное увеличение выделенной памяти! Поскольку init() вызывается только один раз, , в чем причина увеличения объема выделенной памяти?

(Редактирование: снимок экрана с хромом)

chrome screenshot

ПРИМЕЧАНИЕ # 1: Да, я попытался вызвать clearInterval() перед следующим setInterval(). Проблема остается прежней!

ПРИМЕЧАНИЕ # 2: Чтобы изолировать проблему, я сохраняю этот код простым и глупым.

4b9b3361

Ответ 1

EDIT: Ответ Юрия лучше.


tl; dr IMO утечки памяти нет. Положительный наклон - это просто эффект setInterval и setTimeout. Мусор собирается, как видно из пилообразных узоров, что означает по определению отсутствие утечки памяти. (Я думаю).

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

Реальность такова, что нет реальной утечки памяти: сборщик мусора все еще способен собирать память. Утечка памяти по определению "возникает, когда компьютерная программа приобретает память, но не возвращает ее в операционную систему."

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

Я исследовал несколько способов перестановки интервалов, и все они приводят к одному и тому же пилообразному рисунку (хотя некоторые попытки приводят к тому, что сбор мусора никогда не происходит, поскольку ссылки сохраняются).

function doIt() {
    console.log("hai")
}

function a() {
    doIt();
    setTimeout(b, 50);
}
function b() {
    doIt();
    setTimeout(a, 50);
}

a();

http://fiddle.jshell.net/QNRSK/14/

function b() {
    var a = setInterval(function() {
        console.log("Hello");
        clearInterval(a);
        b();                
    }, 50);
}
b();

http://fiddle.jshell.net/QNRSK/17/

function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}
function draw()
{
    console.log('Hello');
}
init();

http://fiddle.jshell.net/QNRSK/20/

function init()
{
    window.ref = window.setInterval(function() { draw(); }, 50);
}
function draw()
{
    console.log('Hello');
    clearInterval(window.ref);
    init();
}
init();​

http://fiddle.jshell.net/QNRSK/21/

По-видимому, setTimeout и setInterval не являются официальными частями Javascript (следовательно, они не являются частью v8). Реализация остается до исполнителя. Я предлагаю вам взглянуть на реализацию setInterval и такое в node.js

Ответ 2

Проблема здесь не в самом коде, она не протекает. Это из-за реализации панели Timeline. Когда Timeline записывает события, мы собираем трассировки стека JavaScript при каждом вызове обратного вызова setInterval. Трассировка стека сначала выделяется в куче JS, а затем копируется в собственные структуры данных, после того как трассировка стека копируется в собственное событие, она становится мусором в куче JS. Это отражено на графике. Отключение следующего вызова http://trac.webkit.org/browser/trunk/Source/WebCore/inspector/TimelineRecordFactory.cpp#L55 делает график памяти плоским.

Есть ошибка, связанная с этой проблемой: https://code.google.com/p/chromium/issues/detail?id=120186

Ответ 3

Каждый раз, когда вы вызываете вызов функции, он создает кадр . В отличие от многих других языков, Javascript хранит фрейм стека в куче, как и все остальное. Это означает, что каждый раз, когда вы вызываете функцию, которую вы делаете каждые 50 мс, в кучу добавляется новый стек стека. Это добавляет и в конечном итоге собирает мусор.

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

Ответ 4

Я хотел ответить на ваш комментарий о setInterval и мерцании:

Я заметил, что правильное использование setInterval(), setTimeout() и даже requestAnimationFrame выделяет память без моего запроса и вызывает частые вызовы сбора мусора. Дополнительные вызовы GC = мерцание: - (

Возможно, вы захотите попробовать заменить вызов setInterval менее злой функцией самозапускания, основанной на setTimeout. Paul Irish упоминает об этом в разговоре 10 вещей, которые я узнал из источника jQuery (видео здесь, отмечает здесь см. # 2). То, что вы делаете, заменяет ваш вызов на setInterval функцией, которая косвенно косвенно запускается через setTimeout после завершения работы, которую она должна выполнять. Процитировать разговор:

Многие утверждают, что setInterval является злой функцией. Он продолжает вызывать функцию через определенные интервалы независимо от того, закончена или нет функция.

Используя приведенный выше пример кода, вы можете обновить свою функцию init с помощью

function init() 
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

в

function init()
{
     //init stuff

     //awesome code

     //start rendering
     drawLoop();
}

function drawLoop()
{
   //do work
   draw();

   //queue more work
   setTimeout(drawLoop, 50);
}

Это должно немного помочь, потому что:

  • draw() не будет снова вызван вашим циклом рендеринга до его завершения.
  • как указано в большинстве вышеперечисленных ответов, все непрерывные вызовы функций из setInterval вносят накладные расходы в браузере.
  • отладка немного проще, так как вы не прерываете непрерывный запуск setInterval

Надеюсь, это поможет!

Ответ 5

Chrome вряд ли видит какое-либо давление памяти вашей программы (1,23 МБ - это очень низкое использование памяти по сегодняшним стандартам), поэтому он, вероятно, не считает, что он должен агрессивно работать на GC. Если вы измените свою программу, чтобы использовать больше памяти, вы увидите, что сборщик мусора взлетел. попробуйте следующее:

<!html>
<html>
<head>
<title>Where goes memory?</title>
</head>
<body>

Greetings!

<script>
function init()
{
    var ref = window.setInterval(function() { draw(); }, 50);
}

function draw()
{
    var ar = new Array();
    for (var i = 0; i < 1e6; ++i) {
        ar.push(Math.rand());
    }
    return true
}

init();
</script>

</body>
</html>

Когда я запускаю это, я получаю шаблон использования памяти пильного диска, пик ниже 13.5 МБ (опять же, довольно маленький по сегодняшним меркам).

PS: Особенности моих браузеров:

Google Chrome   23.0.1271.101 (Official Build 172594)
OS  Mac OS X
WebKit  537.11 (@136278)
JavaScript  V8 3.13.7.5
Flash   11.5.31.5
User Agent  Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11

Ответ 6

Попробуйте сделать это без анонимной функции. Например:

function draw()
{
    return true;
}

function init()
{
    var ref = window.setInterval(draw, 50);
}

Все еще ведет себя так же?

Ответ 7

Кажется, что утечка памяти отсутствует. Пока использование памяти снова уменьшается после GC, а общее использование памяти в среднем не растет вверх, утечки нет.

"Настоящий" вопрос, который я вижу здесь, заключается в том, что setInterval действительно использует память для работы, и не похоже, что он должен выделять что-либо. В действительности ему нужно выделить несколько вещей:

  • Ему нужно будет выделить некоторое пространство стека для выполнения как анонимной функции, так и процедуры draw().
  • Я не знаю, нужно ли выделять временные данные для выполнения самих вызовов (возможно, нет)
  • Необходимо выделить небольшой объем хранилища для хранения этого true возвращаемого значения из draw().
  • Внутри, setInterval может выделить дополнительную память для перераспределения повторного события (я не знаю, как он работает внутри, он может повторно использовать существующую запись).
  • JIT может попытаться проследить этот метод, который будет выделять дополнительное хранилище для трассировки и некоторых показателей. VM может определить, что этот метод слишком мал, чтобы отслеживать его, я не знаю точно, что все пороги предназначены для включения или отключения трассировки. Если вы запустите этот код достаточно долго, чтобы виртуальная машина идентифицировала его как "горячую", он может выделить еще больше памяти для хранения JIT-скомпилированного машинного кода (после чего я ожидаю, что среднее использование памяти уменьшится, поскольку сгенерированный машинный код должен в большинстве случаев выделяют меньше памяти)

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

Ответ 8

У меня тоже такая же проблема. Клиент сообщил мне, что память о его компьютере растет все больше и больше. Сначала мне показалось странным, что веб-приложение может сделать это, даже если к нему обратился простой браузер. Я заметил, что это произошло только в Chrome.

Тем не менее, я начал с партнера для исследования, и через инструменты разработчика Chrome и задачи менеджера мы увидели увеличение памяти, о котором сообщил мне клиент.

Затем мы видим, что функция jquery (кадр анимации запроса) загружалась снова и снова, увеличивая системную память. После этого мы увидели благодарность этому сообщению, обратный отсчет jquery делал это, потому что он имеет внутри "SETINTERVAL", который каждый раз обновлял дату в макете приложения.

Когда я работаю с ASP.NET MVC, я просто убираю этот обратный отсчет jQuery script из BundleConfig и из моего макета, заменяя свой обратный отсчет времени на следующий код:

@(DateTime.Now.ToString("dd/MM/yyyy HH:mm"))