日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

圖解 V8 執(zhí)行 JS 的過程

本文來分享 V8 引擎執(zhí)行 JAVAScript 的過程和垃圾回收機(jī)制。

1、JS 代碼執(zhí)行過程

在說V8的執(zhí)行JavaScript代碼的機(jī)制之前,我們先來看看編譯型和解釋型語言的區(qū)別。

(1)編譯型語言和解釋型語言

我們知道,機(jī)器是不能直接理解代碼的。所以,在執(zhí)行程序之前,需要將代碼翻譯成機(jī)器能讀懂的機(jī)器語言。按語言的執(zhí)行流程,可以把計(jì)算機(jī)語言劃分為編譯型語言和解釋型語言:

  • 編譯型語言:在代碼運(yùn)行前編譯器直接將對(duì)應(yīng)的代碼轉(zhuǎn)換成機(jī)器碼,運(yùn)行時(shí)不需要再重新翻譯,直接可以使用編譯后的結(jié)果。
  • 解釋型語言:需要將代碼轉(zhuǎn)換成機(jī)器碼,和編譯型語言的區(qū)別在于運(yùn)行時(shí)需要轉(zhuǎn)換。解釋型語言的執(zhí)行速度要慢于編譯型語言,因?yàn)榻忉屝驼Z言每次執(zhí)行都需要把源碼轉(zhuǎn)換一次才能執(zhí)行。

Java 和 C++ 等語言都是編譯型語言,而 JavaScript 是解釋性語言,它整體的執(zhí)行速度會(huì)略慢于編譯型的語言。V8 是眾多瀏覽器的 JS 引擎中性能表現(xiàn)最好的一個(gè),并且它是 Chrome 的內(nèi)核,Node.js 也是基于 V8 引擎研發(fā)的。

編譯型語言和解釋器語言代碼執(zhí)行的具體流程如下:

圖解 V8 執(zhí)行 JS 的過程

兩者的執(zhí)行流程如下:

  • 在編譯型語言的編譯過程中,編譯器首先會(huì)依次對(duì)源代碼進(jìn)行詞法分析、語法分析,生成抽象語法樹(AST),然后優(yōu)化代碼,最后再生成處理器能夠理解的機(jī)器碼。如果編譯成功,將會(huì)生成一個(gè)可執(zhí)行的文件。但如果編譯過程發(fā)生了語法或者其他的錯(cuò)誤,那么編譯器就會(huì)拋出異常,最后的二進(jìn)制文件也不會(huì)生成成功。
  • 在解釋型語言的解釋過程中,同樣解釋器也會(huì)對(duì)源代碼進(jìn)行詞法分析、語法分析,并生成抽象語法樹(AST),不過它會(huì)再基于抽象語法樹生成字節(jié)碼,最后再根據(jù)字節(jié)碼來執(zhí)行程序、輸出結(jié)果。

(2) V8 執(zhí)行代碼過程

V8 在執(zhí)行過程用到了解釋器和編譯器。 其執(zhí)行過程如下:

  • Parse 階段:V8 引擎將 JS 代碼轉(zhuǎn)換成 AST(抽象語法樹)。
  • Ignition 階段:解釋器將 AST 轉(zhuǎn)換為字節(jié)碼,解析執(zhí)行字節(jié)碼也會(huì)為下一個(gè)階段優(yōu)化編譯提供需要的信息。
  • TurboFan 階段:編譯器利用上個(gè)階段收集的信息,將字節(jié)碼優(yōu)化為可以執(zhí)行的機(jī)器碼。
  • Orinoco 階段:垃圾回收階段,將程序中不再使用的內(nèi)存空間進(jìn)行回收。

這里前三個(gè)步驟是JavaScript的執(zhí)行過程,最后一步是垃圾回收的過程。下面就先來看看V8 執(zhí)行 JavaScript的過程。

生成抽象語法樹

這個(gè)過程就是將源代碼轉(zhuǎn)換為抽象語法樹(AST),并生成執(zhí)行上下文,執(zhí)行上下文就是代碼在執(zhí)行過程中的環(huán)境信息。

