金沙棋牌官方平台

当前位置:金沙棋牌 > 金沙棋牌官方平台 > JavaScript中的内存泄漏以及如何处理,JS的内存管

JavaScript中的内存泄漏以及如何处理,JS的内存管

来源:http://www.logblo.com 作者:金沙棋牌 时间:2019-11-22 07:40

总结

以上内容是对JavaScript内存管理机制的讲解,以及常见的四种内存泄漏的分析。希望对JavaScript的编程人员有所帮助。

2 赞 2 收藏 1 评论

金沙棋牌官方平台 1

注意,如果我们试图访问x[4],我们将访问与m关联的数据。这是因为我们正在访问数组中不存在的元素

它比数组中最后一个数据实际分配的元素多了4个字节x[3],并且可能最终读取(或覆盖)了一些m比特。这对其余部分会产生不利的后果。

金沙棋牌官方平台 2

当函数调用其它函数时,每个函数被调用时都会得到自己的堆栈块。它会保留所有的局部变量和一个程序计数器,还会记录执行的地方。当功能完成时,其内存块会被释放,可以再次用于其它目的。

b: null

实例:使用 Chrome 发现内存泄漏

实质上有两种类型的泄漏:周期性的内存增长导致的泄漏,以及偶现的内存泄漏。显而易见,周期性的内存泄漏很容易发现;偶现的泄漏比较棘手,一般容易被忽视,偶尔发生一次可能被认为是优化问题,周期性发生的则被认为是必须解决的 bug。

以 Chrome 文档中的代码为例:

JavaScript

