阅读 233

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

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.png

当程序运行到第二行代码的时候,会去栈内存中申请一块内存,地址A2,再去堆内存中申请一块内存存放objA 的值地址H1,将H1的地址存储到A2中, 如下图:

image_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.png

这时候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。

image_3.png

新生代回收过程

新生代又分为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不再使用,将其重新赋值
复制代码
文章分类
前端
文章标签