JavaScript中的数据类型分为基本类型(number/string/boolean/undefined/null/symbol/bigInt)和引用类型(Object),为什么要区分数据类型呢?
因为它们在内存中的存储是不同的。
JavaScript中的内存模型
// 第一行
let a = 0
// 第二行
let objA = {
name: 'jill'
}
// 第三行
console.log(a)
console.log(objA)
......
复制代码
当程序运行到第一行代码的时候,会在栈内存中申请一块内存,地址A1,为变量a 创建一个唯一的标识符a,a与内存地址A1形成映射关系,将a的值存放到A1中, 如下图
当程序运行到第二行代码的时候,会去栈内存中申请一块内存,地址A2,再去堆内存中申请一块内存存放objA 的值地址H1,将H1的地址存储到A2中, 如下图:
当程序运行到第三行代码的时候,会去内存中读取a的值,并输出出来。
当程序运行到第四行代码的时候,会去内存中读取A2的值,找到H1的值,并输出出来。
如果后续没有使用到a, objA就可以对这分配的这些空间A1、A2、H1进行回收。
以上就是内存的生命周期:
分配内存→使用内存→回收内存
假设 这时候如果后续还有如下代码:
// 第四行
let objB = {
name: 'peter'
}
// 第五行
objA = objB
复制代码
当程序运行到第四行代码的时候,会去栈内存中申请一块内存,地址A3,再去堆内存中申请一块内存存放objA 的值地址H2,将H2的地址存储到A3中
程序执行到第五行的时候,会将A2中的地址改为H2。如下图:
这时候H1就没有方式可以访问到了,称为不可达对象,也就“垃圾”。假设有一百、一千个、一万个……这样的不可达对象在内存中,会占用内存,后续程序无法申请内存,造成内存泄漏…..要对H1这种“垃圾”进行回收。
V8内存限制
JavaScript一般是在浏览器端运行的,回收机制大同小异,我们这里以V8引擎为例来说明。
在32位的系统中,V8的内存是0.7GB;在64位的系统中,V8的内存是1.4GB。
那为什么会限制内存呢?
首先,在设计之初,JavaScript仅仅作为脚本语言在浏览器端运行,不可能遇到使用大量内存的情况。
另一方面JavaScript垃圾回收,与JavaScript代码运行时不能同步进行的,意思就是JavaScript进行垃圾回收的时候,JavaScript会暂停执行。如果内存过大,会导致垃圾回收的时间过长,影响用户体验。
常见的垃圾回收算法
首先我们了解常见的垃圾回收算法:
引用计数
-
核心思想: 设置引用数,判断当前引用数是否为0
-
引用计算器
-
引用关系改变是修改引用数字
-
引用数字为0时立即回收
引用计数算法优点:
-
发现垃圾时及时回收
-
最大限度减少程序暂停
引用计数算法缺点:
-
无法回收循环引用的对象
-
时间开销大:引用计数需要维护引用变化,需要时刻监控引用变化
标记清除实现原理
-
核心思想:分标记和清除两个阶段完成
-
遍历所有对象找标记活动对象(可达对象)[第一阶段]
-
遍历所有对象清除没有标记的对象,把标记都清除[第二阶段]
-
回收相应空间
标记清除优缺点:
-
缺点:造成空间碎片化
-
优点:解决循环引用不能回收的问题
-
不会立即回收垃圾
标记整理算法原理
-
标记整理可以看作是标记清除的增强
-
标记阶段的操作和标记清除一致
-
清除阶段会执行整理,移动对象位置
优点: 减少空间碎片化 不会立即回收垃圾
垃圾回收机制
垃圾回收(garbage collector(GC))
V8将内存分为两个部分,新生代内存空间和老生代内存空间:
新生代内存空间:用于存放存活时间比较短的对象,在32位系统中大小是16M,64位系统中大小是32M。新生代又分为semispace(From)和semispace(To)两个等大的空间。
老生代内存空间:用于存放存活时间比较长的对象,在32位系统中大小约为700M,64位系统中大小约为1400M。
新生代回收过程
新生代又分为semispace(From)和semispace(To)两个等大的空间,From空间处于使用状态,To空间处于空闲状态。当我们分配对象时,先是在Form空间进行分配,当进行垃圾回收时,会使用标记整理算法,将存活的对象复制到To空间,而非存活的对象会释放掉。完成之后会将From空间和To空间进行对换。以上这种回收算法称为Scavenge算法。
经过多次复制之后依然的存活的对象,可以移动到老生代存储区, 这个现象称为晋升。
当To空间的使用率达到25%时,也会发生晋升。
老生代对象的回收过程
老生代存储区会采用标记清除算法,首先遍历堆中所有对象,对存活的对象进行标记。然后清除未标记的对象。但是标记清除算法会导致空间碎片化,所以清除之后会使用标记整理算法对老生代存储区空间进行整理。
以上Scavenge算法,标记清除,标记整理算法在运行的时候都需要将应用逻辑暂停下来,等垃圾回收完毕之后在回复应用逻辑执行,这种行为被称为“全停顿”。在垃圾分代回收中,新生代默认配置较小,存活对象少,即使全停顿影响也不大。但是老生代配置大,存活对象多,全停顿会造成较大的影响。
V8后续才用了增量标记,就是将标记的过程拆分为多个小过程,穿插进行。
高效利用内存
了解了JavaScript的垃圾回收机制,那我们在写代码的过程中,能做些什么让垃圾回收机制更高效的工作呢?
分为以下几个方面:
1、主动释放变量
如果是定义在全局对象上的变量,由于全局作用域需要程序退出才会释放对象,此时会导致引用的对象常驻内存(常驻在老生代中)。
以下为示例:
// 在浏览器环境下
this.foo = 1 // this指向window对象
window.addEventListener('keydown', keydownFun) // 绑定事件到全局
let obj = {name: 'jill'}
let timer = setTimeout(() =>{
let a = 1
let b = obj
// a、b、 obj定时器内使用的变量也不会被回收
}, 10) // 创建定时器没销毁
复制代码
对于以上情况,我们使用完了之后 需要主动去释放变量,下次垃圾的时候会及时进行回收。
delete this.foo // 使用delete删除释放 对V8的优化有影响,建议使用下面这种
this.foo = undefined // 重新赋值释放
window.removeEventListener('keydown', keydownFun) // 使用完毕对全局事件进行解绑
clearTimeout(timer) // 主动销毁定时器
timer = null // 解除定时器的引用
复制代码
2、闭包
闭包会造成内存泄漏吗?闭包不会弹出调用栈 ,且常驻内存吗?
其实不是的,这是一个误区,原来IE中有bug,闭包使用完之后,IE依然无法回收闭包中使用的变量。但是这个是IE的问题,并不是闭包的问题。
正常创建闭包,相当于创建了一个全局变量,使用完了之后还是会进行回收的。
function foo() {
let a = 1
function bar () {
console.log(a);
}
return bar ;
}
let testBar = foo();
testBar(); // 1
testBar = null; // 在testBar不再使用,将其重新赋值
复制代码