var x = []; function createSomeNodes() { var div, i = 100, frag = document.createDocumentFragment(); for (;i > 0; i--) { div = document.createElement("div"); div.appendChild(document.createTextNode(i

  • " - "+ new Date().toTimeString())); frag.appendChild(div); } document.getElementById("nodes").appendChild(frag); } function grow() { x.push(new Array(1000000).join('x')); createSomeNodes(); setTimeout(grow,1000); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var x = [];
 
function createSomeNodes() {
    var div,
        i = 100,
        frag = document.createDocumentFragment();
 
    for (;i > 0; i--) {
        div = document.createElement("div");
        div.appendChild(document.createTextNode(i + " - "+ new Date().toTimeString()));
        frag.appendChild(div);
    }
 
    document.getElementById("nodes").appendChild(frag);
}
 
function grow() {
    x.push(new Array(1000000).join('x'));
    createSomeNodes();
    setTimeout(grow,1000);
}

当 grow 执行的时候,开始创建 div 节点并插入到 DOM 中,并且给全局变量分配一个巨大的数组。通过以上提到的工具可以检测到内存稳定上升。

周期引起问题

在周期方面有一个限制。例如下面的例子,创建两个对象并相互引用,这样会创建一个循环引用。在函数调用之后,它们将超出范围,所以它们实际上是无用的,可以被释放。然而,引用计数算法认为,由于两个对象中的每一个都被引用至少一次,所以两者都不能被垃圾收集机制收回。

function f() { var o1 = {}; var o2 = {}; o1.p = o2; // o1 references o2 o2.p = o1; // o2 references o1. This creates a cycle. } f( );

1
2
3
4
5
6
7
8
function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 references o2
  o2.p = o1; // o2 references o1. This creates a cycle.
}
 
f( );

金沙棋牌官方平台 3

当创建对象和字符串等时,JavaScript就会分配内存,并在不再使用时自动释放内存,这种机制被称为垃圾收集。这种释放资源看似是“自动”的,但本质是混淆的,这也给JavaScript(以及其他高级语言)的开发人员产生了可以不关心内存管理的错误印象。其实这是一个大错误。

Counter intuitive behavior of Garbage Collectors -垃圾回收器中违反直觉的行为

找出周期性增长的内存

timeline 标签擅长做这些。在 Chrome 中打开例子,打开 Dev Tools ,切换到 timeline,勾选 memory 并点击记录按钮,然后点击页面上的 The Button 按钮。过一阵停止记录看结果:

金沙棋牌官方平台 4

两种迹象显示出现了内存泄漏,图中的 Nodes(绿线)和 JS heap(蓝线)。Nodes 稳定增长,并未下降,这是个显著的信号。

JS heap 的内存占用也是稳定增长。由于垃圾收集器的影响,并不那么容易发现。图中显示内存占用忽涨忽跌,实际上每一次下跌之后,JS heap 的大小都比原先大了。换言之,尽管垃圾收集器不断的收集内存,内存还是周期性的泄漏了。

确定存在内存泄漏之后,我们找找根源所在。

JavaScript 中的内存泄漏以及如何处理

2017/11/21 · JavaScript · 1 评论 · 内存泄漏

原文出处: Alexander Zlatkov   译文出处:葡萄城控件   

随着现在的编程语言功能越来越成熟、复杂,内存管理也容易被大家忽略。本文将会讨论JavaScript中的内存泄漏以及如何处理,方便大家在使用JavaScript编码时,更好的应对内存泄漏带来的问题。

什么是内存?

在硬件层面上,计算机的内存由大量的触发器组成的。每个触发器包含一些晶体管,并能够存储一位数据。单独的触发器可以通过唯一的标识符来寻址,所以我们可以读取和覆盖它们。因此,从概念上讲,我们可以把整个计算机内存看作是我们可以读写的一大块空间。

很多东西都存储在内存中:

程序使用的所有变量和其他数据。

程序的代码,包括操作系统的代码。

编译器和操作系统一起工作,来处理大部分的内存管理,但是我们需要了解从本质上发生了什么。

编译代码时,编译器会检查原始数据类型,并提前计算它们需要多少内存,然后将所需的内存分配给调用堆栈空间中的程序。分配这些变量的空间被称为堆栈空间,随着函数的调用,内存会被添加到现有的内存之上。当终止时,空间以LIFO(后进先出)顺序被移除。例如如下声明:

intn;//4个字节intx [4];//4个元素的数组,每一个占4个字节doublem;//8个字节

编译器插入与操作系统进行交互的代码,以便在堆栈中请求所需的字节数来存储变量。

在上面的例子中,编译器知道每个变量的确切内存地址。实际上,每当我们写入这个变量n,它就会在内部翻译成“内存地址4127963”。

var a3 = a1.concat(a2);

另一个有用的特性

在 heap allocations 的结果区域,选择 Allocation。

金沙棋牌官方平台 5

这个视图呈现了内存分配相关的功能列表,我们立刻看到了 grow 和 createSomeNodes。当选择 grow 时,看看相关的 object constructor,清楚地看到 (string), HTMLDivElement 和 Text 泄漏了。

结合以上提到的工具,可以轻松找到内存泄漏。

周期不再是问题了

在上面的相互引用例子中,在函数调用返回之后,两个对象不再被全局对象可访问的对象引用。因此,它们将被垃圾收集器发现,从而进行收回。

金沙棋牌官方平台 6

即使在对象之间有引用,它们也不能从root目录中访问,从而会被认为是垃圾而收集。

抵制垃圾收集器的直观行为

尽管垃圾收集器使用起来很方便,但它们也有自己的一套标准,其中之一是非决定论。换句话说,垃圾收集是不可预测的。你不能真正知道什么时候进行收集,这意味着在某些情况下,程序会使用更多的内存,虽然这是实际需要的。在其它情况下,在特别敏感的应用程序中,短暂暂停是很可能出现的。尽管非确定性意味着不能确定何时进行集合,但大多数垃圾收集实现了共享在分配期间进行收集的通用模式。如果没有执行分配,大多数垃圾收集会保持空闲状态。如以下情况:

大量的分配被执行。

大多数这些元素(或所有这些元素)被标记为无法访问(假设我们将一个引用指向不再需要的缓存)。

没有进一步的分配执行。

在这种情况下,大多数垃圾收集不会做出任何的收集工作。换句话说,即使有不可用的引用需要收集,但是收集器不会进行收集。虽然这并不是严格的泄漏,但仍会导致内存使用率高于平时。

.没有后续的分配再被执行

JavaScript 内存管理

JavaScript 是一种垃圾回收语言。垃圾回收语言通过周期性地检查先前分配的内存是否可达,帮助开发者管理内存。换言之,垃圾回收语言减轻了“内存仍可用”及“内存仍可达”的问题。两者的区别是微妙而重要的:仅有开发者了解哪些内存在将来仍会使用,而不可达内存通过算法确定和标记,适时被操作系统回收。

内存生命周期

无论你使用的是什么编程语言,内存生命周期几乎都是一样的:金沙棋牌官方平台 7

 

以下是对内存生命周期中每个步骤发生的情况的概述:

  • 分配内存  – 内存由操作系统分配,允许程序使用它。在简单的编程语言中,这个过程是开发人员应该处理的一个显式操作。然而,在高级编程语言中,系统会帮助你完成这个操作。
  • 内存使用 这是程序使用之前申请内存的时间段,你的代码会通过使用分配的变量

来对内存进行读取和写入操作。

  • 释放内存  - 对于不再需要的内存进行释放的操作,以便确保其变成空闲状态并且可以被再次使用。与分配内存操作一样,这个操作在简单的编程语言中是需要显示操作的。

周期不再是问题了

在上面的相互引用例子中,在函数调用返回之后,两个对象不再被全局对象可访问的对象引用。因此,它们将被垃圾收集器发现,从而进行收回。

金沙棋牌官方平台 8

即使在对象之间有引用,它们也不能从root目录中访问,从而会被认为是垃圾而收集。

function f() {

1:意外的全局变量

JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是 window 。

JavaScript

function foo(arg) { bar = "this is a hidden global variable"; }

1
2
3
function foo(arg) {
    bar = "this is a hidden global variable";
}

真相是:

JavaScript

function foo(arg) { window.bar = "this is an explicit global variable"; }

1
2
3
function foo(arg) {
    window.bar = "this is an explicit global variable";
}

函数 foo 内部忘记使用 var ,意外创建了一个全局变量。此例泄漏了一个简单的字符串,无伤大雅,但是有更糟的情况。

另一种意外的全局变量可能由 this 创建:

JavaScript

function foo() { this.variable = "potential accidental global"; } // Foo 调用自己,this 指向了全局对象(window) // 而不是 undefined foo();

1
2
3
4
5
6
7
function foo() {
    this.variable = "potential accidental global";
}
 
// Foo 调用自己,this 指向了全局对象(window)
// 而不是 undefined
foo();

在 JavaScript 文件头部加上 ‘use strict’,可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。

全局变量注意事项

尽管我们讨论了一些意外的全局变量,但是仍有一些明确的全局变量产生的垃圾。它们被定义为不可回收(除非定义为空或重新分配)。尤其当全局变量用于临时存储和处理大量信息时,需要多加小心。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义。与全局变量相关的增加内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗导致缓存突破上限,因为缓存内容无法被回收。

什么是内存泄漏?

内存泄漏是应用程序使用过的内存片段,在不再需要时,不能返回到操作系统或可用内存池中的情况。

编程语言有各自不同的内存管理方式。但是是否使用某一段内存,实际上是一个不可判定的问题。换句话说,只有开发人员明确的知道是否需要将一块内存返回给操作系统。

在JavaScript中使用内存

基本上在JavaScript中使用分配的内存,意味着在其中读写。

这可以通过读取或写入变量或对象属性的值,或者甚至将参数传递给函数来完成。

// 'o2'作为'o1'的属性被其引用

关于作者:涂鸦码龙

金沙棋牌官方平台 9

不高级前端攻城狮,原名金龙,不姓郭。【忙时码代码,无事乱涂鸦】 个人主页 · 我的文章 · 3 ·    

金沙棋牌官方平台 10

什么是内存?

在硬件层面上,计算机的内存由大量的触发器组成的。每个触发器包含一些晶体管,并能够存储一位数据。单独的触发器可以通过唯一的标识符来寻址,所以我们可以读取和覆盖它们。因此,从概念上讲,我们可以把整个计算机内存看作是我们可以读写的一大块空间。

很多东西都存储在内存中:

  1. 程序使用的所有变量和其他数据。
  2. 程序的代码,包括操作系统的代码。

编译器和操作系统一起工作,来处理大部分的内存管理,但是我们需要了解从本质上发生了什么。

编译代码时,编译器会检查原始数据类型,并提前计算它们需要多少内存,然后将所需的内存分配给调用堆栈空间中的程序。分配这些变量的空间被称为堆栈空间,随着函数的调用,内存会被添加到现有的内存之上。当终止时,空间以LIFO(后进先出)顺序被移除。例如如下声明:

int n; // 4个字节 int x [4]; // 4个元素的数组,每一个占4个字节 double m; // 8个字节

1
2
3
int n; // 4个字节
int x [4]; // 4个元素的数组,每一个占4个字节
double m; // 8个字节

编译器插入与操作系统进行交互的代码,以便在堆栈中请求所需的字节数来存储变量。

在上面的例子中,编译器知道每个变量的确切内存地址。实际上,每当我们写入这个变量n,它就会在内部翻译成“内存地址4127963”。

注意,如果我们试图访问x[4],我们将访问与m关联的数据。这是因为我们正在访问数组中不存在的元素 – 它比数组中最后一个数据实际分配的元素多了4个字节x[3],并且可能最终读取(或覆盖)了一些m比特。这对其余部分会产生不利的后果。

金沙棋牌官方平台 11

当函数调用其它函数时,每个函数被调用时都会得到自己的堆栈块。它会保留所有的局部变量和一个程序计数器,还会记录执行的地方。当功能完成时,其内存块会被释放,可以再次用于其它目的。

随着现在的编程语言功能越来越成熟、复杂,内存管理也容易被大家忽略。本文将会讨论JavaScript中的内存泄漏以及如何处理,方便大家在使用JavaScript编码时,更好的应对内存泄漏带来的问题。

}

Chrome 内存剖析工具概览

Chrome 提供了一套很棒的检测 JavaScript 内存占用的工具。与内存相关的两个重要的工具:timeline 和 profiles。

在JavaScript中使用内存

基本上在JavaScript中使用分配的内存,意味着在其中读写。

这可以通过读取或写入变量或对象属性的值,或者甚至将参数传递给函数来完成。

周期引起问题

在周期方面有一个限制。例如下面的例子,创建两个对象并相互引用,这样会创建一个循环引用。在函数调用之后,它们将超出范围,所以它们实际上是无用的,可以被释放。然而,引用计数算法认为,由于两个对象中的每一个都被引用至少一次,所以两者都不能被垃圾收集机制收回。

function f() {varo1 ={};varo2 ={};

o1.p= o2;//o1 references o2o2.p = o1;//o2 references o1. This creates a cycle.}

f( );

金沙棋牌官方平台 12

Differences between statically and dynamically allocated memory

2:被遗忘的计时器或回调函数

在 JavaScript 中使用 setInterval 非常平常。一段常见的代码:

JavaScript

var someResource = getData(); setInterval(function() { var node = document.getElementById('Node'); if(node) { // 处理 node 和 someResource node.innerHTML = JSON.stringify(someResource)); } }, 1000);

1
2
3
4
5
6
7
8
var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

此例说明了什么:与节点或数据关联的计时器不再需要,node 对象可以删除,整个回调函数也不需要了。可是,计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,someResource 如果存储了大量的数据,也是无法被回收的。

对于观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。如今,即使没有明确移除它们,一旦观察者对象变成不可达,大部分浏览器是可以回收观察者处理函数的。

观察者代码示例:

JavaScript

var element = document.getElementById('button'); function onClick(event) { element.innerHTML = 'text'; } element.addEventListener('click', onClick);

1
2
3
4
5
6
var element = document.getElementById('button');
function onClick(event) {
    element.innerHTML = 'text';
}
 
element.addEventListener('click', onClick);

对象观察者和循环引用注意事项

老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。如今,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法,已经可以正确检测和处理循环引用了。换言之,回收节点内存时,不必非要调用 removeEventListener 了。

动态分配

如若我们不知道编译时,变量需要的内存数量时,事情就会变得复杂。假设我们想要做如下事项:

int n = readInput(); //读取用户的输入 ... //用“n”个元素创建一个数组

1
2
3
int n = readInput(); //读取用户的输入
...
//用“n”个元素创建一个数组

在编译时,编译器不知道数组需要多少内存,因为它是由用户提供的输入值决定的。

因此,它不能为堆栈上的变量分配空间。相反,我们的程序需要在运行时明确地向操作系统请求适当的空间。这个内存是从堆空间分配的。下表总结了静态和动态内存分配之间的区别:

金沙棋牌官方平台 13

总结

以上内容是对JavaScript内存管理机制的讲解,以及常见的四种内存泄漏的分析。希望对JavaScript的编程人员有所帮助。

原文链接:

转载请注明出自:葡萄城控件

};

简介

内存泄漏是每个开发者最终都要面对的问题,它是许多问题的根源:反应迟缓,崩溃,高延迟,以及其他应用问题。

四种常见的JavaScript内存泄漏

1:全局变量

JavaScript以一种有趣的方式来处理未声明的变量:当引用未声明的变量时,会在全局对象中创建一个新变量。在浏览器中,全局对象将是window,这意味着

function foo(arg) { bar = "some text"; }

1
2
3
function foo(arg) {
    bar = "some text";
}

相当于:

function foo(arg) { window.bar = "some text"; }

1
2
3
function foo(arg) {
    window.bar = "some text";
}

bar只是foo函数中引用一个变量。如果你不使用var声明,将会创建一个多余的全局变量。在上述情况下,不会造成很大的问题。但是,如若是下面的这种情况。

你也可能不小心创建一个全局变量this:

function foo() { this.var1 = "potential accidental global"; } // Foo called on its own, this points to the global object (window) // rather than being undefined. foo( );

1
2
3
4
5
6
7
function foo() {
    this.var1 = "potential accidental global";
}
 
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo( );

你可以通过在JavaScript文件的开始处添加‘use strict’;来避免这中错误,这种方式将开启严格的解析JavaScript模式,从而防止意外创建全局变量。

意外的全局变量当然是一个问题。更多的时候,你的代码会受到显式的全局变量的影响,而这些全局变量在垃圾收集器中是无法收集的。需要特别注意用于临时存储和处理大量信息的全局变量。如果必须使用全局变量来存储数据,那么确保将其分配为空值,或者在完成后重新分配。

2:被遗忘的定时器或回调

下面列举setInterval的例子,这也是经常在JavaScript中使用。

对于提供监视的库和其它接受回调的工具,通常在确保所有回调的引用在其实例无法访问时,会变成无法访问的状态。但是下面的代码却是一个例外:

var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); //This will be executed every ~5 seconds.

1
2
3
4
5
6
7
var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //This will be executed every ~5 seconds.

上面的代码片段显示了使用引用节点或不再需要的数据的定时器的结果。

该renderer对象可能会在某些时候被替换或删除,这会使interval处理程序封装的块变得冗余。如果发生这种情况,那么处理程序及其依赖项都不会被收集,因为interval需要先停止。这一切都归结为存储和处理负载数据的serverData不会被收集的原因。

当使用监视器时,你需要确保做了一个明确的调用来删除它们。

幸运的是,大多数现代浏览器都会为你做这件事:即使你忘记删除监听器,当被监测对象变得无法访问,它们就会自动收集监测处理器。这是过去的一些浏览器无法处理的情况(例如旧的IE6)。

看下面的例子:

var element = document.getElementById('launch-button'); var counter = 0; function onClick(event) { counter++; element.innerHtml = 'text ' + counter; } element.addEventListener('click', onClick); // Do stuff element.removeEventListener('click', onClick); element.parentNode.removeChild(element); // Now when element goes out of scope, // both element and onClick will be collected even in old browsers // that don't handle cycles well.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var element = document.getElementById('launch-button');
var counter = 0;
 
function onClick(event) {
   counter++;
   element.innerHtml = 'text ' + counter;
}
 
element.addEventListener('click', onClick);
 
// Do stuff
 
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
 
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers // that don't handle cycles well.

由于现代浏览器支持垃圾回收机制,所以当某个节点变的不能访问时,你不再需要调用removeEventListener,因为垃圾回收机制会恰当的处理这些节点。

如果你正在使用jQueryAPI(其他库和框架也支持这一点),那么也可以在节点不用之前删除监听器。即使应用程序在较旧的浏览器版本下运行,库也会确保没有内存泄漏。

3:闭包

JavaScript开发的一个关键方面是闭包。闭包是一个内部函数,可以访问外部(封闭)函数的变量。由于JavaScript运行时的实现细节,可能存在以下形式泄漏内存:

var theThing = null; var replaceThing = function(){ var originalThing = theThing; var unused = function(){ if(originalThing)//对'originalThing'的引用 console.log(“hi”); }; theThing = { longStr:new Array(1000000).join('*'), someMethod:function(){ console.log(“message”); } }; }; setInterval(replaceThing,1000);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var theThing = null;
 
var replaceThing = function(){
 
  var originalThing = theThing;
  var unused = function(){
    if(originalThing)//对'originalThing'的引用
      console.log(“hi”);
  };
 
  theThing = {
    longStr:new Array(1000000).join('*'),
    someMethod:function(){
      console.log(“message”);
    }
  };
};
 
setInterval(replaceThing,1000);

一旦replaceThing被调用,theThing会获取由一个大数组和一个新的闭包(someMethod)组成的新对象。然而,originalThing会被unused变量所持有的闭包所引用(这是theThing从以前的调用变量replaceThing)。需要记住的是,一旦在同一父作用域中为闭包创建了闭包的作用域,作用域就被共享了。

在这种情况下,闭包创建的范围会将someMethod共享给unused。然而,unused有一个originalThing引用。即使unused从未使用过,someMethod 也可以通过theThing在整个范围之外使用replaceThing。而且someMethod通过unused共享了闭包范围,unused必须引用originalThing以便使其它保持活跃(两封闭之间的整个共享范围)。这就阻止了它被收集。

所有这些都可能导致相当大的内存泄漏。当上面的代码片段一遍又一遍地运行时,你会看到内存使用率的不断上升。当垃圾收集器运行时,其内存大小不会缩小。这种情况会创建一个闭包的链表,并且每个闭包范围都带有对大数组的间接引用。

4:超出DOM引用

在某些情况下,开发人员会在数据结构中存储DOM节点,例如你想快速更新表格中的几行内容的情况。如果在字典或数组中存储对每个DOM行的引用,则会有两个对同一个DOM元素的引用:一个在DOM树中,另一个在字典中。如果你不再需要这些行,则需要使两个引用都无法访问。

var elements = { button: document.getElementById('button'), image: document.getElementById('image') }; function doStuff() { elements.image.src = ''; } function removeImage() { // The image is a direct child of the body element. document.body.removeChild(document.getElementById('image')); // At this point, we still have a reference to #button in the //global elements object. In other words, the button element is //still in memory and cannot be collected by the GC. }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')
};
 
function doStuff() {
    elements.image.src = 'http://example.com/image_name.png';
}
 
function removeImage() {
    // The image is a direct child of the body element.
    document.body.removeChild(document.getElementById('image'));
    // At this point, we still have a reference to #button in the
    //global elements object. In other words, the button element is
    //still in memory and cannot be collected by the GC.
}

在涉及DOM树内的内部节点或叶节点时,还有一个额外的因素需要考虑。如果你在代码中保留对表格单元格(标签)的引用,并决定从DOM中删除该表格,还需要保留对该特定单元格的引用,则可能会出现严重的内存泄漏。你可能会认为垃圾收集器会释放除了那个单元之外的所有东西,但情况并非如此。由于单元格是表格的一个子节点,并且子节点保留着对父节点的引用,所以对表格单元格的这种引用,会将整个表格保存在内存中。

内存引用

垃圾收集算法所依赖的主要概念之一就是内存引用。

在内存管理情况下,如果一个对象访问变量(可以是隐含的或显式的),则称该对象引用另一个对象。例如,JavaScript对象具有对其原对象(隐式引用)及其属性值(显式引用)的引用。

在这种情况下,“对象”的概念扩展到比普通JavaScript对象更广泛的范围,并且还包含函数范围。

if (originalThing) //对originalThing的引用

延伸阅读

  • Memory Management – Mozilla Developer Network
  • JScript Memory Leaks – Douglas Crockford (old, in relation to Internet Explorer 6 leaks)
  • JavaScript Memory Profiling – Chrome Developer Docs
  • Memory Diagnosis – Google Developers
  • An Interesting Kind of JavaScript Memory Leak – Meteor blog
  • Grokking V8 closures

打赏支持我翻译更多好文章,谢谢!

打赏译者

抵制垃圾收集器的直观行为

尽管垃圾收集器使用起来很方便,但它们也有自己的一套标准,其中之一是非决定论。换句话说,垃圾收集是不可预测的。你不能真正知道什么时候进行收集,这意味着在某些情况下,程序会使用更多的内存,虽然这是实际需要的。在其它情况下,在特别敏感的应用程序中,短暂暂停是很可能出现的。尽管非确定性意味着不能确定何时进行集合,但大多数垃圾收集实现了共享在分配期间进行收集的通用模式。如果没有执行分配,大多数垃圾收集会保持空闲状态。如以下情况:

  1. 大量的分配被执行。
  2. 大多数这些元素(或所有这些元素)被标记为无法访问(假设我们将一个引用指向不再需要的缓存)。
  3. 没有进一步的分配执行。

在这种情况下,大多数垃圾收集不会做出任何的收集工作。换句话说,即使有不可用的引用需要收集,但是收集器不会进行收集。虽然这并不是严格的泄漏,但仍会导致内存使用率高于平时。

概述

renderer.innerHTML = JSON.stringify(serverData);

金沙棋牌官方平台,4类 JavaScript 内存泄漏及如何避免

2016/05/26 · JavaScript · 1 评论 · 内存泄漏

本文由 伯乐在线 - 涂鸦码龙 翻译。未经许可,禁止转载!
英文出处:Sebastián Peyrott。欢迎加入翻译组。

译者注:本文并没有逐字逐句的翻译,而是把我认为重要的信息做了翻译。如果您的英文熟练,可以直接阅读原文。

本文将探索常见的客户端 JavaScript 内存泄漏,以及如何使用 Chrome 开发工具发现问题。

引用计数垃圾收集

这是最简单的垃圾收集算法。如果有零个引用指向它,则该对象会被认为是“垃圾收集” 。

看看下面的代码:

var o1 = { o2: { x: 1 } }; // 2 objects are created. // 'o2' is referenced by 'o1' object as one of its properties. // None can be garbage-collected var o3 = o1; // the 'o3' variable is the second thing that // has a reference to the object pointed by 'o1'. o1 = 1; // now, the object that was originally in 'o1' has a // single reference, embodied by the 'o3' variable var o4 = o3.o2; // reference to 'o2' property of the object. // This object has now 2 references: one as // a property. // The other as the 'o4' variable o3 = '374'; // The object that was originally in 'o1' has now zero // references to it. // It can be garbage-collected. // However, what was its 'o2' property is still // referenced by the 'o4' variable, so it cannot be // freed. o4 = null; // what was the 'o2' property of the object originally in // 'o1' has zero references to it. // It can be garbage collected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var o1 = {
  o2: {
    x: 1
  }
};
 
// 2 objects are created.
// 'o2' is referenced by 'o1' object as one of its properties.
// None can be garbage-collected
 
 
var o3 = o1; // the 'o3' variable is the second thing that
            // has a reference to the object pointed by 'o1'.
 
                                                      
o1 = 1;      // now, the object that was originally in 'o1' has a        
            // single reference, embodied by the 'o3' variable
 
var o4 = o3.o2; // reference to 'o2' property of the object.
                // This object has now 2 references: one as
                // a property.
                // The other as the 'o4' variable
 
o3 = '374'; // The object that was originally in 'o1' has now zero
            // references to it.
            // It can be garbage-collected.
            // However, what was its 'o2' property is still
            // referenced by the 'o4' variable, so it cannot be
            // freed.
 
o4 = null; // what was the 'o2' property of the object originally in
           // 'o1' has zero references to it.
           // It can be garbage collected.

引用计数垃圾收集

这是最简单的垃圾收集算法。如果有零个引用指向它,则该对象会被认为是“垃圾收集” 。

看看下面的代码:

varo1 ={

o2: {

x:1}

};//2 objects are created.//'o2' is referenced by 'o1' object as one of its properties.//None can be garbage-collectedvaro3 = o1;//the 'o3' variable is the second thing that//has a reference to the object pointed by 'o1'.o1=1;//now, the object that was originally in 'o1' has a//single reference, embodied by the 'o3' variablevaro4 = o3.o2;//reference to 'o2' property of the object.//This object has now 2 references: one as//a property.//The other as the 'o4' variableo3='374';//The object that was originally in 'o1' has now zero//references to it.//It can be garbage-collected.//However, what was its 'o2' property is still//referenced by the 'o4' variable, so it cannot be//freed.o4=null;//what was the 'o2' property of the object originally in//'o1' has zero references to it.//It can be garbage collected.

//由a1和a2的元素串联成新的4个元素的数组

保存两个快照

切换到 Chrome Dev Tools 的 profiles 标签,刷新页面,等页面刷新完成之后,点击 Take Heap Snapshot 保存快照作为基准。而后再次点击 The Button 按钮,等数秒以后,保存第二个快照。

金沙棋牌官方平台 14

筛选菜单选择 Summary,右侧选择 Objects allocated between Snapshot 1 and Snapshot 2,或者筛选菜单选择 Comparison ,然后可以看到一个对比列表。

此例很容易找到内存泄漏,看下 (string) 的 Size Delta Constructor,8MB,58个新对象。新对象被分配,但是没有释放,占用了8MB。

如果展开 (string) Constructor,会看到许多单独的内存分配。选择某一个单独的分配,下面的 retainers 会吸引我们的注意。

金沙棋牌官方平台 15

我们已选择的分配是数组的一部分,数组关联到 window 对象的 x 变量。这里展示了从巨大对象到无法回收的 root(window)的完整路径。我们已经找到了潜在的泄漏以及它的出处。

我们的例子还算简单,只泄漏了少量的 DOM 节点,利用以上提到的快照很容易发现。对于更大型的网站,Chrome 还提供了 Record Heap Allocations 功能。

内存引用

垃圾收集算法所依赖的主要概念之一就是内存引用。

在内存管理情况下,如果一个对象访问变量(可以是隐含的或显式的),则称该对象引用另一个对象。例如,JavaScript对象具有对其原对象(隐式引用)及其属性值(显式引用)的引用。

在这种情况下,“对象”的概念扩展到比普通JavaScript对象更广泛的范围,并且还包含函数范围。

即使使用高级语言,开发人员也应该理解内存管理的知识。有时自动内存管理也会存在问题(例如垃圾收集器中的错误或实施限制等),开发人员必须了解这些问题才能正确地进行处理。

var s = 'sessionstack'; //为字符串分配内存

Profiles

金沙棋牌官方平台 16

Profiles 是你可以花费大量时间关注的工具,它可以保存快照,对比 JavaScript 代码内存使用的不同快照,也可以记录时间分配。每一次结果包含不同类型的列表,与内存泄漏相关的有 summary(概要) 列表和 comparison(对照) 列表。

summary(概要) 列表展示了不同类型对象的分配及合计大小:shallow size(特定类型的所有对象的总大小),retained size(shallow size 加上其它与此关联的对象大小)。它还提供了一个概念,一个对象与关联的 GC root 的距离。

对比不同的快照的 comparison list 可以发现内存泄漏。

标记和扫描算法

为了决定是否需要对象,标记和扫描算法会确定对象是否是活动的。

标记和扫描算法经过以下3个步骤:

  1. roots:通常,root是代码中引用的全局变量。例如,在JavaScript中,可以充当root的全局变量是“窗口”对象。Node.js中的相同对象称为“全局”。所有root的完整列表由垃圾收集器构建。
  2. 然后算法会检查所有root和他们的子对象并且标记它们是活动的(即它们不是垃圾)。任何root不能达到的,将被标记为垃圾。
  3. 最后,垃圾回收器释放所有未标记为活动的内存块,并将该内存返回给操作系统。

金沙棋牌官方平台 17

这个算法比引用计数垃圾收集算法更好。JavaScript垃圾收集(代码/增量/并发/并行垃圾收集)领域中所做的所有改进都是对这种标记和扫描算法的实现改进,但不是对垃圾收集算法本身的改进。

动态分配

如若我们不知道编译时,变量需要的内存数量时,事情就会变得复杂。假设我们想要做如下事项:

intn = readInput();//读取用户的输入

...

/用“n”个元素创建一个数组

在编译时,编译器不知道数组需要多少内存,因为它是由用户提供的输入值决定的。

因此,它不能为堆栈上的变量分配空间。相反,我们的程序需要在运行时明确地向操作系统请求适当的空间。这个内存是从堆空间分配的。下表总结了静态和动态内存分配之间的区别:

金沙棋牌官方平台 18

console.log("hi");

什么是内存泄漏?

本质上,内存泄漏可以定义为:应用程序不再需要占用内存的时候,由于某些原因,内存没有被操作系统或可用内存池回收。编程语言管理内存的方式各不相同。只有开发者最清楚哪些内存不需要了,操作系统可以回收。一些编程语言提供了语言特性,可以帮助开发者做此类事情。另一些则寄希望于开发者对内存是否需要清晰明了。

概述

像C语言这样的编程语言,具有简单的内存管理功能函数,例如malloc( )和free( )。开发人员可以使用这些功能函数来显式地分配和释放系统的内存。

当创建对象和字符串等时,JavaScript就会分配内存,并在不再使用时自动释放内存,这种机制被称为垃圾收集。这种释放资源看似是“自动”的,但本质是混淆的,这也给JavaScript(以及其他高级语言)的开发人员产生了可以不关心内存管理的错误印象。其实这是一个大错误。

即使使用高级语言,开发人员也应该理解内存管理的知识。有时自动内存管理也会存在问题(例如垃圾收集器中的错误或实施限制等),开发人员必须了解这些问题才能正确地进行处理。

在JavaScript中分配内存

现在来解释如何在JavaScript中分配内存。

JavaScript使得开发人员免于处理内存分配的工作。

varn =374;//allocates memory for a numbervars ='sessionstack';//allocates memory for a stringvaro ={

    a:1,

    b:null};

    //allocates memory for an object and its contained values

    vara = [1,null,'str'];

    /(like object) allocates memory for the

    //array and its contained valuesfunction f(a) {returna +3;

}    

//allocates a function (which is a callable object)//function expressions also allocate an objectsomeElement.addEventListener('click', function() {

someElement.style.backgroundColor='blue';

},false);

