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

How synchronous AJAX call could cause memory leak?

本文关键字:内存 泄漏 何导致 AJAX 调用 同步      更新时间:2023-09-26

我理解这个反对使用同步ajax调用的一般建议,因为同步调用阻塞了UI呈现。

通常给出的另一个原因是同步 AJAX的内存泄漏问题。

从MDN文档-

注意:你不应该使用同步xmlhttprequest,因为,由于网络固有的异步性质,有多种使用同步请求时内存和事件可能泄漏的方式。的唯一的例外是同步请求在worker中工作得很好。

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

我在找一个实际的例子。任何关于这个话题的文献都是很棒的。

如果每个规范都正确实现了XHR,那么它就不会泄漏:

如果XMLHttpRequest对象的状态为,则不能被垃圾收集已打开并且设置了send()标志,则其状态为HEADERS_RECEIVED或它的状态是LOADING,并且下列条件之一为真:

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

上传完成标志被取消设置,并且关联的XMLHttpRequestUpload对象注册了一个或多个事件侦听器类型为progress、abort、error、load、timeout、loadend。

如果XMLHttpRequest对象在其连接期间被垃圾收集仍然打开,则用户代理必须取消任何fetch实例该对象打开的算法,丢弃为它们排队的任何任务,并丢弃从网络接收到的任何进一步的数据。

因此,在您点击.send()之后,XHR对象(以及它引用的任何东西)对GC免疫。但是,任何错误或成功都将使XHR进入DONE状态,并再次受到GC的约束。XHR对象是同步还是异步都无关紧要。在长同步请求的情况下,这并不重要,因为你只会被卡在send语句上,直到服务器响应。

然而,根据这张幻灯片,至少在2012年Chrome/Chromium中没有正确实现。根据规范,不需要调用.abort(),因为DONE状态意味着XHR对象应该已经正常GCd了。

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

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

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

这是因为,如果发起请求的对象在请求返回时不存在,则返回不能完成,但(如果浏览器不完善)保留在内存中。你可以很容易地使用setTimeout来删除请求对象,在请求发出之后,但在它返回之前。

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

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

例如:

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

变量a和b在这里被阻塞(以及整个堆栈)。因此,如果GC开始工作,那么同步XHR阻塞了堆栈,所有堆栈变量将被标记为"幸存的GC",并从早期堆移到更持久的堆。一些对象即使在一次垃圾收集中也不应该存活,但它们会在多次垃圾收集中存活,甚至来自这些对象的引用也会在垃圾收集中存活。

About声明堆栈阻塞GC,并且该对象被标记为长活对象:请参阅努力回到精确。此外,在之后的"标记"对象GCed 通常堆被GCed,并且通常仅在仍然需要释放更多内存时才会(因为收集标记和清除的对象需要更多时间)。

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

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

使用同步AJAX请求的内存泄漏通常由以下原因引起:

  • 使用setInterval/settimeout导致循环调用。
  • 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");
  }
});