乐趣区

关于浏览器:图解-Google-V8事件循环和垃圾回收学习笔记三

这是《图解 Google V8》第三篇 / 共三篇:事件循环和垃圾回收

这里次要讲了 2 点:

  1. 事件循环:宏工作和微工作

    • 什么是微工作
    • 微工作的执行机会
  2. 垃圾回收

    • 垃圾回收运行过程
    • 垃圾回收算法

通过这个专栏的学习,V8 不在是个生疏的黑盒了,变成了一个相熟的黑盒,因为这个专栏让你理解了 V8 的大抵原理,面试时吹吹牛皮还是能够的,不过也就仅此而已,细节方面还须要本人去深刻

17 | 音讯队列:V8 是怎么实现回调函数的?

  • 同步回调函数是在执行函数外部被执行的
  • 异步回调函数是在执行函数内部被执行的

UI 线程是运行窗口的线程,也叫主线程

当鼠标点击了页面,零碎会将该事件交给 UI 线程来解决,然而 UI 线程不能立刻响应来解决

针对这种状况,浏览器为 UI 线程提供了音讯队列,而后 UI 线程会一直的从音讯队列中取出事件和执行事件,如果以后没有任何音讯期待被解决,那么这个循环就会被挂起

setTimeout

在执行 setTimeout,浏览器会将回调函数封装成一个事件,增加到音讯队列中,而后 UI 线程会不间断的从音讯队列中取出工作,执行工作,在适合的机会取出 setTimeout 的回调函数

XMLHttpRequest

UI 线程执行 XMLHttpRequest,会阻塞 UI 线程,所以 UI 线程会将它调配给网络线程(是网络过程中的一个线程):

  1. UI 线程从音讯队列中取出工作,剖析
  2. 发现是一个下载工作,就会交给网络线程去执行
  3. 网络线程接到下载申请后,会和服务器建立联系,收回下载申请
  4. 网络线程一直从服务器接收数据
  5. 网络申请在收到数据后,会将返回的数据和回调函数封装成一个事件,放在音讯队列中
  6. UI 线程循环读取音讯队列,如果是下载状态的事件,UI 线程就会执行回调函数
  7. 直到下载事件完结,页面显示下载实现

18 | 异步编程(一):V8 是如何实现微工作的?

宏工作是音讯队列中期待被主线程执行的事件,每个宏工作在执行的时候都会创立栈,宏工作完结,栈也会被清空

微工作是一个须要异步执行的函数,执行机会是在主函数执行完结之后,以后宏工作完结之前

微工作执行的机会:

  1. 如果当前任务中产生了一个微工作,不会再以后的函数中被执行,所以执行微工作时,不会导致栈的有限扩张
  2. 微工作会在当前任务执行完结之前被执行
  3. 微工作完结执行之前,不会执行其余的工作

参考资料

  1. V8 Promise 源码全面解读
  2. JavaScript Event Loop vs Node JS Event Loop

19 |异步编程(二):V8 是如何实现 async/await 的?

生成器 Generator

带星号的函数配合 yield 能够实现函数的暂停和复原,这个叫生成器

function* getResult() {console.log("getUserID before");
  yield "getUserID";
  console.log("getUserName before");
  yield "getUserName";
  console.log("name before");
  return "name";
}

let result = getResult();

console.log(result.next().value);
console.log(result.next().value);
console.log(result.next().value);

在生成器外部,如果遇到 yield 关键词,那么 V8yield 前面的内容返回给内部,并暂停函数的执行

生成器暂停后,里面代码开始执行,如果想要持续复原生成器的执行,就能够应用 result.next() 办法

在暂停和复原之间切换,这背地的原理是协程,协程是比线程更加轻量级,它是跑在线程上的工作

一个线程有多个协程,但只能执行一个协程,如果 A 协程启动 B 协程,那 A 协程就是 B 协程的父协程

应用生成器编写同步代码

function* getResult() {let id_res = yield fetch(id_url);
  console.log(id_res);
  let id_text = yield id_res.text();
  console.log(id_text);

  let new_name_url = name_url + "?id=" + id_text;
  console.log(new_name_url);

  let name_res = yield fetch(new_name_url);
  console.log(name_res);
  let name_text = yield name_res.text();
  console.log(name_text);
}
let result = getResult();
result
  .next()
  .value.then((response) => {return result.next(response).value;
  })
  .then((response) => {return result.next(response).value;
  })
  .then((response) => {return result.next(response).value;
  })
  .then((response) => {return result.next(response).value;
  });