一些函数调用也会导致对象分配:

vard =newDate();//allocates a Date objectvare = document.createElement('div');//allocates a DOM element

方法可以分配新的值或对象:

vars1 ='sessionstack';vars2 = s1.substr(0,3);//s2 is a new string//Since strings are immutable,//JavaScript may decide to not allocate memory,//but just store the [0, 3] range.vara1 = ['str1','str2'];vara2 = ['str3','str4'];vara3 =a1.concat(a2);//new array with 4 elements being//the concatenation of a1 and a2 elements

Memory references -内存引用

3:脱离 DOM 的引用

有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。

JavaScript

var elements = { button: document.getElementById('button'), image: document.getElementById('image'), text: document.getElementById('text') }; function doStuff() { image.src = ''; button.click(); console.log(text.innerHTML); // 更多逻辑 } function removeButton() { // 按钮是 body 的后代元素 document.body.removeChild(document.getElementById('button')); // 此时,仍旧存在一个全局的 #button 的引用 // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。 }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};
 
function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
    // 更多逻辑
}
 
function removeButton() {
    // 按钮是 body 的后代元素
    document.body.removeChild(document.getElementById('button'));
 
    // 此时,仍旧存在一个全局的 #button 的引用
    // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}

此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 <td> 的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 <td> 以外的其它节点。实际情况并非如此:此<td> 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 <td> 的引用,导致整个表格仍待在内存中。保存 DOM 元素引用的时候,要小心谨慎。

在JavaScript中分配内存

现在来解释如何在JavaScript中分配内存。

JavaScript使得开发人员免于处理内存分配的工作。

var n = 374; // allocates memory for a number var s = 'sessionstack'; // allocates memory for a string var o = { a: 1, b: null }; // allocates memory for an object and its contained values var a = [1, null, 'str']; // (like object) allocates memory for the // array and its contained values function f(a) { return a + 3; } // allocates a function (which is a callable object) // function expressions also allocate an object someElement.addEventListener('click', function() { someElement.style.backgroundColor = 'blue'; }, false);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var n = 374; // allocates memory for a number
var s = 'sessionstack'; // allocates memory for a string
 
var o = {
  a: 1,
  b: null
}; // allocates memory for an object and its contained values
 
var a = [1, null, 'str'];  // (like object) allocates memory for the
                           // array and its contained values
 
function f(a) {
  return a + 3;
} // allocates a function (which is a callable object)
 
// function expressions also allocate an object
someElement.addEventListener('click', function() {
  someElement.style.backgroundColor = 'blue';
}, false);

一些函数调用也会导致对象分配:

var d = new Date(); // allocates a Date object var e = document.createElement('div'); // allocates a DOM element

1
2
var d = new Date(); // allocates a Date object
var e = document.createElement('div'); // allocates a DOM element

方法可以分配新的值或对象:

var s1 = 'sessionstack'; var s2 = s1.substr(0, 3); // s2 is a new string // Since strings are immutable, // JavaScript may decide to not allocate memory, // but just store the [0, 3] range. var a1 = ['str1', 'str2']; var a2 = ['str3', 'str4']; var a3 = a1.concat(a2); // new array with 4 elements being // the concatenation of a1 and a2 elements

1
2
3
4
5
6
7
8
9
10
11
var s1 = 'sessionstack';
var s2 = s1.substr(0, 3); // s2 is a new string
// Since strings are immutable,
// JavaScript may decide to not allocate memory,
// but just store the [0, 3] range.
 
var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2);
// new array with 4 elements being
// the concatenation of a1 and a2 elements

