内存生命周期
1. 分配你所需的内存
2. 使用分配到的内存(比如进行读写操作)
3. 不需要时将其归还、释放
所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。 JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。
内存分配
- 为了不让程序员费心分配内存,JavaScript 在定义变量时就完成了内存分配。
var n = 123;
var s = "azerty";
var o = {
a: 1,
b: null
};
var a = [1, null, "abra"];
function f(a){
return a + 2;
}
someElement.addEventListener('click', function(){
someElement.style.backgroundColor = 'blue';
}, false);
var d = new Date();
var e = document.createElement('div');
var s = "azerty";
var s2 = s.substr(0, 3);
var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2);
使用值
使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
当内存不再需要使用时释放
高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。
- 大多数内存管理的问题都在这个阶段。
- 在这里最艰难的任务是找到“哪些被分配的内存确实已经不再需要了”。
- 它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。
内存泄漏
内存泄露概念
在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。 ——维基百科
- 程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存
- 对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。
- 否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
- 不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
内存泄露案例
全局变量:
function fun() {
text = "文本";
}
上述范例中的 text 时, 会把 text , 定义到全局对象中, 在浏览器中就是 window 上.
在页面中的全局变量, 只有当页面被关闭后才会被销毁. 所以这种写法就会造成内存泄露,
当然在这个例子中泄露的只是一个简单的字符串, 但是在实际的代码中, 往往情况会更加糟糕.
function fun() {
this.text = "文本";
}
fun();
在这种情况下调用 foo, this被指向了全局变量 window, 意外的创建了全局变量.
我们谈到了一些意外情况下定义的全局变量, 代码中也有一些我们明确定义的全局变量.
如果使用这些全局变量用来暂存大量的数据, 记得在使用后, 对其重新赋值为 null.
未销毁的定时器和回调函数
- 在很多库中, 如果使用了观察着模式, 都会提供回调方法, 来调用一些回调函数. 要记得回收这些回调函数.
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000);
如果后续 renderer 元素被移除, 整个定时器实际上没有任何作用.
但如果你没有回收定时器, 整个定时器依然有效,
不但定时器无法被内存回收, 定时器函数中的依赖也无法回收.
在这个案例中的 serverData 也无法被回收.
闭包
- 闭包:一个内部函数, 有权访问包含其的外部函数中的变量
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("message");
}
};
};
setInterval(replaceThing, 1000);
这段代码, 每次调用 replaceThing 时:
theThing 获得了包含一个巨大的数组和一个对于新闭包 someMethod 的对象.
同时 unused 是一个引用了 originalThing 的闭包。
这个范例的关键在于, 闭包之间是共享作用域的。
尽管 unused 可能一直没有被调用, 但是someMethod 可能会被调用, 就会导致内存无法对其进行回收.
当这段代码被反复执行时, 内存会持续增长.
DOM 引用
- 很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中
var elements = {
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
document.body.removeChild(document.getElementById('image'));
}
上述案例中, 即使我们对于 image 元素进行了移除, 但是仍然有对 image 元素的引用, 依然无法对齐进行内存回收.
内存泄漏识别
- Chrome 浏览器查看内存占用,按照以下步骤操作
1. 打开开发者工具,选择 Timeline 面板
2. 在顶部的Capture字段里面勾选 Memory
3. 点击左上角的录制按钮。
4. 在页面上进行各种操作,模拟用户的使用情况。
5. 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。
- 老版的图
- 新版图如下:
- 如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。
- 命令行可以使用 Node 提供的
process.memoryUsage 方法。
console.log(process.memoryUsage());
rss(resident set size):所有内存占用,包括指令区和堆栈。
heapTotal:"堆"占用的内存,包括用到的和没用到的。
heapUsed:用到的堆的部分。
external: V8 引擎内部的 C++ 对象占用的内存。
|