Code前端首页关于Code前端联系我们

合理利用内存的Javascript垃圾回收机制

terry 2年前 (2023-09-08) 阅读数 156 #Vue

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中,如下图

image.pngimage.png

当程序运行到第二行代码时,会在栈内存中查找一块内存,地址A2,然后在堆内存中查找一块内存,存放objA的值地址H1 ,并将H1的地址存入A2中。如下图:

image_1.pngimage_1.png

当程序运行到第三行代码时,就会从内存中读取a的值并输出。

当程序运行到第四行代码时,就会去内存中读取A2的值,找到H1的值,并输出。

如果a以后不再使用,objA可以收回分配的空间A1、A2和H1。

以上是内存的生命周期:

分配内存→使用内存→回收内存

需求此时如果后续有如下代码:

// 第四行
let objB = {
  name: 'peter'
}
// 第五行
objA = objB 
 

当程序运行到第四行代码时,就会在栈内存中查找一块内存,地址A3,然后在堆内存中查找一块内存,存放objA的值地址H2 ,并将H2的地址存入A3

当程序执行到第五行时,A2中的地址将变为H2。如下图:

image_2.pngimage_2.png

此时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。

image_3.pngimage_3.png

新一代的回收工艺

新一代被分为两个同等大小的房间,半空间(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不再使用,将其重新赋值
 

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

上一篇:预编译 JavaScript 下一篇:JS声明的推广

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门