將 JS 代碼解析成 AST主要分為兩個(gè)階段:

  • 詞法分析:這個(gè)階段會(huì)將源代碼拆成最小的、不可再分的詞法單元,稱為 token。比如代碼 var a = 1;通常會(huì)被分解成 var 、a、=、1、; 這五個(gè)詞法單元。代碼中的空格在 JavaScript 中是直接忽略的,簡(jiǎn)單來說就是將 JavaScript 代碼解析成一個(gè)個(gè)令牌(Token)。
  • 語法分析:這個(gè)過程是將上一步生成的 token 數(shù)據(jù),根據(jù)語法規(guī)則轉(zhuǎn)為 AST。如果源碼符合語法規(guī)則,這一步就會(huì)順利完成。如果源碼存在語法錯(cuò)誤,這一步就會(huì)終止,并拋出一個(gè)語法錯(cuò)誤,簡(jiǎn)單來說就是將令牌組裝成一棵抽象的語法樹(AST)。

通過詞法分析會(huì)對(duì)代碼逐個(gè)字符進(jìn)行解析,生成類似下面結(jié)構(gòu)的令牌(Token),這些令牌類型各不相同,有關(guān)鍵字、標(biāo)識(shí)符、符號(hào)、數(shù)字等。代碼 var a = 1;會(huì)轉(zhuǎn)化為下面這樣的令牌:

Keyword(var)
Identifier(name)
Punctuator(=)
Number(1)
  • 1.
  • 2.
  • 3.
  • 4.

語法分析階段會(huì)用令牌生成一棵抽象語法樹,生成樹的過程中會(huì)去除不必要的符號(hào)令牌,然后按照語法規(guī)則來生成。下面來看兩段代碼:

