【问题标题】:Memory leak involving jQuery Ajax requests涉及 jQuery Ajax 请求的内存泄漏
【发布时间】:2010-11-30 04:02:18
【问题描述】:

我的网页在 IE8 和 Firefox 中都存在内存泄漏; Windows 进程资源管理器中显示的内存使用量随着时间的推移不断增长。

以下页面请求“unplanned.json” url,这是一个永远不会更改的静态文件(尽管我确实将我的 Cache-control HTTP 标头设置为 no-cache 以确保 Ajax 请求始终通过)。当它得到结果时,它会清除一个 HTML 表,循环它从服务器返回的 json 数组,并为数组中的每个条目动态地添加一行到 HTML 表中。然后它等待 2 秒并重复此过程。

这是整个网页:

<html> <head>
    <title>Test Page</title>
    <script type="text/javascript"
     src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
</head> <body>
<script type="text/javascript">
    function kickoff() {
        $.getJSON("unplanned.json", resetTable);
    }
    function resetTable(rows) {
        $("#content tbody").empty();
        for(var i=0; i<rows.length; i++) {
            $("<tr>"
                + "<td>" + rows[i].mpe_name + "</td>"
                + "<td>" + rows[i].bin + "</td>"
                + "<td>" + rows[i].request_time + "</td>"
                + "<td>" + rows[i].filtered_delta + "</td>"
                + "<td>" + rows[i].failed_delta + "</td>"
            + "</tr>").appendTo("#content tbody");
        }
        setTimeout(kickoff, 2000);
    }
    $(kickoff);
</script>
<table id="content" border="1" style="width:100% ; text-align:center">
<thead><tr>
    <th>MPE</th> <th>Bin</th> <th>When</th> <th>Filtered</th> <th>Failed</th>
</tr></thead>
<tbody></tbody>
</table>
</body> </html>

如果有帮助,这里是我发回的 json 示例(这是一个包含数千个条目而不仅仅是一个条目的确切数组):

[
    {
        mpe_name: "DBOSS-995",
        request_time: "09/18/2009 11:51:06",
        bin: 4,
        filtered_delta: 1,
        failed_delta: 1
    }
]

编辑:我已经接受了 Toran 非常有帮助的答案,但我觉得我应该发布一些额外的代码,因为他的 removefromdom jQuery 插件有一些限制:

  • 它只删除单个元素。所以你不能给它一个像`$("#content tbody tr")`这样的查询并期望它删除你指定的所有元素。
  • 使用它删除的任何元素都必须具有 `id` 属性。所以如果我想删除我的`tbody`,那么我必须为我的`tbody`标签分配一个`id`,否则会出错。
  • 它会删除元素本身及其所有后代,因此如果您只是想清空该元素,则必须在之后重新创建它(或将插件修改为清空而不是删除)。

所以这是我上面的页面修改为使用 Toran 的插件。为了简单起见,我没有应用任何一般性能建议offered by Peter。这是现在不再发生内存泄漏的页面:

<html>
<head>
    <title>Test Page</title>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
</head>
<body>
<script type="text/javascript">
<!--
    $.fn.removefromdom = function(s) {
        if (!this) return;

        var el = document.getElementById(this.attr("id"));

        if (!el) return;

        var bin = document.getElementById("IELeakGarbageBin");

        //before deleting el, recursively delete all of its children.
        while (el.childNodes.length > 0) {
            if (!bin) {
                bin = document.createElement("DIV");
                bin.id = "IELeakGarbageBin";
                document.body.appendChild(bin);
            }

            bin.appendChild(el.childNodes[el.childNodes.length - 1]);
            bin.innerHTML = "";
        }

        el.parentNode.removeChild(el);

        if (!bin) {
            bin = document.createElement("DIV");
            bin.id = "IELeakGarbageBin";
            document.body.appendChild(bin);
        }

        bin.appendChild(el);
        bin.innerHTML = "";
    };

    var resets = 0;
    function kickoff() {
        $.getJSON("unplanned.json", resetTable);
    }
    function resetTable(rows) {
        $("#content tbody").removefromdom();
        $("#content").append('<tbody id="id_field_required"></tbody>');
        for(var i=0; i<rows.length; i++) {
            $("#content tbody").append("<tr><td>" + rows[i].mpe_name + "</td>"
                + "<td>" + rows[i].bin + "</td>"
                + "<td>" + rows[i].request_time + "</td>"
                + "<td>" + rows[i].filtered_delta + "</td>"
                + "<td>" + rows[i].failed_delta + "</td></tr>");
        }
        resets++;
        $("#message").html("Content set this many times: " + resets);
        setTimeout(kickoff, 2000);
    }
    $(kickoff);
