Js内存泄漏的常见问题及其解决方案[4]

  • 作者:不详
  • 来源:56gee
  • 更新时间:2016-08-19 10:59:47
  • 点击:2670
[0分]

Figure 3. DOM插入顺序泄漏模型

接下来,我们将给出一个躲避了大多数泄漏检测算法的泄漏示例。因为我们实际上没有泄漏任何可见的元素,并且由于被泄漏的对象太小从而你可能根本不会注意这个问题。为了使我们的示例产生泄漏,在动态创建的元素结构中将不得不内联的包含一个脚本函数指针。在我们设置好这些元素间的相互隶属关系后这将会使我们泄漏内部临时脚本对象。由于这个泄漏很小,我们不得不将示例执行成千上万次。事实上,一个对象的泄漏只有很少的字节。在运行示例并将浏览器导航到一个空白页面,你将会看到两个版本代码在内存使用上的区别。当我们使用第一种方法,将子元素加入其父元素再将构成的子树加入页面DOM,我们的内存使用量会有微小的上升。这就是一个交叉导航泄漏,只有当我们重新启动IE进程这些泄漏的内存才会被释放。如果你使用第二种方法将父元素加入页面DOM再将子元素加入其父元素中,同样运行若干次后,你的内存使用量将不会再上升,这时你会发现你已经修复了交叉导航泄漏的问题。
<html>
<head>
<script language="JScript">

function LeakMemory()
{
var hostElement = document.getElementById("hostElement");

// Do it a lot, look at Task Manager for memory response

for(i = 0; i < 5000; i++)
{
var parentDiv =
document.createElement("<div onClick='foo()'>");
var childDiv =
document.createElement("<div onClick='foo()'>");

// This will leak a temporary object
parentDiv.appendChild(childDiv);
hostElement.appendChild(parentDiv);
hostElement.removeChild(parentDiv);
parentDiv.removeChild(childDiv);
parentDiv = null;
childDiv = null;
}
hostElement = null;
}


function CleanMemory()
{
var hostElement = document.getElementById("hostElement");

// Do it a lot, look at Task Manager for memory response

for(i = 0; i < 5000; i++)
{
var parentDiv =
document.createElement("<div onClick='foo()'>");
var childDiv =
document.createElement("<div onClick='foo()'>");

// Changing the order is important, this won't leak
hostElement.appendChild(parentDiv);
parentDiv.appendChild(childDiv);
hostElement.removeChild(parentDiv);
parentDiv.removeChild(childDiv);
parentDiv = null;
childDiv = null;
}
hostElement = null;
}
</script>
</head>

<body>
<button onclick="LeakMemory()">Memory Leaking Insert</button>
<button onclick="CleanMemory()">Clean Insert</button>
<div id="hostElement"></div>
</body>
</html>

这类泄漏应该被澄清,因为这个解决方法有悖于我们在IE中的一些有益经验。创建带有脚本对象的DOM元素,以及它们已进行的相互关联是了解这个泄漏的关键点。这实际上这对于泄漏来说是至关重要的,因为如果我们创建的DOM元素不包含任何的脚本对象,同时使用相同的方式将它们进行关联,我们是不会有任何泄漏问题的。示例中给出的第二种技巧对于关联大的子树结构可能更有效(由于在那个示例中我们一共只有两个元素,所以建立一个和页面DOM不相关的树结构并不会有什么效率问题)。第二个技巧是在创建元素的开始不关联任何的脚本对象,所以你可以安全的创建子树。当你把你的子树关联到页面DOM上后,再继续处理你需要的脚本事件。牢记并遵守关于循环引用和闭包函数的使用规则,你不会再在挂接事件时在你的代码中遇到不同的泄漏。

我真的要指出这个问题,因为我们可以看出不是所有的内存泄漏都是可以很容易发现的。它们可能都是些微不足道的问题,但往往需要成千上万次的执行一个更小的泄漏场景才能使问题显现出来,就像DOM元素插入顺序引起的问题那样。如果你觉得使用所谓的"最佳"经验来编程,那么你就可以高枕无忧,但是这个示例让我们看到,即使是"最佳"经验似乎也可能带来泄漏。我们这里的解决方案希望能提高这些已有的好经验,或者介绍一些新经验使我们避免泄漏发生的可能。

貌似泄漏(Pseudo-Leaks)
在大多数时候,一些APIs的实际的行为和它们预期的行为可能会导致你错误的判断内存泄漏。貌似泄漏大多数时候总是出现在同一个页面的动态脚本操作中,而在从一个页面跳转到空白页面的时候发生是非常少见的。那你怎么能象排除页面间泄漏那样来排除这个问题,并且在新任务运行中的内存使用量是否是你所期望的。我们将使用脚本文本的重写来作为一个貌似泄漏的示例。

象DOM插入顺序问题那样,这个问题也需要依赖创建临时对象来产生"泄漏"。对一个脚本元素对象内部的脚本文本一而再再而三的反复重写,慢慢地你将开始泄漏各种已关联到被覆盖内容中的脚本引擎对象。特别地,和脚本调试有关的对象被作为完全的代码对象形式保留了下来。
<html>
<head>
<script language="JScript">
function LeakMemory()
{
// Do it a lot, look at Task Manager for memory response
for(i = 0; i < 5000; i++)
{
hostElement.text = "function foo() { }";
}
}
</script>
</head>
<body>
<button onclick="LeakMemory()">Memory Leaking Insert</button>
<script id="hostElement">function foo() { }</script>
</body>
</html>

如果你运行上面的示例代码并使用任务管理器查看,当从"泄漏"页面跳转到空白页面时,你并不会注意到任何脚本泄漏。因为这种脚本泄漏完全发生在页面内部,而且当你离开该页面时被使用的内存就会回收。对于我们原本所期望的行为来说这样的情况是糟糕的。你希望当重写了脚本内容后,原来的脚本对象就应该彻底的从页面中消失。但事实上,由于被覆盖的脚本对象可能已用作事件处理函数,并且还可能有一些未被清除的引用计数。正如你所看到的,这就是貌似泄漏。在表面上内存消耗量可能看起来非常的糟糕,但是这个原因是完全可以接受的。

总结
每一位Web开发员可能都整理有一份自己的代码示例列表,当他们在代码中看到如列表中的代码时,他们会意识到泄漏的存在并会使用一些开发技巧来避免这些问题。这样的方法虽然简单便捷,但这也是今天Web页面内存泄漏普遍存在的原因。考虑我们所讨论的泄漏情景而不是关注独立的代码示例,你将会使用更加有效的策略来解决泄漏问题。这样的观念将使你在设计阶段就把问题估计到,并且确保你有计划来处理潜在的泄漏问题。使用编写加固代码(译者注:就是异常处理或清理对象等的代码)的习惯并且采取清理所有自己占用内存的方法。虽然对这个问题来说可能太夸张了,你也可能几乎从没有见到编写脚本却需要自己清理自己占用的内存的情况;使这个问题变得越来越显著的是,脚本变量和expando属性间存在的潜在泄漏可能。

如果对模式和设计感兴趣,我强烈推荐Scott的这篇blog,因为其中演示了一个通用的移除基于闭包泄漏的示例代码。当然这需要我们使用更多的代码,但是这个实践是有效的,并且改进的场景非常容易在代码中定位并进行调试。类似的注入设计也可以用在基于expando属性引起的循环引用中,不过需要注意所注册的方法自身不要让泄漏(特别使用闭包的地方)跑掉。

收藏内容
评分

特别说明:本站所有资源仅供学习与参考,请勿用于商业用途。若转载请注明来自56gee.com中的“来源”。

栏目精华