把执行生成器代码的函数称为执行器(可参考驰名的 co 框架)

function* getResult() {let id_res = yield fetch(id_url);
  console.log(id_res);
  let id_text = yield id_res.text();
  console.log(id_text);

  let new_name_url = name_url + "?id=" + id_text;
  console.log(new_name_url);

  let name_res = yield fetch(new_name_url);
  console.log(name_res);
  let name_text = yield name_res.text();
  console.log(name_text);
}
co(getResult());

async/await

async 是异步执行并隐式返回 Promise 作为后果的函数。

await 前面能够接两种类型的表达式:

  • 任何一般表达式
  • Promise 对象的表达式

如果 await 期待的是一个 Promise 对象,它会暂停执行生成器函数,直到 Promise 对象变成 resolve 才会复原执行,而后 resolve 的值作为 await 表达式的运算后果

function NeverResolvePromise() {return new Promise((resolve, reject) => {});
}
function ResolvePromise() {return new Promise((resolve, reject) => resolve("resolve"));
}
async function getResult() {let a = await NeverResolvePromise();
  console.log(a); // 不会输入
}
async function getResult2() {let b = await ResolvePromise();
  console.log(b); // "resolve"
}
getResult();
getResult2();
console.log(0);

async 是一个异步执行的函数,不会阻塞主线程的执行

async 函数在执行时,是一个独自的协程,能够用 await 来暂停,因为期待的是一个 Promise 对象,就能够用 resolve 来复原该协程

参考资料

  1. 学习 koa 源码的整体架构,浅析 koa 洋葱模型原理和 co 原理

20 | 垃圾回收(一):V8 的两个垃圾回收器是如何工作的?

  1. 通过 GC Root 标记内存中流动对象和非流动对象。

    • V8 采纳可拜访性(reachability)算法判断堆中的对象是是否为流动对象

      • GC Root 可能遍历到的对象,是可拜访的(reachable),称为流动对象
      • GC Root 不能遍历到的对象,认为是不可拜访的(unreachable),称为非流动对象
    • 浏览器环境中有很多 GC Root

      • window 对象(位于每个 iframe 中)
      • DOM,由能够通过遍历文档达到的所有原生 DOM 节点组成
      • 寄存栈上变量
  2. 回收非流动对象所占用的内存
  3. 回收后,做内存整理(可选,有些垃圾回收器不会产生内存碎片,比方副垃圾回收器)

    • 回收完结后,内存中会呈现大量不间断的空间,这空间被称为内存碎片
    • 如果内存碎片太多的话,当须要较大间断的内存时,就会呈现内存不足