// -->
</script>
<div id="message" style="color:red"></div>
<table id="content" border="1" style="width:100% ; text-align:center">
<thead><tr>
    <th>MPE</th>
    <th>Bin</th>
    <th>When</th>
    <th>Filtered</th>
    <th>Failed</th>
</tr></thead>
<tbody id="id_field_required"></tbody>
</table>
</body>
</html>

进一步编辑:我将保持我的问题不变,但值得注意的是,此内存泄漏与 Ajax 无关。事实上,下面的代码同样会发生内存泄漏,并且可以通过 Toran 的 removefromdom jQuery 插件轻松解决:

function resetTable() {
    $("#content tbody").empty();
    for(var i=0; i<1000; i++) {
        $("#content tbody").append("<tr><td>" + "DBOSS-095" + "</td>"
            + "<td>" + 4 + "</td>"
            + "<td>" + "09/18/2009 11:51:06" + "</td>"
            + "<td>" + 1 + "</td>"
            + "<td>" + 1 + "</td></tr>");
    }
    setTimeout(resetTable, 2000);
}
$(resetTable);

【问题讨论】:

  • 愚蠢的问题,但由于它在每次提取时都附加到正文中,它如何使用相同数量的内存?
  • 它在附加到它之前清空 tbody。我希望清空的行会被垃圾收集,但如果我弄错了,肯定有人应该告诉我。
  • 在 IE6/7/8 中你不能真正删除动态创建的 DOM 元素而不做一个奇怪的变通方法来使 innerHTML 为空(设置 innerHTML = "")

标签: javascript jquery ajax memory-leaks


【解决方案1】:

我不确定为什么 firefox 对此不满意,但我可以根据经验说,在 IE6/7/8 中,您必须设置 innerHTML = "";在要从 DOM 中删除的对象上。 (如果你动态创建了这个 DOM 元素)

$("#content tbody").empty(); 可能不会释放这些动态生成的 DOM 元素。

请尝试以下类似的方法(这是我为解决问题而编写的 jQuery 插件)。

jQuery.fn.removefromdom = function(s) {
    if (!this) return;

    var bin = $("#IELeakGarbageBin");

    if (!bin.get(0)) {
        bin = $("<div id='IELeakGarbageBin'></div>");
        $("body").append(bin);
    }

    $(this).children().each(
            function() {
                bin.append(this);
                document.getElementById("IELeakGarbageBin").innerHTML = "";
            }
    );

    this.remove();

    bin.append(this);
    document.getElementById("IELeakGarbageBin").innerHTML = "";
};

你可以这样称呼它:$("#content").removefromdom();

这里唯一的问题是每次构建表时都需要重新创建表。

另外,如果这确实解决了您在 IE 中的问题,您可以在我今年早些时候遇到同样问题时写的博客 post 中阅读更多相关信息。

编辑 我将上面的插件更新为 95% 的 JavaScript 免费,因此它使用的 jQuery 比以前的版本更多。您仍然会注意到我必须使用 innerHTML,因为 jQuery 函数 html("");对 IE6/7/8 的作用不同

【讨论】:

  • 非常感谢,这解决了我的问题。我已经编辑了我的问题以指出该插件的一些限制,并发布了我现在可以工作且不存在内存泄漏的代码,以向人们展示如何将您的插件应用到我的页面。
【解决方案2】:

我不确定是否泄漏,但您的 resetTable() 函数效率很低。尝试先解决这些问题,然后看看结果如何。

  • 不要在循环中附加到 DOM。如果您必须进行 DOM 操作,则附加到文档片段,然后将该片段移动到 DOM。
  • 但无论如何,innerHTML 比 DOM 操作要快,因此请尽可能使用它。
  • 将 jQuery 集存储到局部变量中 - 无需每次都重新运行选择器。
  • 还将重复引用存储在局部变量中。
  • 在对任何类型的集合进行迭代时,也将长度存储在局部变量中。