// 第一段代碼
var a = 1;
// 第二段代碼
function sum (a,b) {
  return a + b;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

將這兩段代碼分別轉(zhuǎn)換成 AST 抽象語法樹之后返回的 JSON 如下:

  • 第一段代碼,編譯后的結(jié)果:
{
  "type": "Program",
  "start": 0,
  "end": 10,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 10,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 9,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 5,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 8,
            "end": 9,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

它的結(jié)構(gòu)大致如下:

圖解 V8 執(zhí)行 JS 的過程

  • 第二段代碼,編譯出來的結(jié)果:
{
  "type": "Program",
  "start": 0,
  "end": 38,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 38,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 12,
        "name": "sum"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "start": 14,
          "end": 15,
          "name": "a"
        },
        {
          "type": "Identifier",
          "start": 16,
          "end": 17,
          "name": "b"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "start": 19,
        "end": 38,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 23,
            "end": 36,
            "argument": {
              "type": "BinaryExpression",
              "start": 30,
              "end": 35,
              "left": {
                "type": "Identifier",
                "start": 30,
                "end": 31,
                "name": "a"
              },
              "operator": "+",
              "right": {
                "type": "Identifier",
                "start": 34,
                "end": 35,
                "name": "b"
              }
            }
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.

它的結(jié)構(gòu)大致如下:

圖解 V8 執(zhí)行 JS 的過程

可以看到,AST 只是源代碼語法結(jié)構(gòu)的一種抽象的表示形式,計(jì)算機(jī)也不會(huì)去直接去識(shí)別 JS 代碼,轉(zhuǎn)換成抽象語法樹也只是識(shí)別這一過程中的第一步。AST 的結(jié)構(gòu)和代碼的結(jié)構(gòu)非常相似,其實(shí)也可以把 AST 看成代碼的結(jié)構(gòu)化的表示,編譯器或者解釋器后續(xù)的工作都需要依賴于 AST。

AST的應(yīng)用場(chǎng)景:

AST 是一種很重要的數(shù)據(jù)結(jié)構(gòu),很多地方用到了AST。比如在 Babel 中,Babel 是一個(gè)代碼轉(zhuǎn)碼器,可以將 ES6 代碼轉(zhuǎn)為 ES5 代碼。Babel 的工作原理就是先將 ES6 源碼轉(zhuǎn)換為 AST,然后再將 ES6 語法的 AST 轉(zhuǎn)換為 ES5 語法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代碼。

除了 Babel 之外,ESLint 也使用到了 AST。ESLint 是一個(gè)用來檢查 JavaScript 編寫規(guī)范的插件,其檢測(cè)流程也是需要將源碼轉(zhuǎn)換為 AST,然后再利用 AST 來檢查代碼規(guī)范化的問題。

除了上述應(yīng)用場(chǎng)景,AST 的應(yīng)用場(chǎng)景還有很多:

  • JS 反編譯,語法解析。
  • 代碼高亮。
  • 關(guān)鍵字匹配。
  • 代碼壓縮。

生成字節(jié)碼

有了 抽象語法樹 AST 和執(zhí)行上下文后,就輪到解釋器就登場(chǎng)了,它會(huì)根據(jù) AST 生成字節(jié)碼,并解釋執(zhí)行字節(jié)碼。

在 V8 的早期版本中,是通過 AST 直接轉(zhuǎn)換成機(jī)器碼的。將 AST 直接轉(zhuǎn)換為機(jī)器碼會(huì)存在一些問題:

  • 直接轉(zhuǎn)換會(huì)帶來內(nèi)存占用過大的問題,因?yàn)閷⒊橄笳Z法樹全部生成了機(jī)器碼,而機(jī)器碼相比字節(jié)碼占用的內(nèi)存多了很多;
  • 某些 JavaScript 使用場(chǎng)景使用解釋器更為合適,解析成字節(jié)碼,有些代碼沒必要生成機(jī)器碼,進(jìn)而盡可能減少了占用內(nèi)存過大的問題。

為了解決內(nèi)存占用問題,就在 V8 引擎中引入了字節(jié)碼。那什么是字節(jié)碼呢?為什么引入字節(jié)碼就能解決內(nèi)存占用問題呢?

字節(jié)碼就是介于 AST 和機(jī)器碼之間的一種代碼。 需要將其轉(zhuǎn)換成機(jī)器碼后才能執(zhí)行,字節(jié)碼是對(duì)機(jī)器碼的一個(gè)抽象描述,相對(duì)于機(jī)器碼而言,它的代碼量更小,從而可以減少內(nèi)存消耗。解釋器除了可以快速生成沒有優(yōu)化的字節(jié)碼外,還可以執(zhí)行部分字節(jié)碼。

生成機(jī)器碼

生成字節(jié)碼之后,就進(jìn)入執(zhí)行階段了,實(shí)際上,這一步就是將字節(jié)碼生成機(jī)器碼。

一般情況下,如果字節(jié)碼是第一次執(zhí)行,那么解釋器就會(huì)逐條解釋執(zhí)行。在執(zhí)行字節(jié)碼過程中,如果發(fā)現(xiàn)有熱代碼(重復(fù)執(zhí)行的代碼,運(yùn)行次數(shù)超過某個(gè)閾值就被標(biāo)記為熱代碼),那么后臺(tái)的編譯器就會(huì)把該段熱點(diǎn)的字節(jié)碼編譯為高效的機(jī)器碼,然后當(dāng)再次執(zhí)行這段被優(yōu)化的代碼時(shí),只需要執(zhí)行編譯后的機(jī)器碼即可,這樣提升了代碼的執(zhí)行效率。

字節(jié)碼配合解釋器和編譯器的技術(shù)就是 即時(shí)編譯(JIT)。在 V8 中就是指解釋器在解釋執(zhí)行字節(jié)碼的同時(shí),收集代碼信息,當(dāng)它發(fā)現(xiàn)某一部分代碼變熱了之后,編譯器便閃亮登場(chǎng),把熱點(diǎn)的字節(jié)碼轉(zhuǎn)換為機(jī)器碼,并把轉(zhuǎn)換后的機(jī)器碼保存起來,以備下次使用。

因?yàn)?V8 引擎是多線程的,編譯器的編譯線程和生成字節(jié)碼不會(huì)在同一個(gè)線程上,這樣可以和解釋器相互配合著使用,不受另一方的影響。下面是JIT技術(shù)的工作機(jī)制:

圖解 V8 執(zhí)行 JS 的過程

解釋器在得到 AST 之后,會(huì)按需進(jìn)行解釋和執(zhí)行。也就是說如果某個(gè)函數(shù)沒有被調(diào)用,則不會(huì)去解釋執(zhí)行它。在這個(gè)過程中解釋器會(huì)將一些重復(fù)可優(yōu)化的操作收集起來生成分析數(shù)據(jù),然后將生成的字節(jié)碼和分析數(shù)據(jù)傳給編譯器,編譯器會(huì)依據(jù)分析數(shù)據(jù)來生成高度優(yōu)化的機(jī)器碼。

優(yōu)化后的機(jī)器碼的作用和緩存很類似,當(dāng)解釋器再次遇到相同的內(nèi)容時(shí),就可以直接執(zhí)行優(yōu)化后的機(jī)器碼。當(dāng)然優(yōu)化后的代碼有時(shí)可能會(huì)無法運(yùn)行(比如函數(shù)參數(shù)類型改變),那么會(huì)再次反優(yōu)化為字節(jié)碼交給解釋器。

整個(gè)過程如下圖所示:

圖解 V8 執(zhí)行 JS 的過程

(3)執(zhí)行過程優(yōu)化

如果JavaScript代碼在執(zhí)行前都要完全經(jīng)過解析才能執(zhí)行,那可能會(huì)面臨以下問題:

  • 代碼執(zhí)行時(shí)間變長(zhǎng):一次性解析所有代碼會(huì)增加代碼的運(yùn)行時(shí)間。
  • 消耗更多內(nèi)存:解析完的 AST 以及根據(jù) AST 編譯后的字節(jié)碼都會(huì)存放在內(nèi)存中,會(huì)占用更多內(nèi)存空間。
  • 占用磁盤空間:編譯后的代碼會(huì)緩存在磁盤上,占用磁盤空間。

所以,V8 引擎使用了延遲解析:在解析過程中,對(duì)于不是立即執(zhí)行的函數(shù),只進(jìn)行預(yù)解析;只有當(dāng)函數(shù)調(diào)用時(shí),才對(duì)函數(shù)進(jìn)行全量解析。

進(jìn)行預(yù)解析時(shí),只驗(yàn)證函數(shù)語法是否有效、解析函數(shù)聲明、確定函數(shù)作用域,不生成 AST,而實(shí)現(xiàn)預(yù)解析的,就是 Pre-Parser 解析器。

以下面代碼為例:

function sum(a, b) {
    return a + b;
}
const a = 666;
const c = 996;
sum(1, 1);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

V8 解析器是從上往下解析代碼的,當(dāng)解析器遇到函數(shù)聲明 sum 時(shí),發(fā)現(xiàn)它不是立即執(zhí)行,所以會(huì)用 Pre-Parser 解析器對(duì)其預(yù)解析,過程中只會(huì)解析函數(shù)聲明,不會(huì)解析函數(shù)內(nèi)部代碼,不會(huì)為函數(shù)內(nèi)部代碼生成 AST。

之后解釋器會(huì)把 AST 編譯為字節(jié)碼并執(zhí)行,解釋器會(huì)按照自上而下的順序執(zhí)行代碼,先執(zhí)行 const a = 666;  和 const c = 996; ,然后執(zhí)行函數(shù)調(diào)用 sum(1, 1) ,這時(shí) Parser 解析器才會(huì)繼續(xù)解析函數(shù)內(nèi)的代碼、生成 AST,再交給解釋器編譯執(zhí)行。

2、垃圾回收

(1)JS 內(nèi)存管理機(jī)制

計(jì)算機(jī)程序語言都運(yùn)行在對(duì)應(yīng)的代碼引擎上,使用內(nèi)存過程可以分為以下三個(gè)步驟:

  • 分配所需要的系統(tǒng)內(nèi)存空間。
  • 使用分配到的內(nèi)存進(jìn)行讀或?qū)懙炔僮鳌?/li>
  • 不需要使用內(nèi)存時(shí),將其空間釋放或者歸還。

