简介
内存泄漏是每个开发者都必须面对的终极问题。它是许多问题的根源:响应缓慢、崩溃、高延迟和其他应用程序问题。
什么是内存泄漏?
本质上,内存泄漏可以定义为:当应用程序不再需要占用内存时,由于某种原因,内存没有被操作系统或可用内存池回收。编程语言以不同的方式管理内存。只有开发人员最清楚哪些内存是不需要的,操作系统可以回收它。一些编程语言提供了语言特性,可以帮助开发者做这样的事情。其他人希望开发人员需要清楚内存。
JavaScript 内存管理
JavaScript 是一种垃圾收集语言。垃圾收集语言通过定期检查之前分配的内存是否可达来帮助开发者管理内存。换句话说,垃圾收集语言缓解了“内存仍然可用”和“内存仍然可用”的问题。两者的区别很微妙也很重要:只有开发者知道未来哪些内存会被使用,不可访问的内存由算法确定和标记,并由操作系统适时回收。
JavaScript 内存泄漏
垃圾收集语言中内存泄漏的主要原因是不需要的引用。在理解之前,你还需要了解垃圾收集语言是如何区分内存可达性和不可达性的。
标记和清除
大多数垃圾收集语言使用的算法称为Mark-and-sweep。该算法包括以下步骤:
垃圾收集器创建了一个“根”列表。根通常是对代码中全局变量的引用。在 JavaScript 中,“window”对象是一个全局变量,被视为根。 window对象一直存在,所以垃圾收集器可以检查它和它的所有子对象是否存在(即不是垃圾);
所有的根都被检查并标记为活动的(即不是垃圾)。所有子对象也被递归检查。如果从 root 开始的所有对象都可以访问,则它们不会被视为垃圾。
所有未标记的内存将被视为垃圾,收集器现在可以释放内存并将其返回给操作系统。
现代垃圾收集器改进了算法,但本质是一样的:标记可达内存,剩下的当作垃圾处理。
不必要的引用意味着开发者知道不再需要内存引用,但由于某种原因,它仍然留在活动的根树中。在 JavaScript 中,不需要的引用是保存在代码中的变量。它不再需要,但指向一块应该释放的内存。有些人认为这是开发者的错。
为了了解 JavaScript 中最常见的内存泄漏,我们需要了解哪种引用方式容易忘记。
三种常见的 JavaScript 内存泄漏
1:意外的全局变量
JavaScript 更松散地处理未定义变量:未定义变量将在全局对象中创建一个新变量。在浏览器中,全局对象是window。
function foo(arg) {
bar = "this is a hidden global variable";
} 事实是:
function foo(arg) {
window.bar = "this is an explicit global variable";
} 忘记在函数 foo 中使用 var,并且不小心创建了一个全局变量。这个例子展示了一个简单的字符串,它是无害的,但也有更糟糕的情况。
另一个意外的全局变量可能由此产生:
function foo() {
this.variable = "potential accidental global";
}
// Foo 调用自己,this 指向了全局对象(window)
// 而不是 undefined
foo(); 在 JavaScript 文件的头部添加“use strict”可以防止此类错误的发生。启用严格模式来解析 JavaScript 以避免意外的全局变量。
关于全局变量的注意事项
虽然我们讨论了一些意想不到的全局变量,但仍然存在一些由显式全局变量产生的垃圾。它们被定义为不可回收(除非它们被定义为空的或重新分配的)。尤其是在使用全局变量临时存储和处理大量信息时,需要格外小心。如果必须使用全局变量来存储大量数据,请务必将其设置为 null 或在使用后重新定义。与全局变量相关的内存消耗增加的主要原因之一是缓存。缓存数据是为了重用,缓存必须有上限才可以使用。高内存消耗导致缓存超过上限,因为缓存内容无法回收。
2:忘记定时器或回调函数
在 JavaScript 中使用 setInterval 是很常见的。一段常见的代码:
var someResource = getData();
setInterval(function() {
var node = document.getElementById(Node);
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000); 这个例子说明了什么:不再需要与节点或数据关联的定时器,可以删除节点对象,不再需要整个回调函数。但是定时器回调函数仍然没有被回收(定时器直到停止才会被回收)。同时,如果某些资源存储了大量数据,则无法回收。
对于观察者示例,一旦不再需要它们(或关联的对象变得不可访问),明确删除它们很重要。旧的 IE 6 无法处理循环引用。如今,即使没有明确移除,一旦观察者对象变得不可访问,大多数浏览器都可以收回观察者处理功能。
观察者代码示例:
var element = document.getElementById(button);
function onClick(event) {
element.innerHTML = text;
}
element.addEventListener(click, onClick); 关于对象观察者和循环引用的注意事项
老版本的 IE 无法检测 DOM 节点和 JavaScript 代码之间的循环引用,这会导致内存泄漏。如今,现代浏览器(包括 IE 和 Microsoft Edge)使用更先进的垃圾收集算法,可以正确检测和处理循环引用。也就是说,在回收节点内存时,不必调用removeEventListener。
3: 来自 DOM 的引用
有时,保存 DOM 节点的内部数据结构很有用。如果您想快速更新表的几行,将 DOM 的每一行保存为字典(JSON 键值对)或数组是有意义的。这时,同一个DOM元素有两个引用:一个在DOM树中,另一个在字典中。当您决定以后删除这些行时,您需要清除这两个引用。
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 代码中保存了对其中一个表的引用。当你决定以后删除整个表时,你直觉地认为GC会回收除了保存的节点之外的其他节点。实际情况并非如此:这是表的一个子节点,引用了子元素和父元素。由于代码保留了引用,整个表都保留在内存中。保存对 DOM 元素的引用时要小心。
4:
闭包是 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); 代码片段做了一件事:每次调用replaceThing时,theThing都会得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,unused 变量是一个引用 originalThing 的闭包(之前的 replaceThing 称为 theThing)。你的想法很混乱吗?最重要的是,一旦创建了闭包的作用域,它们就拥有相同的父作用域,并且作用域是共享的。 someMethod 可以通过 Thing 使用。 SomeMethod 与未使用的共享闭包范围。尽管未使用过从未使用过,但它引用的原始事物强制它保留在内存中(以防止它被回收)。当这段代码反复运行时,你会看到内存使用量不断上升,而垃圾收集器(GC)无法减少内存使用量。本质上已经创建了闭包的链表,每个闭包作用域都携带了一个大数组的间接引用,造成了严重的内存泄漏。
说明如何解决此问题。在 replaceThing 末尾添加 originalThing = null。

