天天看點

《Chrome V8原理講解》第十七篇 JS對象的記憶體布局與建立過程前言1 摘要2 JS對象3 JS對象的建立過程

《Chrome V8原理講解》第十七篇 JS對象的記憶體布局與建立過程前言1 摘要2 JS對象3 JS對象的建立過程

<h1 class="pgc-h-arrow-right" data-track="1">前言</h1>

本系列的前十三篇文章,講解了V8執行Javascript時最基礎的工作流程和原理,包括詞法分析、文法分析、位元組碼生成、Builtins方法、ignition執行單元,等等,達到了從零做起,入門學習的目的。

接下來的文章将以問題為導向講解V8源碼,例如:以閉包技術、或垃圾回收(GC)為專題講解V8中的相關源碼。V8代碼過于龐大,以問題為導向可以使得學習主題更加明确、效果更好。同時,我争取做到每篇文章是一個獨立的知識點,友善大家閱讀。

讀者可以把想學的内容在文末評論區留言,我彙總後出專題文章。

<h1 class="pgc-h-arrow-right" data-track="3">1 摘要</h1>

《javascript進階程式設計》中對JS對象有這樣的描述:“ECMA-262 将對象定義為一組屬性的無序集合。嚴格來說,這意味着對象就是一組沒有特定順序的值。對象的每個屬性或方法都由一個名稱來辨別,這個名稱映射到一個值。可以把ECMAScript的對象想象成一張散清單,其中的内容就是一組名/值對,值可以是資料或者函數。”v8官方文檔提到這樣的描述:“出于性能、亦或代碼設計的考慮,V8中對資料類型的設計做了詳細分類,對JS對象内部的資料成員也做了不同的設計”。

本文深入V8内部,詳細剖析JS對象的建立過程,講解JS對象内部成員的組成方式、記憶體布局,以及重要資料結構。本文内容組織方式:V8中JS對象的重要概念、成員組成和記憶體布局(章節2);JS對象建立過程(章節3)。

<h1 class="pgc-h-arrow-right" data-track="5">2 JS對象</h1>

在V8中,JS對象的每個成員、方法都有詳細的分類和記憶體組織規則,内部成員從資料類型看分為兩大類,元素類(Element)和屬性類(Property),如圖1。

《Chrome V8原理講解》第十七篇 JS對象的記憶體布局與建立過程前言1 摘要2 JS對象3 JS對象的建立過程

圖1(來自V8官方)中,可以看到元素和屬性分開存儲,原因是為提高效率。Element成員可以利用下标通路,它存在連續的位址空間中。Property成員,也是存在連的位址空間中,但不能使用下标通路成員,需要借助Map(HiddenClass)通路,Map記載了資料的描述符,通俗地說,Map描述資料的形狀,資料通路方式等,參見第十四篇文章。元素類資料不需要借助Map,他的通路效率要高一些,如圖2。

《Chrome V8原理講解》第十七篇 JS對象的記憶體布局與建立過程前言1 摘要2 JS對象3 JS對象的建立過程

圖2(出處同圖1)中,除了Element和Property之外,還有In-object property,它與前面提到的Property不同之處是通路時不需要借助Map,這提高了通路效率,但In-object property的數量有限,優先使用In-object property,用完之後使用前面提到的“正常”Property進行存儲。

《Chrome V8原理講解》第十七篇 JS對象的記憶體布局與建立過程前言1 摘要2 JS對象3 JS對象的建立過程

圖3(出處同圖1),JS對象成員的通路方式有三種:

(1) In-Object,JS對象負責維護位址,它直接存在JS對象中;

(2) Slow方式,需要借助Map通路其成員;

(3) Self-dict,JS對象自己維護位址,不需要借助Map,采用字典方式存儲資料。

self-dict是效率最差的存儲方式,當資料很多并且不連續時,V8會放棄Map機制,改用self-dict方式存儲。

