关于js的内存管理机制一直认知较为模糊,之前只知道,当我们创建一个变量的时候,系统为它开辟了一个内存空间,但是我有时候会想,在开发过程中,我们创建了那么多变量,那么系统开辟的内存是否有区别呢?怎样做能更适应js的垃圾回收机制?在同样可以完成功能的前提下,是创建多个基础数据类型数据更好,还是创建一个引用数据更好呢?
首先我们需要了解js内存的生命周期,当我们创建变量、函数或其他东西时,js引擎会自动为它分配一个内存,而当它不被使用时,由于js的垃圾回收机制,js引擎会自动释放掉这块内存(是怎么回收的后续会有解释),大概流程:分配内存-->使用内存-->释放内存。
一、分配内存
js的虚拟空间分为栈内存和堆内存,开发者在创建变量或函数等需要内存占用时,系统会根据其数据类型自动为其分配一个属于它的内存。
栈内存 | 堆内存 | |
存放类型 | 存放基础数据类型(Number、String、Boolean、Null、Undefind、Symbol)和引用数据类型的指针 | 存放引用数据类型(对象、数组) |
存取方式 | 先进后出,后进先出 | 无规律,按指针取 |
内存大小 | 固定 | 不固定 |
参考模型 | 乒乓球盒模型 | 书架模型 |
容量 | 小 | 大 |
举个例子:
let a1 = 0;// 栈内存
let a2 = 'this is string';// 栈内存
let a3 = null;// 栈内存
let b = { m: 20 };// b指针存放在栈内存中,{ m: 20 }存放在堆内存中
let c = [1, 2, 3];// c指针存放在栈内存中,[1, 2, 3]存放在堆内存中
到这里,我纠结了半天栈和队列有什么关系,我在想,栈和堆是开发者创建变量的时候系统为其开辟的内存,那队列呢?为什么网上那么多栈和队列的区别,后来,我发现,栈和堆分为内存中的和数据结构中的,网上对比的是数据结构中的栈和队列,而此处是内存中的栈和队列,好吧,是我钻牛角尖了。
二、使用内存
使用内存也就是对创建的变量或函数的使用,从内存的角度来看变量的使用、引用、拷贝都会简单许多。
拷贝问题
比如,经常会有两道面试题作对比:
var a = 1
var b = a
b=2
console.log(a) //此时a是多少 =>a为1
var c = [1,2,3]
var d = c
d[0] = 4
console.log(c) //此时c是多少 =>c为[4,2,3]
① b拷贝了a,d拷贝了c;
② 改变b时a未被改变,改变d时c被改变;
产生这一现象的原因就可以理解为只有存在栈内存的东西才能被拷贝,a为基本数据类型,b拷贝a 后,二者就是一个完全独立的值了,他们分别呗存放在栈中,互不影响;而c是引用数据类型,d拷贝c后,由于栈内存中存放的是引用类型的指针,所以d拷贝出了另一个指针而已,但他们指向的还是同一个堆内存中存放的值,所以改变 其中一个值,另一个也会相应改变。
第一个:
第二个:
三、释放内存
垃圾回收机制
js有一个自动的垃圾回收机制,简称GC,通过一些回收算法找到不再被使用的变量垃圾回收掉。由于js是单线程的,所以在系统进行垃圾回收的时候,原有的程序逻辑就会停止,也就是常说的全停顿,所以,垃圾回收过程并不是实时的,而是在cpu空闲的时候进行的。
现有的回收算法包括:引用计数算法、标记清除算法、标记整理、分代回收
(1)引用计数算法
也就是看这个变量被引用的次数,为0时就算做垃圾。
① 创建变量a,被赋值一个引用类型的数据时,a次数为1;
② 创建变量b,把a赋值给b时,a次数+1;
③ 把a赋值为null时,a次数为0
let a = {
name: "小陈",
age: 20
}; //此时该对象的引用计数标记为1(a 引用)
let b = a; //此时对象的引用计数标记为2(a、b 引用)
a = null; //此时对象的引用计数标记为1((b 引用))
b = null; //此时对象的引用计数标记为0(无变量引用)
... //等待GC 回收此对象
但是它有个很严重的缺点就是,可能会有嵌套循环的问题,也就是说如果两个变量互相赋值,那么会影响它的引用计数,执行多次则会引起内存泄漏,这时候就需要手动赋值为null,使其切断引用关系;
(2)标记清除算法
假设内存中的所有对象都是垃圾,全部标记为0,然后遍历各个根对象,把不是垃圾的节点改成1,把所有标记为0的垃圾清理掉,销毁并回收他们所占的空间,最后把所有标记修改为0,等待下一轮的回收。
由于在清理垃圾后,剩余对象的位置不变,所以垃圾回收后可能会导致内存空间不连续,出现内存碎片的问题。
(3)标记整理算法
它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存。
(4)分代回收法
针对不同对象采用不同算法:
新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象。
老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象
内存泄漏
内存泄漏也就是指由于疏忽或错误造成程序未能释放已经不再使用的内存,不再用到的内存却没有及时释放,从而造成内存上的浪费,几种常见的内存泄漏:
(1)隐式全局变量引起
在函数中定义的局部变量,等到函数执行完毕就没有存在的必要了,GC机制会识别并回收;但是对于全局变量,垃圾回收器很难判断这些变量什么时候才不被需要,所以全局变量通常不会被回收,从而无限制的增长缓存,所以在创建变量时尽量少使用全局变量。
但是有的时候,会出现意外的全局变量。
情况一:
function test(){
//此时没有用var、let或const声明a,也没有开头添加‘use strict',等价于window.a,导致a变成了全局变量
a=2;
}
情况二:
function test(){
//函数内部的this指向window,等价于window.a,导致a变成了全局变量
this.a=2;
}
(2)闭包引起
闭包函数简单来讲就是函数用了外部的变量,被引用时,由于函数内部没有变量作用域,使得变量一直被引用,无法被垃圾回收器回收,从而导致内存泄漏。
举个例子:
function bibao(){
const a=1;
return function bibao2(){ //bibao2就是一个闭包函数
return a;
}
}
let b=bibao()();//在这里b调用bibao(),bibao()返回一个函数,在该函数中return了a,函数内又没有a,需要从外部引用a,导致a无法进行垃圾回收,造成了内存泄漏
解决办法:
function bibao(){
const a=1;
return function bibao2(){
return a;
}
a=null;//函数结束时手动释放
}
let b=bibao();
(3)DOM之外的引用引起
<div id="root">
<ul id="ul">
<li></li>
<li></li>
<li id="li"></li>
<li></li>
</ul>
</div>
<script>
let root = document.querySelector('#root')
let ul = document.querySelector('#ul')
let li = document.querySelector('#li')
// 由于ul变量存在,整个ul及其子元素都不能GC
root.removeChild(ul)
// 虽置空了ul变量,但由于li变量引用ul的子节点,所以ul元素依然不能被GC
ul = null
// 已无变量引用,此时可以GC
li3 = null
</script>
(4)未清除的定时器引起
在使用定时器之后,没有清除也会造成内存泄漏,所以当不需要 interval 或者 timeout 时,应调用 clearInterval 或者 clearTimeout来清除。
内存溢出
在程序运行的时候,如果程序所需要的内存大于剩余内存(机器能提供给你的内存),就会抛出内存溢出的错误。