在 JavaScript 中,當(dāng)創(chuàng)建變量時(shí),系統(tǒng)會(huì)自動(dòng)給對(duì)象分配對(duì)應(yīng)的內(nèi)存,來看下面的例子:

var a = 123; // 給數(shù)值變量分配棧內(nèi)存
var etf = "ARK"; // 給字符串分配棧內(nèi)存
// 給對(duì)象及其包含的值分配堆內(nèi)存
var obj = {
  name: 'tom',
  age: 13
}; 
// 給數(shù)組及其包含的值分配內(nèi)存
var a = [1, null, "str"]; 
// 給函數(shù)分配內(nèi)存
function sum(a, b){
  return a + b;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

JavaScript 中的數(shù)據(jù)分為兩類:

  • 基本類型:這些類型在內(nèi)存中會(huì)占據(jù)固定的內(nèi)存空間,它們的值都保存在棧空間中,直接可以通過值來訪問這些;
  • 引用類型:由于引用類型值大小不固定,棧內(nèi)存中存放地址指向堆內(nèi)存中的對(duì)象,是通過引用來訪問的。

棧內(nèi)存中的基本類型,可以通過操作系統(tǒng)直接處理;而堆內(nèi)存中的引用類型,正是由于可以經(jīng)常變化,大小不固定,因此需要 JavaScript 的引擎通過垃圾回收機(jī)制來處理。所謂的垃圾回收是指:JavaScript代碼運(yùn)行時(shí),需要分配內(nèi)存空間來儲(chǔ)存變量和值。當(dāng)變量不在參與運(yùn)行時(shí),就需要系統(tǒng)收回被占用的內(nèi)存空間。 Javascript 具有自動(dòng)垃圾回收機(jī)制,會(huì)定期對(duì)那些不再使用的變量、對(duì)象所占用的內(nèi)存進(jìn)行釋放,原理就是找到不再使用的變量,然后釋放掉其占用的內(nèi)存。

JavaScript中存在兩種變量:局部變量和全局變量。全局變量的生命周期會(huì)持續(xù)要頁(yè)面卸載;而局部變量聲明在函數(shù)中,它的生命周期從函數(shù)執(zhí)行開始,直到函數(shù)執(zhí)行結(jié)束,在這個(gè)過程中,局部變量會(huì)在堆或棧中存儲(chǔ)它們的值,當(dāng)函數(shù)執(zhí)行結(jié)束后,這些局部變量不再被使用,它們所占有的空間就會(huì)被釋放。不過,當(dāng)局部變量被外部函數(shù)使用時(shí),其中一種情況就是閉包,在函數(shù)執(zhí)行結(jié)束后,函數(shù)外部的變量依然指向函數(shù)內(nèi)部的局部變量,此時(shí)局部變量依然在被使用,所以不會(huì)回收。

(2)V8 垃圾回收過程

先來看看 Chrome 瀏覽器的垃圾回收過程:

通過 GC Root 標(biāo)記空間中活動(dòng)對(duì)象和?活動(dòng)對(duì)象

目前 V8 采用的可訪問性算法來判斷堆中的對(duì)象是否是活動(dòng)對(duì)象。這個(gè)算法是將一些 GC Root 作為初始存活的對(duì)象的集合,從 GC Roots 對(duì)象出發(fā),遍歷 GC Root 中所有對(duì)象:

  • 通過 GC Root 遍歷到的對(duì)象是可訪問的,必須保證這些對(duì)象應(yīng)該在內(nèi)存中保留,可訪問的對(duì)象稱為活動(dòng)對(duì)象。
  • 通過 GC Roots 沒有遍歷到的對(duì)象是不可訪問的,這些不可訪問的對(duì)象就可能被回收,不可訪問的對(duì)象稱為非活動(dòng)對(duì)象。

回收非活動(dòng)對(duì)象所占據(jù)的內(nèi)存

其實(shí)就是在所有的標(biāo)記完成之后,統(tǒng)一清理內(nèi)存中所有被標(biāo)記為可回收的對(duì)象。

內(nèi)存整理

一般來說,頻繁回收對(duì)象后,內(nèi)存中就會(huì)存在大量不連續(xù)空間,這些不連續(xù)的內(nèi)存空間稱為內(nèi)存碎片。當(dāng)內(nèi)存中出現(xiàn)了大量的內(nèi)存碎片之后,如果需要分配較大的連續(xù)內(nèi)存時(shí),就有可能出現(xiàn)內(nèi)存不足的情況,所以最后一步需要整理這些內(nèi)存碎片。這步其實(shí)是可選的,因?yàn)橛械睦厥掌鞑粫?huì)產(chǎn)生內(nèi)存碎片。