圖4給出了JS對象的記憶體部局。

《Chrome V8原理講解》第十七篇 JS對象的記憶體布局與建立過程前言1 摘要2 JS對象3 JS對象的建立過程

申請JS對象時,對象的首位址指向Map(Map大小為:80byte),存在多個JS對象共用同一Map的情況,對象中還包括了Property back store和Element back store指針,是否包含其它成員依據情況而定,稍後見代碼解釋。

JS對象的成員方法存在哪裡?文章開頭處提到:它是普通的Property成員。通過下面的測試代碼解釋成員方法的存儲方式。

上半部分是js源碼,下半部分是Bytecode。代碼19行Construct建構JS對象person,并傳遞參數Nicholas,代碼16,17行從常量池中取出Nicholas并存儲到r2寄存器。而sayname成員是一個方法,因為lazy編譯的原因,此時不做編譯,而是在代碼6行執行時才做編譯,如下給出sayname成員編譯後的位元組碼:

在上面的位元組碼中,看不到console.log,因為在此階段V8僅執行sayname,雖然我們知道sayname的主體功能隻有console.log,但V8還沒有執行它,是以不編譯。不執行時不編譯,這就是lazy思想。

上述代碼可以看出:sayname雖然是一個成員方法,但在JS對象内部,它隻是普通的Property成員。

<h1 class="pgc-h-arrow-right" data-track="15">3 JS對象的建立過程</h1>

使用上面的測試用例,下面是建立person對象的源碼位置:

建立過程由RUNTIME_FUNCTION(Runtime_NewObject)開始,它是一個宏模闆,在上篇文章中講過,本文不在贅述。JSObject::New()方法建立JS對象,進入JSFunction::GetDerivedMap(isolate, constructor, new_target), JSObject);方法,源碼如下:

Handle&lt;JSFunction&gt; constructor是構造函數person(測試代碼),代碼16行時構造函數的Map基本完成,這裡設定并安裝prototype。代碼4行,EnsureHasInitialMap(constructor);很重要,它的作用是計算構造函數constructor的形狀并生成Map。然後使用這個Map申請記憶體、建立對象的執行個體worker(測試樣例代碼),計算過程需要對它進行編譯(如果之前沒有編譯過),代碼如下:

代碼5行,如果已經有Map,不用再計算了,傳回。代碼7行,計算構造函數的屬性值,這裡進行編譯。代26~30行,生成prototype(注意區分:這裡是生成,然後才是前面提到的設定和安全),構造函數是第一次生成,沒有prototype,進入代碼29行。注意: 這裡可以看出,同一個構函數的不同執行個體之間是共用一個prototype,因為prototype設定在構造函數person上,我們用person執行個體多個對象時,隻有在person初次生成時才執行代碼29行。

再來看代碼7行,源碼如下:

Handle&lt;JSFunction&gt; function是構造函數person,代碼13行進行編譯并計算對象的屬性值數量。代碼28行,會與MaxInObject比較,如果大于MaxInObject,那麼屬性值數量就是MaxInObject。前面提到JS對象中In-Object數量有限,MaxInObject正是它的最大數量。多出的屬性值在後面會作處理——放入圖1的屬性值存儲區。

回到JSObject::New()方法中代碼27行,用Map去申請JS對象的記憶體,後面就是對象執行個體化的參數設定等等,請讀者根據圖5的函數堆棧自行跟蹤。

《Chrome V8原理講解》第十七篇 JS對象的記憶體布局與建立過程前言1 摘要2 JS對象3 JS對象的建立過程

好了,今天到這裡,下次見。

懇請讀者批評指正、提出寶貴意見

微信:qq9123013 備注:v8交流 郵箱:[email protected]

本文由灰豆原創釋出

轉載,請參考轉載聲明,注明出處: https://www.anquanke.com/post/id/257484

安全客 - 有思想的安全新媒體

繼續閱讀