当内存不再需要时进行释放

大部分内存泄漏问题都是在这个阶段产生的,这个阶段最难的问题就是确定何时不再需要已分配的内存。它通常需要开发人员确定程序中的哪个部分不再需要这些内存,并将其释放。

高级语言嵌入了一个名为垃圾收集器的功能,其工作是跟踪内存分配和使用情况,以便在不再需要分配内存的情况下自动释放内存。

不幸的是,这个过程无法做到那么准确,因为像某些内存不再需要的问题是不能由算法来解决的。

大多数垃圾收集器通过收集不能被访问的内存来工作,例如指向它的变量超出范围的这种情况。然而,这种方式只能收集内存空间的近似值,因为在内存的某些位置可能仍然有指向它的变量,但它却不会被再次访问。

由于确定一些内存是否“不再需要”,是不可判定的,所以垃圾收集机制就有一定的局限性。下面将解释主要垃圾收集算法及其局限性的概念。

//但由于其'o2'属性仍被'o4'变量引用,所以不能被释放

4:闭包

闭包是 JavaScript 开发的一个关键方面:匿名函数可以访问父级作用域的变量。

代码示例:

JavaScript

var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) console.log("hi"); }; theThing = { longStr: new Array(1000000).join('*'), someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
 
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
 
setInterval(replaceThing, 1000);

代码片段做了一件事情:每次调用 replaceThing ,theThing 得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 replaceThing 又调用了 theThing )。思绪混乱了吗?最重要的事情是,闭包的作用域一旦创建,它们有同样的父级作用域,作用域是共享的。someMethod 可以通过 theThing 使用,someMethod 与 unused 分享闭包作用域,尽管 unused 从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC)并无法降低内存占用。本质上,闭包的链表已经创建,每一个闭包作用域携带一个指向大数组的间接的引用,造成严重的内存泄漏。

