34

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

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

MDN文档 -

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

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

我正在寻找一个实际的例子。任何指向有关该主题的任何文献的指针都会很棒。

4

5 回答 5

17

如果 XHR按照规范正确实现,那么它不会泄漏:

如果一个 XMLHttpRequest 对象的状态是 OPENED 并且设置了 send() 标志,它的状态是 HEADERS_RECEIVED,或者它的状态是 LOADING,并且以下情况之一为真,则不得对 XMLHttpRequest 对象进行垃圾回收:

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

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

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

因此,在您点击.send()XHR 对象(以及它引用的任何东西)之后,就会对 GC 免疫。但是,任何错误或成功都会使 XHR 进入 DONE 状态,并再次成为 GC 的对象。XHR 对象是同步还是异步都没有关系。如果再次出现长同步请求,这并不重要,因为您只会停留在发送语句上,直到服务器响应。

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

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

于 2013-08-17T14:51:42.677 回答
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 不存在,但现在的问题是当抛出错误时垃圾收集器是如何工作的。它是否清除了与错误相关的所有对象,是否保留了错误对象或类似的东西。如果有人对此有更多了解并在这里写信,我会很高兴。

于 2013-08-17T12:31:18.333 回答
3

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

例如:

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中的 Conservative Garbage Collection 部分。此外,在通常的堆被 GC之后,“标记”的对象被 GC,并且通常只有在仍然需要释放更多内存时(因为收集标记和清除的对象需要更多时间)。

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

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

如果同步调用在完成之前被中断(即由用户事件重新使用 XMLHttpRequest 对象),则未完成的网络查询可能会挂起,无法被垃圾收集。

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

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

于 2013-08-22T13:11:33.507 回答
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");
  }
});
于 2013-08-16T16:34:30.637 回答