关于javascript:深入浅出JavaScript-GC-垃圾回收机制

7次阅读

共计 3585 个字符,预计需要花费 9 分钟才能阅读完成。

写作不易,未经作者容许禁止以任何模式转载!
如果感觉文章不错,欢送关注、点赞和分享!
继续分享技术博文,关注微信公众号 👉🏻 前端 LeBron
掘金原文

为什么须要垃圾回收

  • 在 C / C++ 中,跟踪内存的应用和治理内存对开发者来说是很大的累赘

    • JavaScript 是应用垃圾回收机制的语言,也就是说执行环境负责在代码执行时治理内存,帮开发者卸下了这个累赘
    • 通过主动内存治理实现内存的调配和资源的回收
    • 基本思路很简略,确定哪个变量不会再被应用了,把它的内存空间开释
    • 这个过程是周期性的,意思是这个垃圾回收程序每隔一段时间就会运行一次
  • 像 JS 中的对象、字符串、对象的内存是不固定的,只有真正用到的时候才会动静分配内存

    • 这些内存需在不应用后进行开释以便再次应用,否则在计算机可用内存耗尽后造成解体
  • 浏览器发展史上的垃圾回收法次要有

    • 援用计数法
    • 标记革除法

援用计数法

思路

  • 变量只是对值进行援用
  • 当变量援用该值时,援用次数 +1
  • 当该变量的援用被笼罩或者革除时,援用次数 -1
  • 当援用次数为 0 时,就能够平安地开释这块内存。
let arr = [1, 0, 1]   // [1, 0, 1]这块内存被 arr 援用  援用次数为 1
arr = [0, 1, 0]  // [1, 0, 1]的内存援用次数为 0 被开释  
                 // [0, 1, 0]的内存被 arr 援用   援用次数为 1
const tmp = arr  // [0, 1, 0]的内存被 tmp 援用   援用次数为 2 

循环援用问题

Netscape Navigator 3.0 采纳

  • 在这个例子中,ObjectA 和 ObjectB 的属性别离互相援用
  • 造成这个函数执行后,Object 被援用的次数不会变成 0,影响了失常的 GC。
  • 如果执行屡次,将造成重大的内存透露。
  • 而标记革除法令不会呈现这个问题。
function Example(){let ObjectA = new Object();
    let ObjectB = new Object();

    ObjectA.p = ObjectB;
    ObjectB.p = ObjectA;   

}

Example();
  • 解决办法:在函数完结时将其指向 null
ObjectA = null;
ObjectB = null;

标记革除法

为了解决循环援用造成的内存透露问题,Netscape Navigator 4.0 开始采纳标记革除法

到了 2008 年,IE、Firefox、Opera、Chrome 和 Safari 都在本人的 JavaScript 实现中采纳标记清理(或 其变体),只是在运行垃圾回收的频率上有所差别。

思路

  • 在变量进入执行上下文时打上“进入”标记
  • 同时在变量来到执行上下文时也打上“来到”标记

    • 从此以后,无法访问这个变量
    • 在下一次垃圾回收时进行内存的开释
function Example(n){
    const a = 1, b = 2, c = 3;
    return n * a * b * c;
}
// 标记 Example 进入执行上下文

const n = 1;  // 标记 n 进入执行上下文
Example(n);   // 标记 a,b,c 进入执行上下文
console.log(n); // 标记 a, b, c 来到执行上下文,期待垃圾回收

const 和 let 申明晋升性能

  • const 和 let 不仅有助于改善代码格调,同时有利于垃圾回收性能的晋升
  • const 和 let 使 JS 有了块级作用域,当块级作用域比函数作用域更早完结时,垃圾回收程序更早染指
  • 尽早回收该回收的内存,晋升了垃圾回收的性能

V8 引擎的垃圾回收

V8 引擎的垃圾回收采纳标记革除法与分代回收法

分为新生代和老生代

新生代

新生代垃圾回收采纳Scavenge 算法

调配给罕用内存和新调配的小量内存

  • 内存大小

    • 32 位零碎 16M 内存
    • 64 位零碎 32M 内存
  • 分区

    • 新生代内存分为以下两区,内存各占一半
    • From space
    • To space
  • 运行

    • 理论运行的只有 From space
    • To space 处于闲暇状态
  • Scavenge算法

    • 当 From space 内存应用将要达到下限时开始垃圾回收,将 From space 中的不可达对象都打上标记
    • 将 From space 的未标记对象复制到 To space。

      • 解决了内存散落分块的问题(不间断的内存空间)
      • 相当于用空间换工夫。
    • 而后清空 From space、将其闲置,也就是转变为 To space,俗称反转。
  • 新生代 -> 老生代

    • 新生代寄存的是新调配的小量内存,如果达到以下条件中的一个,将被调配至老生代

      • 内存大小达到 From space 的 25%
      • 经验了 From space <-> To space 的一个轮回