Meteor 的博文 解释了如何修复此种问题。在 replaceThing 的最后添加 originalThing = null 。

当内存不再需要时进行释放

大部分内存泄漏问题都是在这个阶段产生的,这个阶段最难的问题就是确定何时不再需要已分配的内存。它通常需要开发人员确定程序中的哪个部分不再需要这些内存,并将其释放。

高级语言嵌入了一个名为垃圾收集器的功能,其工作是跟踪内存分配和使用情况,以便在不再需要分配内存的情况下自动释放内存。

不幸的是,这个过程无法做到那么准确,因为像某些内存不再需要的问题是不能由算法来解决的。

大多数垃圾收集器通过收集不能被访问的内存来工作,例如指向它的变量超出范围的这种情况。然而,这种方式只能收集内存空间的近似值,因为在内存的某些位置可能仍然有指向它的变量,但它却不会被再次访问。

由于确定一些内存是否“不再需要”,是不可判定的,所以垃圾收集机制就有一定的局限性。下面将解释主要垃圾收集算法及其局限性的概念。

像C语言这样的编程语言,具有简单的内存管理功能函数,例如malloc( )和free( )。开发人员可以使用这些功能函数来显式地分配和释放系统的内存。

};

Record heap allocations 找内存泄漏

回到 Chrome Dev Tools 的 profiles 标签,点击 Record Heap Allocations。工具运行的时候,注意顶部的蓝条,代表了内存分配,每一秒有大量的内存分配。运行几秒以后停止。

金沙棋牌官方平台 19

上图中可以看到工具的杀手锏:选择某一条时间线,可以看到这个时间段的内存分配情况。尽可能选择接近峰值的时间线,下面的列表仅显示了三种 constructor:其一是泄漏最严重的(string),下一个是关联的 DOM 分配,最后一个是 Text constructor(DOM 叶子节点包含的文本)。

从列表中选择一个 HTMLDivElement constructor,然后选择 Allocation stack。

金沙棋牌官方平台 20

现在知道元素被分配到哪里了吧(grow -> createSomeNodes),仔细观察一下图中的时间线,发现 HTMLDivElement constructor 调用了许多次,意味着内存一直被占用,无法被 GC 回收,我们知道了这些对象被分配的确切位置(createSomeNodes)。回到代码本身,探讨下如何修复内存泄漏吧。

什么是内存泄漏?

内存泄漏是应用程序使用过的内存片段,在不再需要时,不能返回到操作系统或可用内存池中的情况。

编程语言有各自不同的内存管理方式。但是是否使用某一段内存,实际上是一个不可判定的问题。换句话说,只有开发人员明确的知道是否需要将一块内存返回给操作系统。

四种常见的JavaScript内存泄漏

1:全局变量

JavaScript以一种有趣的方式来处理未声明的变量:当引用未声明的变量时,会在全局对象中创建一个新变量。在浏览器中,全局对象将是window,这意味着

function foo(arg) {

bar="some text";

}

相当于:

function foo(arg) {

window.bar="some text";

}

bar只是foo函数中引用一个变量。如果你不使用var声明,将会创建一个多余的全局变量。在上述情况下,不会造成很大的问题。但是,如若是下面的这种情况。

你也可能不小心创建一个全局变量this:

function foo() {this.var1 ="potential accidental global";

}//Foo called on its own, this points to the global object (window)//rather than being undefined.foo( );

