同步AJAX调用如何导致内存泄漏?

时间:2013-01-16 18:15:14

标签: javascript ajax jquery

我理解this针对使用同步ajax调用的一般建议,因为同步调用会阻止UI呈现。

通常给出的另一个原因是使用同步 AJAX进行内存泄漏。

来自MDN文档 -

  

注意:您不应该使用同步XMLHttpRequests,因为   网络固有的异步性,有各种各样的   使用同步请求时内存和事件可能泄漏的方式。该   唯一的例外是同步请求在Workers内部运行良好。

同步调用如何导致内存泄漏?

我正在寻找一个实际的例子。 任何关于这个主题的文献都会很棒。

5 个答案:

答案 0 :(得分:13)

如果XHR正确实施per spec,那么它不会泄漏:

  

如果状态为XMLHttpRequest对象,则不得对其进行垃圾回收   OPENED和send()标志置位,其状态为HEADERS_RECEIVED,或   它的状态是LOADING,其中一个是真的:

     

它有一个或多个已注册类型为的事件侦听器   readystatechange,progress,abort,error,load,timeout或loadend。

     

上传完成标志未设置且已关联   XMLHttpRequestUpload对象已注册一个或多个事件侦听器   其类型是进度,中止,错误,加载,超时或loadend。

     

如果XMLHttpRequest对象在其连接时被垃圾收集   仍处于打开状态,用户代理必须取消任何获取实例   该对象打开的算法,丢弃任何排队的任务,   并丢弃从网络收到的任何其他数据。

因此,在您点击.send()后,XHR对象(及其引用的任何内容)对GC免疫。但是,any error or success will put the XHR into DONE state并再次受GC限制。如果XHR对象是同步或异步则完全没关系。如果再次发出长同步请求,则无关紧要,因为在服务器响应之前,您只会被卡在发送语句中。

但是,根据this slide,至少在2012年Chrome / Chromium中没有正确实现。根据规范,没有必要调用.abort(),因为DONE状态意味着XHR对象应该已经是GCd。

我找不到任何支持MDN声明的证据,我通过Twitter联系了作者。

答案 1 :(得分:3)

我认为内存泄漏的发生主要是因为垃圾收集器无法完成其工作。即你有一些东西的引用,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不存在,但现在的问题是当抛出错误时垃圾收集器如何工作。它是否清除了与错误相关的所有对象,是保留错误对象还是类似的东西。如果有人知道更多相关信息并写在这里,我会很高兴。

答案 2 :(得分:3)

从GC中同步XHR块线程执行和该线程的函数执行堆栈中的所有对象。

E.g:

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

变量a和b在这里被阻止(也是整个堆栈)。 因此,如果GC开始工作,那么同步XHR已阻止堆栈,所有堆栈变量将被标记为“幸存的GC”并从早期堆移动到更持久的堆。并且即使单个GC也不能存活的对象的音调将存在许多垃圾收集,甚至来自这些对象的引用也将在GC中存活。

关于声明堆栈块GC,以及标记为长期居住对象的对象:请参阅中的保守垃圾收集部分 Clawing Our Way Back To Precision。 此外,“标记”对象GCed 通常的堆GC之后,通常只如果仍然需要释放更多内存(因为收集标记和清除的objs需要更多时间)。

更新: 它真的是泄漏,而不仅仅是早期堆无效的解决方案吗? 有几件事需要考虑。

  • 请求完成后这些对象将被锁定多长时间?
  • 同步XHR可以无限制地阻止堆栈,XHR没有超时属性(在所有非IE浏览器中),网络问题并不罕见。
  • 锁定了多少UI元素?如果它阻止20M的内存仅持续1秒= = 200k领先2分钟。考虑许多背景标签。
  • 考虑单个同步阻止资源和浏览器音调的情况 转到交换文件
  • 当另一个事件试图改变DOM时,可能会被同步XHR阻止,另一个线程被阻止(整个它也是堆栈)
  • 如果用户重复导致同步XHR的操作,整个浏览器窗口将被锁定。浏览器使用max = 2线程来处理窗口事件。
  • 即使没有阻止,这也消耗了大量的操作系统和浏览器内部资源:线程,关键部分资源,UI资源,DOM ...想象一下,您可以打开(由于内存问题)10个标签,其中包含使用同步XHR和100的网站包含使用异步XHR的站点的选项卡。这不是内存泄漏。

答案 3 :(得分:3)

如果同步调用在完成之前被中断(即通过重用XMLHttpRequest对象的用户事件),那么未完成的网络查询可能会挂起,无法进行垃圾回收。

这是因为,如果请求返回时发起请求的对象不存在,则返回无法完成,但(如果浏览器不完整)仍保留在内存中。您可以使用setTimeout轻松地在请求发出后但在返回之前删除请求对象。

我记得我在IE中有一个很大的问题,早在2009年左右,但我希望现代浏览器不会受此影响。当然,现代库(即JQuery)可以防止可能发生的情况,允许在不必考虑的情况下发出请求。

答案 4 :(得分:0)

使用同步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");
  }
});