DOM:为啥这是内存泄漏?

Posted

技术标签:

【中文标题】DOM:为啥这是内存泄漏?【英文标题】:DOM: why is this a memory leak?DOM:为什么这是内存泄漏? 【发布时间】:2013-03-23 13:39:47 【问题描述】:

考虑一下the Mozilla Docs on javascript memory leaks的这句话:

function addHandler() 
    var el = document.getElementById('el');
    el.onclick = function() 
        this.style.backgroundColor = 'red';
    

上面的代码设置元素被点击时变成红色。它 还会造成内存泄漏。为什么?因为对 el 的引用是 无意中陷入为匿名内部创建的闭包中 功能。这会在 JavaScript 之间创建循环引用 对象(函数)和原生对象(el)。

请用简单明了的方式解释上述泄漏的原因,我没有得到确切的意思。

网站/页面是否因泄漏而面临安全问题?我该如何避免它们?还有哪些其他代码会导致内存泄漏?如何判断何时发生内存泄漏?

我是内存泄漏主题的绝对初学者。有人可以一步一步地为我澄清这些东西吗?还有人可以帮我澄清这个陈述“这会在 JavaScript 对象(函数)和本机对象(el)之间创建循环引用。”

【问题讨论】:

javascriptkit.com/javatutors/closuresleak, google.com/search?q=explanation+of+javascript+memory+leaks @GrantKiely 来自 MDN @Maizere:你得到报价的地方(MDN,我现在在问题的正文中链接到它)很好地解释了这种特殊的内存泄漏。是否有您不了解的具体内容,或者您​​是否希望深入了解闭包? 顺便说一下,我想强调这是一个Microsoft 独有的错误。通过在您的代码中放置此类循环引用,您会进一步降低 IE 体验,从而鼓励用户切换到更好、更安全的浏览器,因此请抓住一切机会这样做。 @CrescentFresh 我没有收到此语句“因为对 el 的引用无意中被捕获在为匿名内部函数创建的闭包中。这会在 JavaScript 对象(函数)和本机对象(el)。”英语不太好,所以需要一个简单的解释 【参考方案1】:

有两个概念可以帮助你理解这个例子。

1) 闭包

闭包的定义是Every inner function enjoys access to its parent's function variables and parameters.

addHandler()函数结束时,匿名函数仍然可以访问父变量el

2) 功能 = 记忆

每次定义function 时,都会创建一个新对象。 让这个例子有点混乱的是 onclick 是一个只能被设置为 DOM 元素一次的事件。

所以el.onclick = function(); 肯定会覆盖旧函数,对吧?

错了!每次 addHandler 运行时,都会创建一个新的函数对象。

结论:

函数每次运行时都会创建一个新对象,并带有一个包含el 的闭包。由于匿名函数保持对el 的访问,垃圾收集器无法将其从内存中删除。

anon函数会维护对el的访问,el对函数有访问权,即循环引用,导致IE内存泄漏。

【讨论】:

“在这种情况下,this 指的是 el。” ...这是真的,但这个特定的事实与闭包没有任何关系。这是事件处理程序的一个属性,无论它们是否是闭包。 el 可在内部函数中访问这一事实很重要。 @FelixKling 谢谢!我从我的答案中删除了那部分。 只是出于好奇,如果元素从 DOM 中移除,函数是否会超出范围,或者由于循环引用而仍然潜伏在周围? 您的第二点对我来说似乎不正确。在 Firefox 65 中,onclick 方法简写绝对会覆盖之前的值,因此之前的订阅本质上会被破坏。【参考方案2】:

JavaScript 的内存管理通常是这样工作的:“只要可以到达,就保留它”。这基本上是任何垃圾回收驱动的内存模型背后的范例。

垃圾收集器往往非常擅长他们的工作,他们甚至可以检测某组元素是否只能在这组元素中访问。这些组也称为循环引用,因为如果您遵循引用,您最终会到达您已经访问过的元素:您已经运行了一个圆圈。

但是,在您的示例中,您实际上有来自两个不同“世界”的两个对象:

Internet Explorer 为此使用自己的垃圾收集方案,与 JavaScript 使用的机制分开。正是两者之间的交互会导致内存泄漏。

这正是发生的事情,可能导致内存泄漏。

【讨论】:

【参考方案3】:

每当您在 JavaScript 中定义一个函数时,都会为它创建一个 execution context;这个执行上下文包含对作用域链中所有变量的引用,从全局作用域一直到局部作用域:

function test()

    var el = document.getElementById('el');
    el.onclick = function() 
        // execution context of this function: el, test
        alert('hello world');
    

test() 完成时,匿名函数还没有被回收,因为它现在被分配给DOM的一个元素;即它被 DOM 元素的属性引用

同时,DOM 元素本身也是函数执行上下文的一部分,由于循环引用,现在无法回收,尽管它实际上并不能立即被使用;你可以在 this answer 找到一个演示。

也就是说,如今,大多数 JavaScript 引擎(甚至是 IE 中的引擎)都使用更高级的 garbage collector,它可以更好地识别未使用的变量,使用诸如 mark-and-sweep 或分代/临时垃圾收集之类的技术。

为了确保您不会在任何浏览器上遇到问题(不过,由于页面的典型生命周期,这主要是理论上的):

document.getElementById('el').onclick = function() 
    alert('hello world');

【讨论】:

无法得到这个“但 el 也没有被回收,因为它是该函数执行上下文的一部分。” @Maizere 执行上下文包含(或者更确切地说,引用)el,因此由于引用计数而无法回收。 是的,相反,document.getElementById('el').onclick = function() ... 不会导致内存泄漏。 啊,我需要休息一下。谢谢你的帖子 @Maizere 顺便说一句,垃圾收集器的最新进展使这个问题过时了。刚刚注意到您接受/不接受,所以我决定将这部分添加到我的答案中:)【参考方案4】:

另请参阅有关该问题的 MS 文章的 more information 部分:

发生这种内存泄漏是因为 DOM 对象是非 JScript 对象。 DOM 对象不在标记和清除垃圾收集方案中 脚本。因此,DOM 对象之间的循环引用和 在浏览器完全浏览之前,JScript 处理程序不会被破坏 撕掉页面。

但请注意,与那篇文章中所述的相反(当浏览器转到新页面时,内存将被回收),this article 确认 IE 6 中的错误导致内存永远泄漏。

【讨论】:

以上是关于DOM:为啥这是内存泄漏?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 swift 中创建字符串时会出现内存泄漏?

为啥保留循环泄漏内存?

为啥这段代码不会导致内存泄漏? [复制]

为啥这个 INotifyCollectionChanged 会导致内存泄漏?

为啥 OpenCV Mat 会造成内存泄漏?

为啥迁移到 ARC 后我的应用程序充满了内存泄漏?