你可以通过在JavaScript文件的开始处添加‘use strict’;来避免这中错误,这种方式将开启严格的解析JavaScript模式,从而防止意外创建全局变量。

意外的全局变量当然是一个问题。更多的时候,你的代码会受到显式的全局变量的影响,而这些全局变量在垃圾收集器中是无法收集的。需要特别注意用于临时存储和处理大量信息的全局变量。如果必须使用全局变量来存储数据,那么确保将其分配为空值,或者在完成后重新分配。

2:被遗忘的定时器或回调

下面列举setInterval的例子,这也是经常在JavaScript中使用。

对于提供监视的库和其它接受回调的工具,通常在确保所有回调的引用在其实例无法访问时,会变成无法访问的状态。但是下面的代码却是一个例外:

varserverData =loadData();

setInterval(function() {varrenderer = document.getElementById('renderer');if(renderer) {

renderer.innerHTML=JSON.stringify(serverData);

}

},5000);//This will be executed every ~5 seconds.

上面的代码片段显示了使用引用节点或不再需要的数据的定时器的结果。

该renderer对象可能会在某些时候被替换或删除,这会使interval处理程序封装的块变得冗余。如果发生这种情况,那么处理程序及其依赖项都不会被收集,因为interval需要先停止。这一切都归结为存储和处理负载数据的serverData不会被收集的原因。

当使用监视器时,你需要确保做了一个明确的调用来删除它们。

幸运的是,大多数现代浏览器都会为你做这件事:即使你忘记删除监听器,当被监测对象变得无法访问,它们就会自动收集监测处理器。这是过去的一些浏览器无法处理的情况(例如旧的IE6)。

看下面的例子:

varelement = document.getElementById('launch-button');varcounter =0;

function onClick(event) {

counter++;

element.innerHtml='text'+counter;

}

element.addEventListener('click', onClick);//Do stuffelement.removeEventListener('click', onClick);

element.parentNode.removeChild(element);//Now when element goes out of scope,//both element and onClick will be collected even in old browsers//that don't handle cycles well.

由于现代浏览器支持垃圾回收机制,所以当某个节点变的不能访问时,你不再需要调用removeEventListener,因为垃圾回收机制会恰当的处理这些节点。

如果你正在使用jQueryAPI(其他库和框架也支持这一点),那么也可以在节点不用之前删除监听器。即使应用程序在较旧的浏览器版本下运行,库也会确保没有内存泄漏。

3:闭包

JavaScript开发的一个关键方面是闭包。闭包是一个内部函数,可以访问外部(封闭)函数的变量。由于JavaScript运行时的实现细节,可能存在以下形式泄漏内存:

vartheThing =null;varreplaceThing =function(){varoriginalThing =theThing;varunused =function(){if(originalThing)//对'originalThing'的引用console.log(“hi”);

};

theThing={

longStr:newArray(1000000).join('*'),

someMethod:function(){

console.log(“message”);

}

};

};

setInterval(replaceThing,1000);

一旦replaceThing被调用,theThing会获取由一个大数组和一个新的闭包(someMethod)组成的新对象。然而,originalThing会被unused变量所持有的闭包所引用(这是theThing从以前的调用变量replaceThing)。需要记住的是,一旦在同一父作用域中为闭包创建了闭包的作用域,作用域就被共享了。

在这种情况下,闭包创建的范围会将someMethod共享给unused。然而,unused有一个originalThing引用。即使unused从未使用过,someMethod 也可以通过theThing在整个范围之外使用replaceThing。而且someMethod通过unused共享了闭包范围,unused必须引用originalThing以便使其它保持活跃(两封闭之间的整个共享范围)。这就阻止了它被收集。

所有这些都可能导致相当大的内存泄漏。当上面的代码片段一遍又一遍地运行时,你会看到内存使用率的不断上升。当垃圾收集器运行时,其内存大小不会缩小。这种情况会创建一个闭包的链表,并且每个闭包范围都带有对大数组的间接引用。

4:超出DOM引用

在某些情况下,开发人员会在数据结构中存储DOM节点,例如你想快速更新表格中的几行内容的情况。如果在字典或数组中存储对每个DOM行的引用,则会有两个对同一个DOM元素的引用:一个在DOM树中,另一个在字典中。如果你不再需要这些行,则需要使两个引用都无法访问。

varelements ={

button: document.getElementById('button'),

image: document.getElementById('image')

};

function doStuff() {

elements.image.src='';

}

