【问题标题】:How do I create a memory leak in JavaScript?如何在 JavaScript 中创建内存泄漏?
【发布时间】:2013-04-21 19:28:12
【问题描述】:

我想了解哪种代码会导致 JavaScript 中的内存泄漏,并创建了下面的脚本。但是,当我在 OS X 上的 Safari 6.0.4 中运行脚本时,活动监视器中显示的内存消耗并没有真正增加。

我的脚本有问题还是现代浏览器不再有问题?

<html>
<body>
</body>
<script>
var i, el;

function attachAlert(element) {
    element.onclick = function() { alert(element.innerHTML); };
}

for (i = 0; i < 1000000; i++) {
    el = document.createElement('div');
    el.innerHTML = i;
    attachAlert(el);
}
</script>
</html>

该脚本基于 Google JavaScript 样式指南的 Closure 部分: http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml?showone=Closures#Closures

编辑:导致上述代码泄漏的错误显然已得到修复:http://jibbering.com/faq/notes/closures/#clMem

但我的问题仍然存在:是否有人能够提供一个现实的 JavaScript 代码示例,该示例会在现代浏览器中泄漏内存?

互联网上有很多文章表明内存泄漏可能是复杂的单页应用程序的一个问题,但我很难找到可以在浏览器中运行的示例。

【问题讨论】:

标签: javascript memory-leaks garbage-collection closures


【解决方案1】:

您没有保留在任何地方创建和引用的元素 - 这就是您没有看到内存使用量增加的原因。尝试将元素附加到 DOM,或将其存储在对象中,或将 onclick 设置为不同的元素。然后你会看到内存使用量猛增。垃圾收集器将通过并清理任何不再被引用的东西。

基本上是您的代码演练:

  • 创建元素 (el)
  • 创建一个引用它的新函数 元素
  • 将函数设置为该元素的 onclick
  • 用新元素覆盖元素

一切都以现有元素为中心。一旦无法访问元素,就无法再访问 onclick。因此,由于无法访问 onclick,因此创建的函数被销毁.. 并且该函数具有对元素的唯一引用.. 所以元素也被清理了。

有人可能有一个更技术性的例子,但这是我理解 javascript 垃圾收集器的基础。

编辑:这是脚本泄漏版本的多种可能性之一:

<html>
<body>
</body>
<script>
var i, el;

var createdElements = {};
var events = [];

function attachAlert(element) {
    element.onclick = function() { alert(element.innerHTML); };
}

function reallyBadAttachAlert(element) {
    return function() { alert(element.innerHTML); };
}

for (i = 0; i < 1000000; i++) {
    el = document.createElement('div');
    el.innerHTML = i;

    /** posibility one: you're storing the element somewhere **/
    attachAlert(el);
    createdElements['div' + i] = el; 

    /** posibility two: you're storing the callbacks somewhere **/
    event = reallyBadAttachAlert(el);
    events.push(event);
    el.onclick = event;

}
</script>
</html>

因此,对于#1,您只是在某处存储对该元素的引用。你永远不会使用它并不重要 - 因为该引用是在对象中进行的,元素及其回调永远不会消失(或者至少在你从对象中删除元素之前)。对于可能性 #2,您可以将事件存储在某处。因为事件可以被访问(即通过执行events[10]();),即使该元素无处可寻,它仍然被事件引用。所以该元素将与事件一样保留在内存中,直到它从数组。

【讨论】:

  • 感谢您的回复!您能否发布我的脚本的修改版本以进行说明?
  • 完成。希望这会有所帮助。
  • 谢谢,但我不认为这是一个相关的例子。如果我存储对创建的所有元素的引用,我不希望它们被垃圾收集。我的代码的重点是闭包创建循环引用。循环引用对于垃圾收集器来说通常没有问题,但是 - 正如我现在了解到的 - 当循环引用中涉及的对象之一是 DOM 元素时,存在一个错误,即垃圾收集在某些浏览器中不起作用。
  • 这个例子不再造成内存泄漏
【解决方案2】:

更新:这是一个基于 Google I/O 演示中的缓存场景的非常简单示例:

/*
This is an example of a memory leak. A new property is added to the cache
object 10 times/second. The value of performance.memory.usedJSHeapSize
steadily increases.

Since the value of cache[key] is easy to recalculate, we might want to free
that memory if it becomes low. However, there is no way to do that...

Another method to manually clear the cache could be added, but manually
adding memory checks adds a lot of extra code and overhead. It would be
nice if we could clear the cache automatically only when memory became low.

Thus the solution presented at Google I/O!
*/

(function(w){
    var cache = {}
    function getCachedThing(key) {
        if(!(key in cache)) {
            cache[key] = key;
        }
        return cache[key];
    }

    var i = 0;
    setInterval(function() {
        getCachedThing(i++);
    }, 100);
    w.getCachedThing = getCachedThing
})(window);

因为usedJSHeapSize does not update when the page is opened from the local file system,您可能看不到内存使用量的增加。在这种情况下,我在这里为您托管了此代码:https://memory-leak.surge.sh/example-for-waterfr


This Google I/O'19 presentation 给出了真实世界内存泄漏的示例以及避免它们的策略:

  • 方法getImageCached() 返回一个对象的引用,同时缓存一个本地引用。即使这个引用超出了方法使用者的范围,所引用的内存也不能被垃圾回收,因为在getImageCached() 的实现中仍然存在一个强引用。理想情况下,如果内存太低,缓存的引用将有资格进行垃圾收集。 (确切地说不是内存泄漏,而是一种可以释放内存的情况,代价是再次运行昂贵的操作。
  • 泄漏 #1:对缓存图像的引用。通过在 getImageCached() 中使用弱引用解决。
  • 泄漏 #2:缓存中的字符串键(映射对象)。使用新的FinalizationGroup API 解决了问题。

请查看链接的 JS 代码视频,逐行解释。

更一般地说,“真正的”JS 内存泄漏是由不需要的引用(对永远不会再次使用的对象)引起的。它们通常是 JS 代码中的错误。本文讲解four common ways memory leaks are introduced in JS

  1. 意外的全局变量
  2. 忘记的计时器/回调
  3. 没有 DOM 引用
  4. 关闭

An interesting kind of JavaScript memory leak 记录了闭包如何在流行的 MeteorJS 框架中导致内存泄漏。

【讨论】:

【解决方案3】:

我试图做类似的事情,但内存不足。

const test = (array) => {
  array.push((new Array(1000000)).fill('test'));
};

const testArray = [];

for(let i = 0; i <= 1000; i++) {
  test(testArray);
}

【讨论】:

【解决方案4】:

2020 年更新:

大多数 CPU 端内存溢出不再适用于基于现代 v8 引擎的浏览器。但是,我们可以通过运行这个脚本来溢出 GPU 端的内存

// Initialize canvas and its context
window.reallyFatCanvas = document.createElement('canvas');
let context = window.reallyFatCanvas.getContext('2d');

// References new context inside context, in loop.
function leakingLoop() {
    context.canvas.width = document.body.clientWidth;
    context.canvas.height = document.body.clientHeight;
    const newContext = document.createElement('canvas').getContext('2d');
    context.context = newContext;
    context.drawImage(newContext.canvas, 0, 0);
    
    // The new context will reference another context on the next loop
    context = newContext;
}

// Use interval instead of while(true) {...}
setInterval(leakingLoop,1);

编辑:我重命名了每个变量(和常量),所以这很有意义。这是解释。

根据我的观察,画布上下文似乎与视频内存同步。因此,如果我们将一个画布对象的引用同时引用另一个画布对象等等,那么视频 RAM 会比 DRAM 填充更多,在 Microsoft Edge 和 chrome 上进行测试。

这是我第三次尝试截图:

我不知道为什么我的笔记本电脑在运行此脚本时在截屏后总是卡住几秒钟。如果您想尝试该脚本,请小心。

【讨论】:

  • 考虑到这个技术主题,也许可以考虑添加解释导致内存泄漏的原因。
  • @dantechguy 我尽力观察为什么这个脚本会导致 VRAM 泄漏,请查看我编辑的答案。谢谢!
【解决方案5】:

最简单的方法是:

while(true){}

【讨论】:

  • 这将创建高 CPU。
  • 但是,这不是memory leak
【解决方案6】:

导致 1MB 内存泄漏的代码小示例:

Object.defineProperty(globalThis, Symbol(), {value: new Uint8Array(1&lt;&lt;20).slice(), writable: false, configurable: false})

运行该代码后,释放泄漏内存的唯一方法是关闭运行它的选项卡。

【讨论】:

    【解决方案7】:

    如果您只想创建内存泄漏,那么 IMO 最简单的方法是实例化 TypedArray,因为它们占用固定大小的内存并且比任何引用都长。例如,使用2^27 元素创建Float64Array 会消耗1GiB (1 Gibibyte) 的内存,因为它需要8 bytes per element

    启动控制台,然后写这个:

    new Float64Array(Math.pow(2, 27))
    

    【讨论】:

    猜你喜欢
    • 2011-11-06
    • 2011-09-22
    • 1970-01-01
    • 2019-09-26
    • 1970-01-01
    相关资源
    最近更新 更多