Chrome 内存分析工具概述
Chrome 提供了一组很棒的工具来检测 JavaScript 内存使用情况。与记忆相关的两个重要工具:时间轴和配置文件。
时间线

timeline 可以检测代码中不必要的内存。在此屏幕截图中,我们可以看到潜在泄漏对象的稳步增长。数据采集即将结束时,内存使用量明显高于采集开始时,节点总数也较高。各种迹象表明代码中存在DOM节点泄漏。

Profiles 是一个工具,你可以花很多时间去关注。它可以保存快照,比较不同的 JavaScript 代码内存使用快照,并记录时间分配。每个结果都包含不同类型的列表。与内存泄漏相关的是汇总列表和比较列表。
汇总列表显示了不同类型对象的分配和总大小:浅层大小(特定类型的所有对象的总大小)、保留大小(浅层大小加上与此相关的其他对象的大小)。它还提供了一个对象和关联的 GC 根之间距离的概念。
比较不同快照的对比列表,找出内存泄漏。
示例:使用 Chrome 查找内存泄漏
基本上有两种类型的泄漏:由周期性内存增长引起的泄漏和偶尔的内存泄漏。显然,周期性内存泄漏很容易发现;偶尔的泄漏是棘手的,通常很容易被忽视。偶尔出现可能被认为是一个优化问题,而周期性出现被认为是一个必须解决的错误。
以Chrome文档中的代码为例:
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 中,并为全局变量分配一个巨大的数组。上面提到的工具可以检测到内存的稳定增长。
找到周期性增长的内存
时间轴标签擅长于此。在 Chrome 中打开示例,打开 Dev Tools,切换到时间线,检查内存并单击录制按钮,然后单击页面上的按钮按钮。稍等片刻停止录制,看看效果:

图中有两个迹象表明存在内存泄漏,Nodes(绿线)和JS heap(蓝线)。节点稳步增长,没有下降,这是一个重要的迹象。
JS heap 的内存使用量也在稳步增长。由于垃圾收集器的影响,并不是那么容易找到。从图中可以看出内存使用量上下波动。实际上,每次减少之后,JS 堆的大小都比以前更大。换句话说,虽然垃圾收集器继续收集内存,但内存会定期泄漏。
在确认存在内存泄漏后,我们寻找根本原因。
保存两个快照
切换到Chrome Dev Tools的profiles选项卡,刷新页面,页面刷新完成后,点击Take Heap Snapshot,将快照保存为benchmark。然后再次单击按钮按钮,等待几秒钟,然后保存第二个快照。

在过滤器菜单中选择摘要,在右侧选择快照1和快照2之间分配的对象,或在过滤器菜单中选择比较,然后您可以看到一个比较列表。
在这个例子中,很容易找到内存泄漏。查看(字符串)大小 DeltaConstructor,8MB,58 个新对象。新对象已分配但未释放,占用8MB。
如果你展开(string)Constructor,你会看到很多单独的内存分配。选择单一分配,下面的保持器会引起我们的注意。

我们选择的分配是数组的一部分,它与 window 对象的 x 变量相关联。这显示了从巨大对象到无法回收的根(窗口)的完整路径。我们已经找到了潜在的泄漏及其来源。
我们的例子相当简单,只有少量的DOM节点被泄露,使用上面提到的快照很容易找到。对于较大的网站,Chrome 还提供了 Record Heap Allocations 功能。
记录堆分配发现内存泄漏
返回 Chrome Dev Tools 的配置文件选项卡,然后单击 Record Heap Allocations。工具运行时,注意顶部的蓝色条,代表内存分配,每秒都有大量内存分配。运行几秒钟后停止。

该工具的杀手锏特性如上图所示:选择某个时间轴,可以看到该时间段内的内存分配情况。选择一个尽可能接近峰值的时间线。下面的列表只显示了三个构造函数:一个是最泄漏的(字符串),其次是关联的 DOM 分配,最后一个是 Textconstructor(包含在 DOM 叶节点中的文本)。
从列表中选择一个 HTMLDivElementconstructor,然后选择 Allocation stack。

现在您知道元素的分配位置(grow->createSomeNodes)。仔细看图中的timeline,发现HTMLDivElementconstructor被调用了很多次,说明内存已经被占用,不能被GC回收。我们知道这些对象被分配的确切位置(createSomeNodes)。回到代码本身并讨论如何修复内存泄漏。
另一个有用的功能
在堆分配的结果区,选择Allocation。

此视图显示了与内存分配相关的函数列表。我们立即看到了grow 和createSomeNodes。选择grow的时候,查看相关的对象构造函数,可以清楚的看到(string), HTMLDivElement, Text都被泄露了。
结合上面提到的工具,你可以很容易地发现内存泄漏。
每日一道前端题,带你走上高级前端之路!每天早上9点左右更新前一天的问题和答案!
github地址:
推荐一个网页程序员必备的微信帐号
▼
网络夜读课
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-380824-1.html
你是怎么算出来这结果的