function removeImage() {//The image is a direct child of the body element.document.body.removeChild(document.getElementById('image'));//At this point, we still have a reference to #button in the//global elements object. In other words, the button element is//still in memory and cannot be collected by the GC.}

在涉及DOM树内的内部节点或叶节点时,还有一个额外的因素需要考虑。如果你在代码中保留对表格单元格(标签)的引用,并决定从DOM中删除该表格,还需要保留对该特定单元格的引用,则可能会出现严重的内存泄漏。你可能会认为垃圾收集器会释放除了那个单元之外的所有东西,但情况并非如此。由于单元格是表格的一个子节点,并且子节点保留着对父节点的引用,所以对表格单元格的这种引用,会将整个表格保存在内存中。

// img元素的父元素是bodydocument.body.removeChild(document.getElementById('image'));

打赏支持我翻译更多好文章,谢谢!

任选一种支付方式

金沙棋牌官方平台 21 金沙棋牌官方平台 22

1 赞 10 收藏 1 评论

内存生命周期

无论你使用的是什么编程语言,内存生命周期几乎都是一样的:

金沙棋牌官方平台 23

以下是对内存生命周期中每个步骤发生的情况的概述:

分配内存- 内存由操作系统分配,允许程序使用它。在简单的编程语言中,这个过程是开发人员应该处理的一个显式操作。然而,在高级编程语言中,系统会帮助你完成这个操作。

内存使用-这是程序使用之前申请内存的时间段,你的代码会通过使用分配的变量

来对内存进行读取和写入操作。

释放内存- 对于不再需要的内存进行释放的操作,以便确保其变成空闲状态并且可以被再次使用。与分配内存操作一样,这个操作在简单的编程语言中是需要显示操作的。

释放内存—当不再需要使用内存时,就是完全释放整个被分配内存空间的时机,内存重新变为可用的。与分配内存一样,该操作只在低级语言中需要手动进行。

Timeline

金沙棋牌官方平台 24

timeline 可以检测代码中不需要的内存。在此截图中,我们可以看到潜在的泄漏对象稳定的增长,数据采集快结束时,内存占用明显高于采集初期,Node(节点)的总量也很高。种种迹象表明,代码中存在 DOM 节点泄漏的情况。

标记和扫描算法

为了决定是否需要对象,标记和扫描算法会确定对象是否是活动的。

标记和扫描算法经过以下3个步骤:

roots:通常,root是代码中引用的全局变量。例如,在JavaScript中,可以充当root的全局变量是“窗口”对象。Node.js中的相同对象称为“全局”。所有root的完整列表由垃圾收集器构建。

然后算法会检查所有root和他们的子对象并且标记它们是活动的(即它们不是垃圾)。任何root不能达到的,将被标记为垃圾。

最后,垃圾回收器释放所有未标记为活动的内存块,并将该内存返回给操作系统。

金沙棋牌官方平台 25

这个算法比引用计数垃圾收集算法更好。JavaScript垃圾收集(代码/增量/并发/并行垃圾收集)领域中所做的所有改进都是对这种标记和扫描算法的实现改进,但不是对垃圾收集算法本身的改进。

因此,它们将被垃圾回收器认定是不可达的。

JavaScript 内存泄漏

垃圾回收语言的内存泄漏主因是不需要的引用。理解它之前,还需了解垃圾回收语言如何辨别内存的可达与不可达。

var o1 = {

三种类型的常见 JavaScript 内存泄漏

尽管GC很方便,但也带来一些取舍权衡。其中一点是其不可预知性。换句话说,GC是没准儿的,无法真正的说清回收什么时候进行。这意味着有时程序使用了超过其实际需要的内存;另一些情况下,应用可能会假死。

Mark-and-sweep

大部分垃圾回收语言用的算法称之为 Mark-and-sweep 。算法由以下几步组成:

  1. 垃圾回收器创建了一个“roots”列表。Roots 通常是代码中全局变量的引用。JavaScript 中,“window” 对象是一个全局变量,被当作 root 。window 对象总是存在,因此垃圾回收器可以检查它和它的所有子对象是否存在(即不是垃圾);
  2. 所有的 roots 被检查和标记为激活(即不是垃圾)。所有的子对象也被递归地检查。从 root 开始的所有对象如果是可达的,它就不被当作垃圾。
  3. 所有未被标记的内存会被当做垃圾,收集器现在可以释放内存,归还给操作系统了。

现代的垃圾回收器改良了算法,但是本质是相同的:可达内存被标记,其余的被当作垃圾回收。

不需要的引用是指开发者明知内存引用不再需要,却由于某些原因,它仍被留在激活的 root 树中。在 JavaScript 中,不需要的引用是保留在代码中的变量,它不再需要,却指向一块本该被释放的内存。有些人认为这是开发者的错误。

为了理解 JavaScript 中最常见的内存泄漏,我们需要了解哪种方式的引用容易被遗忘。

 

o1.p = o2; // o1 references o2

在直接转入JS内存的话题前,我们主要讨论一下通常内存的含义,并简短说一下它是如何工作的。

金沙棋牌官方平台 26

var o4 = o3.o2;

该问题的更多描述见Meteor团队的这篇文章。

setInterval(function() {

由于找出某些内存是否“不再被需要”是不可决定的,gc实现了对解决一般问题的一个限制。本章将解释必要的概念,以理解主要的gc算法和其限制。

var o1 = {};

someMethod: function () {

该算法靠判断对象是否可达,来决定对象是否是需要的。

}

//直接执行了构造函数,this指向了window

function f(a) {

在以上例子中,编译器清楚的制度每个变量所需内存。事实上,每当我们写入变量n时,这个变量在内部就被翻译成类似“内存地址4127963”了。

看下面的代码:

foo();

} //为函数分配内存(也就是一个可调用的对象)

尽管不可预知性意味着无法确定回收的执行时机,但大部分GC的实现都共享了在分配过程中才执行回收的通用模式。如果没有执行分配,大部分GC也会保持空闲。

//变量'o3'引用了'o1'指向的对象

element.parentNode.removeChild(element);

在JS中使用setInterval稀松平常。

在此上下文中,所谓“对象”的指向就比纯JavaScript object更宽泛了,包括了函数作用域(或全局词法作用域)在内。

}

//作为父对象的属性,以及被变量‘o4’引用

var counter = 0;

为了正确处理(或尽快找到合适的变通方案)时不时由自动内存管理引发的问题(一些bug或者gc的实现局限性等),即便是使用高级语言,开发者也应该理解内存管理(至少是基本的)。

o4 = null;

//因为字符串是不可变的,

}; //为对象和其包含的值分配内存

4 + 4 × 4 + 8 = 28 bytes.

这是最简单的一种gc算法。如果一个对象是“零引用”了,就被认为是该回收的。

function foo() {

而对于观察者的场景,重要的是移除那些不再有用的明确引用(或相关的对象)。


遗憾的是,这只是一个粗略估算的过程,因为要知道需要多少内存的问题是不可决定的(无法通过算法解决)。

这就是当前整数和双精度浮点数的工作方式;而在20年前(16位机器上),典型的整数只用2字节存储,而双精度数用4字节。所以代码不应该依赖于当前基础数据类型的大小。

var element = document.getElementById('launch-button');

if(renderer) {

作为人类,难以在位层面思考和计算,而是从大的维度上管理数据—将位集合成大一些的组就可以用来表示数字。8位被叫做1字节。除了字节,有时还有16位或32位等分组称呼。

var d = new Date(); // allocates a Date object

Garbage collection -内存回收器

//只是存储[0, 3]的范围.

在类似C的语言中,存在一些诸如malloc()和free()的低级操作方法,用来人为的精确分配和释放操作系统内存。

金沙棋牌官方平台 27

2: Timers or callbacks that are forgotten -被遗忘的定时器或回调函数

var elements = {

Using memory in JavaScript -在JS中使用内存

int n; // 4 bytes

o2.p = o1; // o2 references o1. This creates a cycle.

编译器和操作系统共同管理大部分内存,但最好看一看底层发生了什么。当编译代码时,编译器会检查基本数据类型并提前计算它们需要多少内存。所需的内存数量被以“栈空间”的名义分配给程序,而这种称呼的原因是:当函数被调用时,其内存被置于已存在内存的顶部;当调用结束后,以LIFO(后入先出)的顺序被移除。举例来说,看一下以下声明:

var originalThing = theThing;

};

The four types of common JavaScript leaks -四种常见的JS内存泄漏

有时把DOM节点储存在数据结构里是有用的。假设要一次性更新表格的多行内容,那么把每个DOM行的引用保存在一个字典或数组中是合理的;这样做的结果是,同一个DOM元素会在DOM数和JS数据中

编译器向栈中申请好一定数量的字节,并把即将和操作系统交互的代码插入其中,以存储变量。

.其中的大部分元素(或全部)被标记为不可达(假设我们对不再需要用的一个缓存设为null)。

}

function foo(arg) {

var s1 = 'sessionstack';

bar = "some text";

4: Out of DOM references -脱离DOM的引用

var n = 374; //为数字分配内存

int x[4]; // array of 4 elements, each 4 bytes

someElement.addEventListener('click', function() {

如果bar应该是所在foo函数作用域中的变量,而你忘了用var声明它,那就会创建一个期望外的全局变量。

可以看这篇帖子快速了解调用栈和内存堆。

o2: {

}

使用内存—使用程序代码中的变量等时,引发了读写操作,从而真正使用了先前分配的内存。

What are memory leaks? -何为内存泄漏

金沙棋牌官方平台 28

theThing = {

//函数表达式也是为对象分配内存

gc算法主要依赖的一个概念就是引用

Release when the memory is not needed anymore -当不再需要内存时释放它

循环引用会带来问题。在下面的例子中,两个对象被创建并互相引用,这就形成了一个循环引用。当他们都离开了所在函数的作用域后,却因为互相有1次引用,而被引用计数算法认为不能被gc。

elements.image.src = '';

诸如jQuery等框架和库在去除节点之前做了移除监听工作(当调用其特定API时)。这种库内部的处理同时确保了没有泄露发生,即便是运行在问题频发的浏览器时。。。嗯,说的就是IE6。

var serverData = loadData();

标记清除法的运行示意图

算法由以下步骤组成:

所以,无法为变量在栈上分配房间了。相应的,程序必须在运行时明确向操作系统申请正确数量的空间。这部分内存从堆空间中指派。关于静态内存和动态内存分配的不同之处总结在下表中:

然而JS则是在对象(或字符串等)被创建时自动分配内存,并在其不再被使用时“自动”用垃圾回收机制(gc)释放内存。但这种看起来顺其自然的“自动”释放资源成了混乱之源,并给JS(及其他高级语言)开发者一种错误的印象,那就是他们可以不关心内存管理。这是个大毛病。

//原本被'o1'引用的对象只剩下了变量‘o3'的引用

1: Global variables -全局变量

高级语言包含了垃圾回收器的功能,其职责就是跟踪内存分配和使用,以便找出什么时候相应的内存不再有用,并自动释放它。

一些函数调用也按对象分配:

在这里学习更多这种JS执行的模式。

return a + 3;

longStr: new Array(1000000).join('*'),

金沙棋牌官方平台 29

词法作用域定义了如何在嵌套的函数中处理变量名称:内部函数包含了父函数的作用域,即便父函数已经return

o1 = 1;

各有一个引用。如果未来某个时刻要删除这些行,就得使两种引用都不可达才行。

这段代码做了一件事:每次调用replaceThing时,theThing获得一个包含了一个巨大数组和一个新闭包(someMethod)的新对象。同时,变量unused则指向一个引用了originalThing(其实就是前一次调用replaceThing时指定的theThing)的闭包。已经懵了,哈?关键之处在于:一旦同一个父作用域中的闭包们的作用域被创建了,则其作用域是共享的。

长按二维码或搜索 fewelife 关注我们哦

element.innerHtml = 'text ' + counter;

如果试图访问这里的x[4] ,就会访问关联数据m。这是因为访问的是数组中一个并不存在的元素—比数组中实际分配的最后一个元素x[3]又远了4个字节,也就有可能结束读写在m的某个位上。这几乎可以确定将给后续的程序带来非常不希望发生的后果。

}

当前,现代浏览器(包括IE和Microsoft Edge)都使用了可以检测这些循环引用并能正确处理之的现代垃圾回收器算法。也可以说,在使得节点不可达之前,不再有必要严格的调用removeEventListener了。

编译器立刻就能算出这部分代码需要的空间

这个算法比引用计数法更好的地方在于:“零引用”会导致这个对象不可到达;而相反的情况并不像我们在循环引用中看到的那样无法正确处理。

之前,这对某些无法很好的管理循环引用(见上文)的浏览器(IE6咯)非常关键。当今,即使没有明确删除监听器,大部分浏览器都能在观察对象不可达时回收处理函数;但在对象被去除之前,明确移除这些观察者,始终是个好习惯。

var o = {

int n = readInput(); // reads input from the user

在这个场景下,大部分GC不会再运行回收操作。也就是说,尽管有不可达的引用可被回收,但回收器并不工作。并不算严格的泄漏,但仍然导致内存实用高于正常。

另一种意外创建全局变量的途径是通过‘this’ :

直到一块内存中所有的东西都不是活跃的了,就可以被认为都是垃圾了。回收器可以释放这块内存并将其返还给OS。

}

o3 = '374';

image: document.getElementById('image')

];  //为数组和其包含的值分配内存

尽管两个对象相互引用,但根元素无法找到它们。

var o3 = o1;

var a1 = ['str1', 'str2'];

Cycles are creating problems -循环引用带来问题

1, null, ‘str'

3: Closures -闭包

在JS中使用内存,基本上就意味着对其读写。这将发生在读写一个变量、对象属性,或对一个函数传递值等时候。

var a = [

.所有程序使用的变量和其他数据

尽管我们谈论了未知的全局变量,其实代码中也大量存在明确定义的全局变量。它们被定义为不可回收的(除非赋值为null或重新赋值)。特别是用全局变量暂存数据或处理大量的数据,也是值得注意的—如果非要这么做,记得在使用后对其赋值为null或重新指定。

Reference-counting garbage collection -引用计数法

在硬件层面,计算机内存由大量触发器组成。每个触发器包含一些晶体管,并用来储存1比特位(以下简称位)的数据。不同的触发器由唯一标识符定位以便对其读写。所以从概念上讲,我们可以把整个计算机内存想象成一个可读写的巨大位数组。

大部分gc通过收集无法再被访问到的内存来工作,例如所有指向该内存块的变量都离开了其作用域。然而,这只是一组可被收集的内存空间的粗略估计,因为可能存在着某一个变量仍处在其作用域内,但就是永远不再被访问的情况。

var replaceThing = function () {

原文:

var renderer = document.getElementById('renderer');

Cycles are not a problem anymore -循环引用不再是个问题

不管使用什么编程语言,内存生命周期几乎总是相同的:

f();

Memory life cycle -内存生命周期

element.addEventListener('click', onClick);

//原本被'o1'引用的对象现在是“零引用”了

金沙棋牌官方平台 30

垃圾回收器会创建一个列表,用来保存根元素,通常指的是代码中引用到的全局变量。在JS中,’window’对象通常被作为一个根元素。

JS文件开头添加'use strict';可以防止出现这种错误。这将允许用一种严格模式来处理JS,以防意外创建全局变量。**

...

}, false);

编程语言喜欢用不同的方式管理内存。但是,一块内存是否被使用确实是个无解的问题。换句话说,只有开发者能弄清一块内存是否能被返还给操作系统。

var s2 = s1.substr(0, 3); // s2是一个新字符串

金沙棋牌官方平台 31

setInterval(replaceThing, 1000);

Dynamic allocation -动态分配

在内存管理的上下文中,说一个对象引用了另一个的意思,就是指前者直接或间接的访问到了后者。举例来说,一个JavaScriptobject间接引用了其原型对象,而直接引用了其属性值。

// 'o2'现在有了两个引用

button: document.getElementById('button'),

};

方法会被分配新值或对象:

最难办的事就是找出什么时候分配的内存不再有用了。这通常需要开发者决定代码中的哪一块不再需要内存,并释放它。

某些编程语言提供了帮助开发者达到此目的的特性。其他一些期望当一块内存不被使用时,开发者完全明示。

function doStuff() {

Mark-and-sweep algorithm -标记清除法

本质上来说,内存泄漏可以定义为:不再被应用需要的内存,由于某种原因,无法返还给操作系统或空闲内存池。

//现在,当元素离开作用域

JS用一种很逗的方式处理未声明的变量:对一个未声明变量的引用将在global对象中创建一个新变量;在浏览器中就是在window对象中创建。换句话说:

var a2 = ['str3', 'str4'];

var e = document.createElement('div'); // allocates a DOM element

//此时,全局对象elements中仍引用着#button

当这段代码被重复运行时,可以观察到内存占用持续增长,并且在GC运行时不会变小。本质上是,创建了一个闭包的链表(以变量theThing为根),其中每个闭包作用域间接引用一个巨大的数组,从而导致一个超出容量的泄漏。

Allocation in JavaScript - JS中的分配

所有根元素被监视,并被标记为活跃的(也就是不作为垃圾)。所有子元素也被递归的如此处理。从根元素可达的每个元素都不被当成垃圾。

this.var1 = "potential accidental global";

function removeImage() {

//创建了2个对象

// create an array with "n" elements

}

element.removeEventListener('click', onClick);

What is memory? -什么是内存?

分配内存—内存被操作系统分配给程序使用。在低级语言(比如C)中,由开发者手动处理;而在高级语言中,开发者是很省心的。

function foo(arg) {

a: 1,

在本例中,someMethod和unused共享了作用域;而unused引用了originalThing。尽管unused从来没被调用,但通过theThing,someMethod可能会在replaceThing外层作用域(例如全局的某处)被调用。并且因为someMethod和unused共享了闭包作用域,unused因为有对originalThing的引用,从而迫使其保持活跃状态(被两个闭包共享的整个作用域)。这也就阻止了其被回收。

在这个例子中,泄漏的只是一个无害的简单字符串,但实际情况肯定会更糟糕的。

function onClick(event) {

var theThing = null;

自从2012年起,所有现代浏览器都包含了一个标记清除法的垃圾回收器,虽然没有改进算法本身或其判断对象是否可达的目标,但过去一年在JS垃圾回收领域关于标记清除法取得的所有进步(分代回收、增量回收、并发回收、并行回收)都包含在其中了。

在未来的某个时刻,由renderer代表的对象可能会被移除,使得整个定时处理函数块变为无用的。但因为定时器始终有效,处理函数又不会被回收(需要停止定时器才行)。这也意味着,那个看起来个头也不小的serverData,同样也不会被回收。

等价于:

.很大一组分配操作被执行。

var o2 = {};

遗憾的是,当我们不知道编译时变量需要多少内存时,事情就没那么简单了。假设我们要做如下的事情:

x: 1

counter++;

另外需要额外考虑的是对一个DOM树的内部节点或叶子节点的引用。比方说JS代码引用了表格中某个单元格(一个td标签);一旦决定从DOM中删除整个表格,却保留了之前对那个单元格的引用的话,是不会想当然的回收除了那个td之外的其他东西的。实际上,因为单元格作为表格的子元素而持有对父元素的引用,所以JS中对单元格的引用导致了整个表格留在内存中。当保留对DOM元素的引用时,要格外注意这点。

Overview -概览

内存泄漏是不好的...对吧?

window.bar = "some text";

在上面的第一个例子中,当函数调用结束,两个对象将不再被任何从跟对象可达的东西引用。

金沙棋牌官方平台 32

}

当函数调用其他函数时,每个函数各自有其自己调用的那块栈空间。该空间保存着函数所有本地变量,以及一个用来记住执行位置的程序计数器。当函数结束时,这个内存块再次被置为可用,以供其他用处。

//两者都不能被回收

console.log("message");

//原本被'o1'引用的对象可以被gc了

大多数库,如果提供了观察者之类的功能,都会有回调函数;当这些库工具本身的实例变为不可达后,要注意使其引用的回调函数也应不可达。对于setInterval来说,下面这种代码却很常见:

现在解释一下在JS中的第一步(分配内存)如何工作。与声明变量并赋值的同时,JS自动进行了内存分配—从而在内存分配问题上解放了开发者们。

//即便是老旧浏览器,也能正确回收元素和处理函数了

JS开发中很重要的一方面就是闭包:一个有权访问所包含于的外层函数中变量的内部函数。归因于JS运行时的实现细节,在如下方式中可能导致内存泄漏:

someElement.style.backgroundColor = 'blue';

内存中存储了很多东西:

此处,在编译时,编译器并不知道数组需要多少内存,因为这取决于用户的输入。

考虑以下场景:

可以在这篇文章中阅读追踪垃圾回收算法及其优化的更多细节。

周期中每一步的基本是这样的:

}, 5000); //大约每5秒执行一次

金沙棋牌官方平台 33

varunused= function () {

这个例子演示了定时器会发生什么:定时器引用了不再需要的节点或数据。

.操作系统和程序的所有代码

};

//换句话说,GC无法回收button元素

大部分内存管理问题都发生在这个阶段。

//做些什么

//所以JS并不分配新的内存,

}

double m; // 8 bytes

要全面理解动态内存分配如何工作,需要花费更多时间在指针上,可能有点太过背离本篇的主题了。

本文由金沙棋牌发布于金沙棋牌官方平台,转载请注明出处:JavaScript中的内存泄漏以及如何处理,JS的内存管

关键词: