一步一步重温 JavaScript 基础系列。
本章节将会理解JS的内存空间、垃圾回收机制、内存泄漏以及如何分析定位内存泄漏
内存空间
堆与栈
- 栈内存,结构特点是后进先出(LIFO)
- 堆内存,结构类似于书架,通过 key-value 的形式读取
变量的存放
- 基本类型,保存在栈内存中,占据内存空间大小固定,通过按值来访问
- 引用类型,其值大小不固定,保存在堆内存中,但其引用大小固定,保存在栈内存中。当查询引用类型的变量时,先从栈中读取内存地址,然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问
可以通过复制变量的值来理解如下:
内存空间管理
内存空间生命周期
- 分配所需的内存
- 使用分配的内存(读、写)
- 不需要时将其释放
垃圾回收机制
JS有自动垃圾收集机制GC(Garbage collection),因为开销较大,GC并非实时进行,而是定时周期性执行。所以,通过将变量赋值为null等方法将变量从当前执行环境释放时,并不会被立即回收清除。
在局部作用域中,当函数执行完毕后,执行栈就会将其推出,内部变量很容易被标记并回收。但是全局作用域以及闭包内的变量,什么时候释放回收,JS引擎很难判断,因此,在实际开发中,应尽量减少使用全局变量和闭包。
回收方式一:标记清除
- 当变量进入执行环境时,标记为“进入环境”,离开执行环境时,标记为“离开”;
- 垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式);
- 然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包);
- 而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了;
- 最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
回收方式二:引用计数
- 引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
内存泄漏
内存溢出,是指程序运行所需要的内存,超过剩余内存,常见如递归死循环等
内存泄漏,是指不再用到的内存,没有及时释放。
引起内存泄漏的几种情况
DOM元素的不恰当处理
- 原因:DOM元素被删除,但是仍然有变量引用该DOM,导致GC无法回收该变量
- 解决:DOM元素被删除时,将引用该DOM的变量,赋值为null,解除引用,待GC回收清除
1
2
3
4
5
6
7
8// 声明一个变量,保存DOM元素video
var video = document.getElmentById('#video');
// 将video从body中移除
document.querySelector('body').removeChild(video);
console.log(video);
// 打印结果仍然为DOM元素video意外的全局变量
- 原因:在函数作用域内,忘记使用var声明变量,或者是在this上新增属性,导致意外创建一个全局变量
- 解决:使用严格模式 ‘use strict’,避免此类错误发生
1
2
3
4
5
6
7
8
9
10function foo () {
// 忘记使用var声明变量,此时name成为全局变量
name = 'xug';
// 在this上新增age属性,由于foo由全局调用,此时this指向全局作用域window,故age成为全局作用域
this.age = 32;
}
foo()
// 如以上所示,即便foo被GC回收,name和age也不会被回收,由此造成内存泄漏被遗忘的计时器或回调函数
- 原因:setInterval()和 setTimeout()在使用完之后如果没有手动清除,会一直存在执行,占用内存
- 解决:定时器手动清除,且将引用定时器的变量值置为null
例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// setInterval
var timer1 = setInterval(function(){
console.log('111')
},1000)
clearInterval(timer1)
timer1 = null
// setTimeout
var timer2 = setTimeout(function(){
console.log('222')
},1000)
clearTimeout(timer2)
timer2 = null
闭包
- 原因:形成闭包后,内层函数会引用外层函数的变量,这样导致JS引擎无法回收此变量
- 解决:解除对匿名函数的引用
例如:1
2
3
4
5
6
7
8
9
10
11
12function fn () {
var num = 3;
return function () {
var n = 0;
console.log(++n);
console.log(++num);
}
}
var fn1 = fn();
fn1() // 1 4
fn1() // 1 5
一般情况下,在函数fn执行完后,就应该连同它里面的变量一同被销毁,但是在这个例子中,匿名函数作为fn的返回值被赋值给了fn1,并且匿名函数内部引用着fn里的变量num,所以变量num无法被GC回收,由此产生内存泄漏。而变量n是每次被调用时新创建的,所以每次fn1执行完后它就把属于自己的变量连同自己一起销毁。
只需将 fn1 = null, 即可释放对匿名函数的引用
如何分析内存泄漏
window.performance.memory 输出当前内存属性
- jsHeapSizeLimit: 内存大小限制
- totalJSHeapSize: 可使用的内存
- usedJSHeapSize: JS对象(包括V8引擎内部对象)已占用的内存
1
2
3
4
5
6window.performance.memory
MemoryInfo {
jsHeapSizeLimit: 4294705152,
totalJSHeapSize: 101622250,
usedJSHeapSize: 86986030
}
Chrome浏览器,录制 Performance-memory 查看各项指标变化
- JS Heap: 应用的内存占用量
- Documents: 当前页面中使用的样式或者脚本文件数目
- Nodes: 内存中 DOM 节点数目
- Listeners: 当前页面上注册的 JavaScript 事件监听器数量
如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。反之,则存在内存泄漏。
利用node命令行:process.memoryUsage(), 输出内存使用情况
- rss(resident set size):所有内存占用,包括指令区和堆栈。
- heapTotal:”堆”占用的内存,包括用到的和没用到的。
- heapUsed:用到的堆的部分。
- external: V8 引擎内部的 C++ 对象占用的内存