以上就是大致的垃圾回收流程。目前 V8 使用了兩個(gè)垃圾回收器:主垃圾回收器和副垃圾回收器。下面就來看看 V8 是如何實(shí)現(xiàn)垃圾回收的。

在 V8 中,會(huì)把堆分為新生代和老生代兩個(gè)區(qū)域,新生代中存放的是生存時(shí)間短的對(duì)象,老生代中存放生存時(shí)間久的對(duì)象:

圖解 V8 執(zhí)行 JS 的過程

新?代通常只?持 1~8M 的容量,而老生代支持的容量就大很多。對(duì)于這兩塊區(qū)域,V8分別使用兩個(gè)不同的垃圾回收器,以便更高效地實(shí)施垃圾回收:

  • 副垃圾回收器:負(fù)責(zé)新生代的垃圾回收。
  • 主垃圾回收器:負(fù)責(zé)老生代的垃圾回收。

副垃圾回收器(新生代)

副垃圾回收器主要負(fù)責(zé)新生代的垃圾回收。大多數(shù)的對(duì)象最開始都會(huì)被分配在新生代,該存儲(chǔ)空間相對(duì)較小,分為兩個(gè)空間:from 空間(對(duì)象區(qū))和 to 空間(空閑區(qū))。

新加入的對(duì)象都會(huì)存放到對(duì)象區(qū)域,當(dāng)對(duì)象區(qū)域快被寫滿時(shí),就需要執(zhí)行一次垃圾清理操作:首先要對(duì)對(duì)象區(qū)域中的垃圾做標(biāo)記,標(biāo)記完成之后,就進(jìn)入垃圾清理階段。副垃圾回收器會(huì)把這些存活的對(duì)象復(fù)制到空閑區(qū)域中,同時(shí)它還會(huì)把這些對(duì)象有序地排列起來。這個(gè)復(fù)制過程就相當(dāng)于完成了內(nèi)存整理操作,復(fù)制后空閑區(qū)域就沒有內(nèi)存碎片了:

圖解 V8 執(zhí)行 JS 的過程

完成復(fù)制后,對(duì)象區(qū)域與空閑區(qū)域進(jìn)行角色翻轉(zhuǎn),也就是原來的對(duì)象區(qū)域變成空閑區(qū)域,原來的空閑區(qū)域變成了對(duì)象區(qū)域,這種算法稱之為 Scavenge 算法,這樣就完成了垃圾對(duì)象的回收操作。同時(shí),這種角色翻轉(zhuǎn)的操作還能讓新生代中的這兩塊區(qū)域無限重復(fù)使用下去:

圖解 V8 執(zhí)行 JS 的過程

不過,副垃圾回收器每次執(zhí)行清理操作時(shí),都需要將存活的對(duì)象從對(duì)象區(qū)域復(fù)制到空閑區(qū)域,復(fù)制操作需要時(shí)間成本,如果新生區(qū)空間設(shè)置得太大了,那么每次清理的時(shí)間就會(huì)過久,所以為了執(zhí)行效率,一般新生區(qū)的空間會(huì)被設(shè)置得比較小。 也正是因?yàn)樾律鷧^(qū)的空間不大,所以很容易被存活的對(duì)象裝滿整個(gè)區(qū)域,副垃圾回收器一旦監(jiān)控對(duì)象裝滿了,便執(zhí)行垃圾回收。同時(shí),副垃圾回收器還會(huì)采用對(duì)象晉升策略,也就是移動(dòng)那些經(jīng)過兩次垃圾回收依然還存活的對(duì)象到老生代中。

主垃圾回收器(老生代)

主垃圾回收器主要負(fù)責(zé)老生代中的垃圾回收。除了新生代中晉升的對(duì)象,?些?的對(duì)象會(huì)直接被分配到老生代里。因此,老生代中的對(duì)象有兩個(gè)特點(diǎn):

  • 對(duì)象占用空間大。
  • 對(duì)象存活時(shí)間間。

由于老生代的對(duì)象比較大,若要在老生代中使用 Scavenge 算法進(jìn)行垃圾回收,復(fù)制這些大的對(duì)象將會(huì)花費(fèi)較多時(shí)間,從而導(dǎo)致回收?qǐng)?zhí)行效率不高,同時(shí)還會(huì)浪費(fèi)空間。所以,主垃圾回收器采用標(biāo)記清除的算法進(jìn)行垃圾回收。

這種方式分為標(biāo)記和清除兩個(gè)階段:

  • 標(biāo)記階段: 從一組根元素開始,遞歸遍歷這組根元素,在這個(gè)遍歷過程中,能到達(dá)的元素稱為活動(dòng)對(duì)象,沒有到達(dá)的元素就可以判斷為垃圾數(shù)據(jù)。
  • 清除階段: 主垃圾回收器會(huì)直接將標(biāo)記為垃圾的數(shù)據(jù)清理掉。

這兩個(gè)階段如圖所示:

圖解 V8 執(zhí)行 JS 的過程

對(duì)垃圾數(shù)據(jù)進(jìn)行標(biāo)記,然后清除,這就是標(biāo)記清除算法,不過對(duì)一塊內(nèi)存多次執(zhí)?標(biāo)記清除算法后,會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片。而碎片過多會(huì)導(dǎo)致大對(duì)象無法分配到足夠的連續(xù)內(nèi)存,于是又引入了另外一種算法——標(biāo)記整理。

這個(gè)算法的標(biāo)記過程仍然與標(biāo)記清除算法里的是一樣的,先標(biāo)記可回收對(duì)象,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉這一端之外的內(nèi)存:

圖解 V8 執(zhí)行 JS 的過程

全停頓

我們知道,JavaScript 是單行線語言,運(yùn)行在主線程上。一旦執(zhí)行垃圾回收算法,都需要將正在執(zhí)行的 JavaScript 腳本暫停下來,待垃圾回收完畢后再恢復(fù)腳本執(zhí)行。這種行為叫做全停頓。

主垃圾回收器執(zhí)行一次完整的垃圾回收流程如下圖所示:

圖解 V8 執(zhí)行 JS 的過程

在 V8 新生代的垃圾回收中,因其空間較小,且存活對(duì)象較少,所以全停頓的影響不大。但老生代中,如果在執(zhí)行垃圾回收的過程中,占用主線程時(shí)間過久,主線程是不能做其他事情的,需要等待執(zhí)行完垃圾回收操作才能做其他事情,這將就可能會(huì)造成頁(yè)面的卡頓現(xiàn)象。

為了降低老生代的垃圾回收而造成的卡頓,V8 將標(biāo)記過程分為一個(gè)個(gè)的子標(biāo)記過程,同時(shí)讓垃圾回收標(biāo)記和 JavaScript 應(yīng)用邏輯交替進(jìn)行,直到標(biāo)記階段完成,這個(gè)算法稱為增量標(biāo)記算法。如下圖所示:

圖解 V8 執(zhí)行 JS 的過程

使用增量標(biāo)記算法可以把一個(gè)完整的垃圾回收任務(wù)拆分為很多小的任務(wù),這些小的任務(wù)執(zhí)行時(shí)間比較短,可以穿插在其他的 JavaScript 任務(wù)中間執(zhí)行,這樣當(dāng)執(zhí)行代碼時(shí),就不會(huì)讓用戶因?yàn)槔厥杖蝿?wù)而感受到頁(yè)面的卡頓了。

(3)減少垃圾回收

雖然瀏覽器可以進(jìn)行垃圾自動(dòng)回收,但是當(dāng)代碼比較復(fù)雜時(shí),垃圾回收所帶來的代價(jià)較大,所以應(yīng)該盡量減少垃圾回收:

  • 對(duì)數(shù)組進(jìn)行優(yōu)化: 在清空一個(gè)數(shù)組時(shí),最簡(jiǎn)單的方法就是給其賦值為[ ],但是與此同時(shí)會(huì)創(chuàng)建一個(gè)新的空對(duì)象,可以將數(shù)組的長(zhǎng)度設(shè)置為0,以此來達(dá)到清空數(shù)組的目的。
  • 對(duì)object進(jìn)行優(yōu)化: 對(duì)象盡量復(fù)用,對(duì)于不再使用的對(duì)象,就將其設(shè)置為null,盡快被回收。
  • 對(duì)函數(shù)進(jìn)行優(yōu)化: 在循環(huán)中的函數(shù)表達(dá)式,如果可以復(fù)用,盡量放在函數(shù)的外面。

分享到:
標(biāo)簽:JS
用戶無頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定