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时立即回收
引用计数算法的优点:
-
发现垃圾及时回收
-
最大限度地减少程序中断
参考计数算法缺点:
-
循环引用的对象无法回收
-
时间成本大:引用计数必须维护引用变化,并且必须一直监控引用变化
标签去除的实现原理
-
核心思想:标记并删除两个步骤即可完成
-
遍历所有对象以查找标记的活动对象(可到达的对象)[第一步]
-
查看所有对象并清除未标记的对象,清除所有标记【第二步】
-
收回相应空间
标记清楚优点和缺点:
-
Udel:空间碎片
-
好处:修复循环引用不被回收的问题
-
不立即收集垃圾
标签排序算法原理
清除标记可以看作是对标记去除的改进
-
标签阶段的操作与标签移除一致
-
清理阶段将进行清理,移动物体位置
优点:减少空间碎片。不立即回收垃圾
垃圾收集机制
垃圾收集器 (GC)
V8将内存分为两部分,新生代内存空间和老年代内存空间:
新生代内存空间:用于存储存活时间相对较短的对象。大小在32位系统中为16M,在64位系统中为32M。新一代分为两个同等大小的房间,半空间(Fra)和半空间(To)。
老年代内存空间:用于存储生存时间比较长的对象,大小在32位系统中约为700M,在64位系统中约为1400M。
新一代的回收工艺
新一代被分为两个同等大小的房间,半空间(From)和半空间(To)。 “起始”房间正在使用中,而“终止”房间则处于非活动状态。当我们分配对象时,我们首先在模式空间中分配它们。当进行垃圾回收时,会使用标记算法将存活的对象复制到To空间,而未存活的对象将被释放。完成后,From空间和To空间将会切换。上述回收算法称为Scavenge算法。
多次复制后幸存的对象可以移至老年代存储区。这种现象称为促销。
当To空间使用率达到25%时,活动也会进行。
老年代对象的回收过程
老年代存储区会使用标记清除算法,首先遍历堆中的所有对象,并对存活的对象进行标记。然后删除未标记的对象。但标记清除算法会造成空间碎片,所以清除后会使用标记清除算法来整理老年代存储区的空间。
上述Scavenge、Tag Removal、Tag Sort算法在运行时都必须暂停应用逻辑,并在垃圾回收完成后恢复应用逻辑的执行。这种行为称为“完全突破”。在分代垃圾回收中,新生代的默认配置很小,剩余的对象很少。即使完全停止,影响也不会很大。但老年代的配置较大,剩余项目较多,所以一个时期影响会比较大。
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 的一个错误。应用闭包后,IE 仍然无法回收闭包中使用的变量。但这是IE问题,而不是闭包问题。
创建闭包通常相当于创建一个全局变量,使用后仍然会被回收。
function foo() {
let a = 1
function bar () {
console.log(a);
}
return bar ;
}
let testBar = foo();
testBar(); // 1
testBar = null; // 在testBar不再使用,将其重新赋值
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。