新代码:

<html> <head>
    <title>Test Page</title>
    <script type="text/javascript"
     src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
</head> <body>
<script type="text/javascript">
$(function()
{
    var $tbody = $("#content tbody");

    function kickoff() {
        $.getJSON("test.php", resetTable);
    }

    function resetTable(rows)
    {
        var html = ''
          , i = 0
          , l = rows.length
          , row;
        for ( ; i < l; i++ )
        {
            row = rows[i];
            html += "<tr>"
                + "<td>" + row.mpe_name + "</td>"
                + "<td>" + row.bin + "</td>"
                + "<td>" + row.request_time + "</td>"
                + "<td>" + row.filtered_delta + "</td>"
                + "<td>" + row.failed_delta + "</td>"
            + "</tr>";
        }
        $tbody.html( html );
        setTimeout(kickoff, 2000);
    }

    kickoff();
});
</script>
<table id="content" border="1" style="width:100% ; text-align:center">
<thead>
    <th>MPE</th> <th>Bin</th> <th>When</th> <th>Filtered</th> <th>Failed</th>
</thead>
<tbody></tbody>
</table>
</body> </html>

参考资料:

【讨论】:

  • 这些是一些很棒的建议,但不幸的是,在运行您的代码时我仍然看到内存泄漏。除了我将“test.php”替换为“unplanned.json”之外,一切都完全相同——我假设你制作了一个 test.php 页面来自己测试它,这真的很棒而且很有帮助。所以感谢您的意见,我将来肯定会使用其中一些技术,但是如果您对可能导致这种内存使用的原因有任何其他想法,那么我很乐意听到他们的意见。
  • 我做到了。我会再考虑一下这个问题,看看我能不能想出什么办法。您返回到 AJAX 的数据大小是多少?
【解决方案3】:

如果我在这里错了,请纠正我,但 SetTimeout(fn) 不会阻止调用者的内存空间释放吗?这样在 resetTable(rows) 方法期间分配的所有变量/内存都将保持分配状态,直到循环完成?

如果是这种情况,将字符串构造和 appendTo 逻辑推送到不同的方法可能会更好一些,因为这些对象在每次调用后都会被释放,并且只有返回值的内存(在这种情况下是字符串标记或如果新方法执行了 appendTo()) 将不会保留在内存中。

本质上:

初始调用启动

-> 调用 resetTable()

-> -> SetTimeout 再次调用启动

-> -> -> 再次调用resetTable()

-> -> -> -> 继续直到无限

如果代码永远无法真正解析,树会继续增长。

基于此释放一些内存的另一种方法类似于以下代码:

function resetTable(rows) {
    appendRows(rows);
    setTimeout(kickoff, 2000);
}
function appendRows(rows)
{
    var rowMarkup = '';
    var length = rows.length
    var row;

    for (i = 0; i < length; i++)
    {
        row = rows[i];
        rowMarkup += "<tr>"
                + "<td>" + row.mpe_name + "</td>"
                + "<td>" + row.bin + "</td>"
                + "<td>" + row.request_time + "</td>"
                + "<td>" + row.filtered_delta + "</td>"
                + "<td>" + row.failed_delta + "</td>"
                + "</tr>";      
    }

    $("#content tbody").html(rowMarkup);
}

这会将标记附加到您的 tbody 中,然后完成堆栈的那部分。我很确定“行”的每次迭代仍将保留在内存中。但是,标记字符串等最终应该会释放出来。

再次...自从我在这个低级别查看 SetTimeout 已经有一段时间了,所以我在这里可能完全错了。无论如何,这不会消除泄漏,只会降低增长率。这取决于正在使用的 JavaScript 引擎的垃圾收集器如何处理 SetTimeout 循环,就像您在这里一样。

【讨论】:

  • 感谢您的建议 - 感谢 Toran 上面的建议,我的页面可以正常工作,但在未来的某个时候,我会检查您的想法,看看它们是否也有效。如果他们这样做,我可能会接受你的建议,因为它比 Toran 编写的 jQuery 插件简单得多,尽管我猜他的方法可能是让我的页面不泄漏内存的唯一真正方法。
猜你喜欢
  • 2012-04-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多