老生代

老生代采纳 mark-sweep 标记革除和 mark-compact 标记整顿

通常寄存较大的内存块和从新生代调配过去的内存块

  • 内存大小

    • 32 位零碎 700M 左右
    • 64 位零碎 1.4G 左右
  • 分区

    • Old Object Space

      • 字面的老生代,寄存的是新生代调配过去的内存。
    • Large Object Space

      • 寄存其余区域放不下的较大的内存,根本都超过 1M
    • Map Space

      • 寄存存储对象的映射关系
    • Code Space

      • 存储编译后的代码
  • 回收流程

    • 标记分类(三色标记)

      • 未被扫描,可回收,上面简称 1 类
      • 扫描中,不可回收,上面简称 2 类
      • 扫描实现,不可回收,上面简称 3 类
    • 遍历

      • 采纳深度优先遍历,遍历每个对象。
      • 首先将非根部对象全副标记为 1 类,而后进行深度优先遍历。
      • 遍历过程中将对象压入栈,这个过程中对象被标记为 2 类
      • 遍历实现对象出栈,这个对象被标记为 3 类
      • 整个过程直至栈空
    • Mark-sweep

      • 标记实现之后,将标记为 1 类 的对象进行内存开释
    • Mark-compact

      • 垃圾回收实现之后,内存空间是不间断的。
      • 这样容易造成无奈调配较大的内存空间的问题,从而触发垃圾回收。
      • 所以,会有 Mark-compact 步骤将未被回收的内存块整顿为间断地内存空间。
      • 频繁触发垃圾回收会影响引擎的性能,内存空间有余时也会优先触发 Mark-compact

垃圾回收优化

  • 增量标记

    • 如果用集中的一段时间进行垃圾回收,新生代倒还好,老生代如果遍历较大的对象,可能会造成卡顿。
    • 增量标记:使垃圾回收程序和利用逻辑程序交替运行,思维相似 Time Slicing
  • 并行回收

    • 在垃圾回收的过程中,开启若干辅助线程,进步垃圾回收效率。
  • 并发回收

    • 在逻辑程序执行的过程中,开启若干辅助线程进行垃圾回收,清理和主线程没有任何逻辑关系的内存。

内存泄露场景

全局变量

// exm1
function Example(){exm = 'LeBron'}

// exm2
function Example(){this.exm = 'LeBron'}
Example()

未革除的定时器

const timer = setInterval(() => {//...}, 1000)

// clearInterval(timer)

闭包

function debounce(fn, time) {
  let timeout = null; 
  return function () {if (timeout) {clearTimeout(timeout);
    }

    timeout = setTimeout(() => {fn.apply(this, arguments);
    }, time);
  };
}

const fn = debounce(handler, 1000); // fn 援用了 timeout

未革除的 DOM 元素援用

const element = {
    // 此处援用了 DOM 元素
    button:document.getElementById('LeBron'),
    select:document.getElementById('select')
}

document.body.removeChild(document.getElementById('LeBron'))

如何检测内存透露

这个其实不难,浏览器原带的开发者工具 Performance 就能够

  • 步骤

    • F12 关上开发者工具
    • 抉择 Performance 工具栏
    • 勾选屏幕截图和 Memory
    • 点击开始录制
    • 一段时间之后完结录制
  • 后果

    • 堆内存会周期性地调配和开释
    • 如果堆内存的 min 值在逐步回升则存在内存透露

优化内存应用

  1. 尽量不在 for 循环中定义函数
// exm
const fn = (idx) => {return idx * 2;}

function Example(){for(let i=0;i<1000;i++){//const fn = (idx) => {
        //    return idx * 2;
        // }
        const res = fn(i);
    }
}
  1. 尽量不在 for 循环中定义对象
function Example() {const obj = {};
  let res = "";
  for (let i = 0; i < 1000; i++) {
    // const obj = {
    //   a: i,
    //   b: i * 2,
    //   c: i * 3,
    // };
    obj.a = i;
    obj.b = i * 2;
    obj.c = i * 3;
    res += JSON.stringify(obj);
  }
  return res
}
  1. 清空数组
arr = [0, 1, 2]
arr.length = 0; // 清空了数组,数组类型不变
// arr = []  // 从新申请了一块空数组对象内存
  • 掘金原文
  • 掘金:前端 LeBron
  • 知乎:前端 LeBron
  • 继续分享技术博文,关注微信公众号 👉🏻 前端 LeBron
正文完
 0