Как синхронный вызов AJAX может вызвать утечку памяти?

Я понимаю этот общий совет по использованию синхронных вызовов ajax, поскольку синхронные вызовы блокируют отображение пользовательского интерфейса.

Другая общая причина - утечка памяти с помощью синхронного AJAX.

Из MDN docs -

Примечание. Вы не должны использовать синхронные XMLHttpRequests, потому что из-за по сути, асинхронный характер сетей, существуют различные способы памяти и события могут протекать при использовании синхронных запросов. единственным исключением является то, что синхронные запросы хорошо работают внутри Рабочих.

Как синхронные вызовы могут вызвать утечку памяти?

Я ищу практический пример. Любые указатели на любую литературу по этой теме были бы замечательными.

Ответы

Ответ 1

Если XHR правильно реализована за спецификацию, то он не будет протекать:

Объект XMLHttpRequest не должен собираться в мусор, если его состояние OPENED и флаг send(), его состояние HEADERS_RECEIVED или его состояние равно LOADING, и одно из следующего верно:

В нем есть один или несколько зарегистрированных слушателей событий, тип которых readystatechange, progress, abort, error, load, timeout или loadend.

Флаг завершения загрузки не установлен, а связанный с ним Объект XMLHttpRequestUpload имеет один или несколько зарегистрированных прослушивателей событий тип которого - прогресс, прерывание, ошибка, загрузка, тайм-аут или загрузка.

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

Итак, после того, как вы нажмете .send(), объект XHR (и все, что он ссылается) становится невосприимчивым к GC. Тем не менее, любая ошибка или успех поставит XHR в состояние DONE, и он снова станет объектом GC. Не имеет значения, если объект XHR синхронизирован или асинхронен. В случае длинного запроса на синхронизацию это не имеет значения, потому что вы просто зациклились на инструкции send, пока сервер не ответит.

Однако, согласно этому слайду, он не был правильно реализован, по крайней мере, в Chrome/Chromium в 2012 году. По спецификации не нужно было бы звонить .abort() поскольку состояние DONE означает, что объект XHR должен быть обычно GCd.

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

Ответ 2

Я думаю, что утечки памяти происходят в основном потому, что сборщик мусора не может выполнять свою работу. То есть у вас есть ссылка на что-то, и GC не может его удалить. Я написал простой пример:

var getDataSync = function(url) {
    console.log("getDataSync");
    var request = new XMLHttpRequest();
    request.open('GET', url, false);  // `false` makes the request synchronous
    try {
        request.send(null);
        if(request.status === 200) {
            return request.responseText;
        } else {
            return "";
        }
    } catch(e) {
        console.log("!ERROR");
    }
}

var getDataAsync = function(url, callback) {
    console.log("getDataAsync");
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    xhr.onload = function (e) {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                callback(xhr.responseText);
            } else {
                callback("");
            }
        }
    };
    xhr.onerror = function (e) {
        callback("");
    };
    xhr.send(null);
}

var requestsMade = 0
var requests = 1;
var url = "http://missing-url";
for(var i=0; i<requests; i++, requestsMade++) {
    getDataSync(url);
    // getDataAsync(url);
}

Кроме того, что синхронная функция блокирует много вещей, есть еще одна большая разница. Обработка ошибок. Если вы используете getDataSync и удалите блок try-catch и обновите страницу, вы увидите, что выдается ошибка. Это потому, что url не существует, но теперь возникает вопрос, как сборщик мусора работает при ошибке. Очищает ли он все объекты, связанные с ошибкой, поддерживает ли объект ошибки или что-то в этом роде. Я буду рад, если кто-нибудь узнает об этом и напишет здесь.

Ответ 3

Выполнение синхронизации блока синхронизации XHR и всех объектов в стеке выполнения функций этого потока из GC.

например:.

function (b) { 
  var a = <big data>;
  <work with> a and b
  sync XHR
}

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

О требованиях блоков стека GC, и этот объект помечен как объекты с длительным проживанием: см. раздел Консервативная сборка мусора в Увязка нашего пути к точности. Кроме того, "отмеченные" объекты GCed после, обычная куча GCed и обычно только, если по-прежнему необходимо освободить больше памяти (поскольку сбор отмеченных и подмеченных объектов больше времени).

UPDATE: Действительно ли это утечка, а не только неэффективное решение на ранней стадии? Есть несколько вещей, которые следует учитывать.

  • Как долго этот объект будет заблокирован после завершения запроса?
  • Sync XHR может блокировать стек неограниченное время, XHR не имеет свойства тайм-аута (во всех браузерах, не относящихся к IE), сетевые проблемы не редки.
  • Сколько элементов интерфейса заблокировано? Если он блокирует 20M памяти всего за 1 секунду == 200k в 2min. Рассмотрим множество фоновых вкладок.
  • Рассмотрим случай, когда одна синхронизация блокирует тон ресурсов и браузера переходит в файл подкачки
  • Когда другое событие пытается изменить DOM внутри, может быть заблокировано синхронизацией XHR, другой поток блокируется (и весь его стек тоже)
  • Если пользователь повторит действия, которые приводят к синхронизации XHR, окно браузера целое будет заблокировано. Браузеры используют поток max = 2 для обработки событий окна.
  • Даже без блокировки это потребляет много ресурсов операционной системы и браузера: поток, ресурсы критического раздела, ресурсы пользовательского интерфейса, DOM... Представьте, что вы можете открыть (из-за проблемы с памятью) 10 вкладок с сайтами, использующими синхронизацию XHR и 100 вкладки с сайтами, использующими async XHR. Это не утечка памяти.

Ответ 4

Если синхронный вызов прерывается (т.е. пользовательским событием, повторно использующим объект XMLHttpRequest) до его завершения, тогда выдающийся сетевой запрос может оставаться висящим, неспособным собрать мусор.

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

Помню, у меня была большая проблема с этим в IE, примерно в 2009 году, но я надеюсь, что современные браузеры не восприимчивы к этому. Конечно, современные библиотеки (т.е. JQuery) предотвращают ситуации, в которых это может произойти, позволяя делать запросы, не задумываясь об этом.

Ответ 5

Утечки памяти с использованием синхронных запросов AJAX часто вызваны:

  • используя setInterval/setTimout, вызывающие циклические вызовы.
  • XmlHttpRequest - при удалении ссылки, поэтому xhr становится недоступным

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

Это может произойти из-за ошибок браузера, проблем с расширениями браузера и, что гораздо реже, наших ошибок в архитектуре кода.

Здесь пример утечки памяти возникает при запуске setInterval в новом контексте:

var
Context  = process.binding('evals').Context,
Script   = process.binding('evals').Script,
total    = 5000,
result   = null;

process.nextTick(function memory() {
  var mem = process.memoryUsage();
  console.log('rss:', Math.round(((mem.rss/1024)/1024)) + "MB");
  setTimeout(memory, 100);
});

console.log("STARTING");
process.nextTick(function run() {
  var context = new Context();

  context.setInterval = setInterval;

  Script.runInContext('setInterval(function() {}, 0);',
                      context, 'test.js');
  total--;
  if (total) {
    process.nextTick(run);
  } else {
    console.log("COMPLETE");
  }
});