V8 受代际假说影响,应用了两个垃圾回收器:主垃圾回收器 (Major GC),副垃圾回收器(Minor GC

代际假说:

  1. 大部分对象都是“朝生夕死”的,也就是说大部分对象在内存中存活的工夫很短,比方函数外部申明的变量,或者块级作用域中的变量,当函数或者代码块执行完结时,作用域中定义的变量就会被销毁。因而这一类对象一经分配内存,很快就变得不可拜访
  2. 不死的对象,会活得更久,比方:windowDOMWeb API

V8 把堆分为两个区域:

  • 新生代:寄存生存工夫短的对象

    • 容量小,1~8M
    • 应用副垃圾回收器(Minor GC
    • 应用 Scavenge 算法,将新生代区域分成两局部

      • 对象区域 (from-space)
      • 闲暇区域 (to-space)

        1. 对象区域放新退出的对象
        2. 对象区域快满的时候,执行垃圾清理(先标记,再清理)
        3. 清理的把流动对象复制到闲暇区域,并且排序(闲暇区域就没有内存碎片了)
        4. 复制完之后,把对象区域和闲暇区域进行翻转
        5. 反复执行下面的步骤
        6. 通过两次垃圾回收后还存在的对象,挪动到老生代中
  • 老生代:寄存生存工夫久的对象

    • 容量大

      • 对象占用空间大
      • 对象存活工夫长
    • 应用主垃圾回收器(Major GC
    • 应用标记 – 革除算法(Mark-Sweep

      • 标记:从根元素开始,找到流动对象,找不到的就是垃圾
      • 清理:间接清理垃圾(会产生垃圾碎片)
    • 或者应用标记 – 整顿算法(Mark-Compact

      • 标记:从根元素开始,找到流动对象,找不到的就是垃圾
      • 整顿:把流动对象向同一端挪动,另一端间接清理(不会产生垃圾碎片)

参考资料

  1. Understanding Garbage Collection and hunting Memory Leaks in Node.js
  2. 深刻了解 Node.js:核心思想与源码剖析
  3. When and How JavaScript garbage collector works
  4. V8 引擎的内存治理
  5. Trash talk: the Orinoco garbage collector

21 | 垃圾回收(二):V8 是如何优化垃圾回收器执行效率的?

JavaScript 是运行在主线程上的,一旦执行垃圾回收算法,JavaScript 会暂停执行,等垃圾回收结束后再复原执行,这种行为成为全进展(Stop-The-World

V8 团队向现有的垃圾回收器增加并行、并发、增量等垃圾回收技术

这些技术次要从两方面解决垃圾回收效率的问题:

  1. 将一个残缺的垃圾回收工作拆分成多个小的工作
  2. 将标记对象、挪动对象等工作转移到后端线程进行

并行回收(在主线程执行,全进展)

在主线程执行垃圾回收的工作时,开启多个帮助线程,同时执行回收工作

采纳并行回收,垃圾回收所耗费的工夫 = 辅助线程数 * 单个线程所耗费的工夫

在执行垃圾标记的过程中,主线程不会同时执行 JavaScript 代码,因而代码不会扭转回收过程

假如内存状态是动态的,因而只有确保同时只有一个辅助线程在拜访对象就好了

这是 V8 副垃圾回收器采纳的策略

它在执行垃圾回收的过程中,启动多个线程来负责新生代中的垃圾清理,同时将对象空间中的数据挪动到闲暇区域,这操作会导致数据地址变了,所以还须要同步更新这些对象的指针

增量回收(在主线程执行,穿插在各个工作之间)

将标记工作合成为更小的块,穿插在主线程不同的工作之间执行

采纳增量回收,垃圾回收器没必要一次执行实现的垃圾回收流程,每次执行的只是一小部分工作

增量回收是并发的,须要满足两点要求:

  1. 垃圾回收能够随时被暂停和重启,暂停的时候须要保留扫描后果,期待下一次回收
  2. 在暂停期间,被标记好的垃圾数据,如果被批改了,垃圾回收器须要正确的解决

在垃圾回收的时候,V8 应用三色标记法:

  • 彩色:示意所有能拜访到的数据(流动数据),且子节点曾经都标记实现
  • 红色:示意还没有被拜访到,如果在一轮遍历完结还是红色,这个数据就会被回收
  • 灰色:示意正在解决这个节点,且子节点还没被解决

垃圾回收器会依据有没有灰色的节点来判断这一轮遍历有没有完结

  • 没有灰色:一轮遍历完结,能够清理垃圾
  • 有灰色:一轮遍历还没完结,从灰色的节点继续执行

标记为彩色的数据被批改了,也就是说彩色的节点援用了一个红色的节点,然而彩色的节点是曾经实现标记的,这是它前面还有一个红色的节点是不会被标记为彩色的

这就须要一个约束条件:不能让彩色节点指向红色节点

这个约束条件是:写屏障机制(Write-barrier):

  • 当产生彩色节点援用红色节点,写屏障机制会强制将这个红色节点变为灰色的,从而保障彩色节点不能指向红色节点

这种办法被称为强三色不变性

并发回收(不在主线程执行)

在主线程执行 JavaScript 时,辅助线程在后盾执行垃圾回收操作

长处:主线程不会被挂起(JavaScript 能够自在执行,同时辅助线程能够执行垃圾回收)

但有两点导致它很难实现:

  1. 主线程执行 JavaScript 时,堆中的内容随时会变动,就会使得辅助线程之前的工作白做
  2. 主线程和辅助线程可能会在同一时间去批改同一个对象,这就须要额定实现读写锁的性能

《图解 Google V8》学习笔记系列

  1. 《图解 Google V8》设计思维篇——学习笔记(一)
  2. 《图解 Google V8》编译流水篇——学习笔记(二)
退出移动版