一个简单的测试用例表明,如果一个变量被捕获在被传递的函数的闭包中,作为稍后执行的参数,似乎没有资格进行垃圾收集,即浏览器似乎仍然存在对该函数的引用,至少关闭变量.
我们的测试用例只执行一次setInterval函数,然后清除间隔定时器,即一段时间后没有任何代码正在运行,并且没有任何变量可以被访问(我可以看到,在这段代码中没有引入全局变量,除了方法运行在onload),但是这个过程占用了半GB的内存(取决于迭代次数).
有趣的是,如果我们使用setTimeout方法(而且IE9中的问题似乎不存在,Chrome,FF的当前版本)也不会发生这种情况.
this fiddle可以看出问题.
在Windows 8的IE10新鲜实例中运行它,并打开任务管理器来监视内存使用情况.它将快速增长到350兆字节,并将在脚本执行后保持在那里.
这是有问题的代码片段的重要部分:
// the function that when called multiple times will cause the leak in IE10 var eatMemory = function() { var a = null; // the captured closure variable var intervalId = setInterval(function() { a = createBigArray(); // call a method that allocates a lot of memory clearInterval(intervalId); // stop the interval timer },100); }
(我知道很容易修复这个特定的代码段,但这不是重点 – 这只是我们提出的最小的代码,它重现了这个问题,真正的代码实际上是在封闭和对象中捕捉到的从来没有收集垃圾.)
在我们的代码中是否有错误,或者有没有办法使用setInterval,其中一个闭包变量保存对大对象的引用,而不会触发内存泄漏,而不会恢复为“递归”setTimeout调用?
(我也是posted the question on MSDN)
更新:Windows 7中的IE10中也存在此问题,但如果切换到IE9标准模式,则不存在.我提交给MS Connect并报告进度.
更新:Microsoft accepted the issue并报告它在IE11(预览版)中修复 – 我还没有确认这个,但(任何人?)
更新:IE 11已正式发布,我无法使用我的系统(Win 8.1 Pro 64bit)再现该版本的问题.
解决方法
正如我已经写的(和评论者建议),这可以通过回退到setTimeout来解决(不固定).这不是微不足道的,因为需要完成一些id记录.这是我建议的修复,你可以test and fork from this fiddle:
var registerSetIntervalFix = function(){ var _setTimeout = window.setTimeout; var _clearTimeout = window.clearTimeout; window.setInterval = function(fn,interval){ var recurse = function(){ var newId = _setTimeout(recurse,interval); window.setInterval.mapping[returnValue] = newId; fn(); } var id = _setTimeout(recurse,interval); var returnValue = id; while (window.setInterval.mapping[returnValue]){ returnValue++; } window.setInterval.mapping[returnValue] = id; return returnValue; } window.setInterval.mapping = {}; window.clearInterval = function(id){ var realId = window.setInterval.mapping[id]; _clearTimeout(realId); delete window.setInterval.mapping[id]; } }
这个想法是递归地调用setTimeout来模拟循环的setInterval调用.在这个实现中有一点开销,因为它必须执行更改ID的记帐,所以我不建议应用此修复,除非需要.
不幸的是,我不能想出一个“特征”检测算法(更像是一个“bug”检测算法),所以我想你必须恢复到旧的浏览器检测.此外,我的实现不能将字符串作为第一个参数来处理,也不会将额外的参数传递给内部函数.最后两次称这个方法是不安全的,所以使用它自己的风险(并随意改进它)!
(注意:对于我们的库,我们将从现在开始停止使用setInterval,而是重写依赖它的代码中的几个部分直接使用setTimeout.)