前言:
程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
内存:
简单理解,在硬件级别上,计算机内存由大量触发器组成。每个触发器包含几个晶体管,能够存储一个位。单个触发器可以通过唯一标识符寻址,因此我们可以读取和覆盖它们。因此,从概念上讲,我们可以把我们的整个计算机内存看作是一个巨大的位数组,我们可以读和写。
内存生命周期:
内存也是有生命周期的,一般可以按顺序分为三个周期:
- 分配期(分配所需要的内存)
- 使用期(使用分配到的内存(读、写))
- 释放期(不需要时将其释放和归还)
内存分配 -> 内存使用 -> 内存释放。
内存泄漏:
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
如果内存不需要时,没有经过生命周期的释放期,那么就存在内存泄漏。
内存管理机制:
JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。
JavaScript 内存管理机制和内存的生命周期是一一对应的。首先需要分配内存,然后使用内存,最后释放内存。
其中 JavaScript 语言不需要程序员手动分配内存,绝大部分情况下也不需要手动释放内存,对 JavaScript 程序员来说通常就是使用内存(即使用变量、函数、对象等)。
内存分配:
JavaScript 的内存是自动分配:
// 给数值变量分配内存
let number = 1;
// 给字符串分配内存
const string = 'natsu';
// 给对象及其包含的值分配内存
const object = {
a: 'natsu',
b: null
};
// 给数组及其包含的值分配内存(就像对象一样)
const array = [1, null, "natsu"];
// 给函数(可调用的对象)分配内存
function func(a){
return a;
}
内存使用:
使用值的过程实际上是对分配内存进行读取与写入的操作:
// 写入内存
number = 2;
// 读取 number 和 func 的内存,写入 func 参数内存
func(number);
内存回收:
Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。其原理是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大并且GC时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。
垃圾内存的两种回收方式:
引用计数:
用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存。
function test() {
var a = {}; // a指向对象的引用次数为1
var b = a; // a指向对象的引用次数加1,为2
var c = a; // a指向对象的引用次数再加1,为3
var b = {}; // a指向对象的引用次数减1,为2
}
缺点:循环引用的情况下无法清除变量。
function test() {
var a = {};
var b = {};
a.pro = b;
b.pro = a;
}
test();
标记清除:
js中最常用的垃圾回收方式就是标记清除。垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
function test(){
var a = 10 ; // 被标记 ,进入环境
var b = 20 ; // 被标记 ,进入环境
}
test(); // 执行完毕 之后 a、b又被标离开环境,被回收。
内存泄漏的场景:
JavaScript 的内存回收机制虽然能回收绝大部分的垃圾内存,但是还是存在回收不了的情况:
闭包:
function fn1(){
var n=1;
function fn2(){
alert(n);
}
return fn2();
}
fn1();
意外的全局变量:
// 在全局作用域下定义
function add(b) {
// a 相当于 window.a= 2;
a= 2;
return a + b;
}
定时器setTimeout setInterval:
不需要setInterval或者setTimeout时,定时器没有被clear,定时器的回调函数以及内部依赖的变量都不能被回收,造成内存泄漏。
clearTimeout(***)
clearInterval(***)
被遗忘的事件监听器:
window.addEventListener()添加事件后,如果页面不在需要进行监听可以使用removeEventListener()进行清除,例如mounted/created 钩子中使用 JS 绑定了 DOM/BOM 对象中的事件,需要在 beforeDestroy 中做对应解绑处理(vue)。
ES6 Set 成员:
// 造成内存泄漏用法(成员是引用类型的,即对象)
let map = new Set();
let value = { test: 22};
map.add(value);
value= null;
// 正确使用方式
let map = new Set();
let value = { test: 22};
map.add(value);
map.delete(value);
value = null;
// WeakSet 的成员是弱引用,更便捷
let map = new WeakSet();
let value = { test: 22};
map.add(value);
value = null;
ES6 Map 键名:
// 造成内存泄漏用法(key值是引用类型的,即对象)
let map = new Map();
let key = new Array(5* 10);
map.set(key, 1);
key = null;
// 正确使用方式
let map = new Map();
let key = new Array(5 * 10);
map.set(key, 1);
map.delete(key);
key = null;
// WeakMap 的键名是弱引用,更便捷
let map = new WeakMap();
let key = new Array(5 * 10);
map.set(key, 1);
key = null;
mounted/created 钩子中使用了$on,需要在beforeDestroy 中做对应解绑($off)处理:(vue)
beforeDestroy() {
this.bus.$off('****');
}
给DOM对象添加的属性是一个对象的引用:
var testObject = {};
document.getElementById('xxx').property = testObject;
//释放内存
window.onunload=function(){
document.getElementById('xxx').property = null;
};
DOM对象与JS对象相互引用:
死循环等:
内存泄漏排查:
确定是否是内存泄漏问题
打开谷歌开发者工具,切换至 Performance 选项,勾选 Memory 选项,在页面上点击运行按钮,然后在开发者工具上面点击左上角的录制按钮,通过内存走势图判断当前页面是否有内存泄漏。
查找内存泄漏出现的位置:
打开谷歌开发者工具,切换至 Memory 选项。页面上点击运行按钮,然后点击开发者工具左上角录制按钮,录制完成后继续点击录制,直至录制完三个为止。然后点击页面的停止按钮,再连续录制 3 次内存(不要清理之前的录制)。
按照 Shallow Size 进行逆序排序,对内存占用较高的变量进行排查:
总结:
了解相关的内存知识,才能更好的合理使用内存,使得页面更加可靠、稳定。在代码编写过程中注意相关的内存泄漏的场景,虽然现在浏览器的垃圾回收机制已经大大优化了,但是养成良好的编写代码对于我们编写高性能页面同等重要。