深入內(nèi)存原理談JS中變量存儲(chǔ)在堆中還是棧中
JavaScript
中基本類型存儲(chǔ)在堆中還是棧中?
---- 不基本類型的基本類型
看到這個(gè)問(wèn)題,相信大家都覺(jué)得這個(gè)題目實(shí)在基礎(chǔ)的不能再基礎(chǔ)了。隨手百度一下,就能看到很多人說(shuō):基本類型存在棧中,引用類型存在堆中。
真的這么簡(jiǎn)單么?
一、裝不進(jìn)冰箱的大象
讓我們看一下這段代碼:
在這里,我們聲明了一個(gè)67MiB大小的字符串,如果字符串真的存在棧中,這就不好解釋了。畢竟,v8默認(rèn)的棧區(qū)大小為984KiB??隙ㄊ谴娌幌碌?。
注:在不同時(shí)期,不同操作系統(tǒng)中V8對(duì)于字符串大小的限制并不相同。大概有個(gè)范圍是256MiB ~ 1GiB
node --v8-options | grep -B0 -A1 stack-size
說(shuō)到這里,各位是不是心里已經(jīng)開(kāi)始疑惑了呢。難道百度的答案不對(duì),得用谷歌搜?
讓我們看看這到底是怎么回事。
二、影分身的字符串
const BasicVarGen = function () { this.s1 = 'IAmString' this.s2 = 'IAmString' } let a = new BasicVarGen() let b = new BasicVarGen()
在這里,我們聲明了兩個(gè)一樣的對(duì)象,每個(gè)對(duì)象包括兩個(gè)相同的字符串。
通過(guò)開(kāi)發(fā)者工具,我們看到雖然我們聲明了四個(gè)字符串,但是其內(nèi)存指向了同一個(gè)地址。
備注:chrome
無(wú)法查看實(shí)際地址,此處為抽象后的地址
這說(shuō)明了啥?說(shuō)明了四個(gè)字符串中存的是引用地址。
所以上文中那個(gè)無(wú)法裝進(jìn)冰箱的大象,也就好解釋了。字符串并沒(méi)有存到棧中,而是存到了一個(gè)別的地方,再把這個(gè)地方的地址存到了棧中。
那,讓我們修改一下其中一個(gè)字符串的內(nèi)容
const BasicVarGen = function () { this.s0 = 'IAmString' this.s1 = 'IAmString' } let a = new BasicVarGen() let b = new BasicVarGen() debugger a.s0 = 'different string' a.s2 = 'IAmString'
debugger
之前的內(nèi)存快照
debugger
之后的內(nèi)存快照
我們可以看到,a.s0 一開(kāi)始內(nèi)容為 ‘IAmString'
,在我們修改其內(nèi)容后,地址發(fā)生了變化。
而我們新增的a.s2 其內(nèi)容為 ‘IAmString'
,其地址與其他值為 ‘IAmString'
的變量保持一致。
當(dāng)我們聲明一個(gè)字符串時(shí):
- v8內(nèi)部有一個(gè)名為
stringTable
的hashmap
緩存了所有字符串,在V8閱讀我們的代碼,轉(zhuǎn)換抽象語(yǔ)法樹(shù)時(shí),每遇到一個(gè)字符串,會(huì)根據(jù)其特征換算為一個(gè)hash
值,插入到hashmap
中。在之后如果遇到了hash值一致的字符串,會(huì)優(yōu)先從里面取出來(lái)進(jìn)行比對(duì),一致的話就不會(huì)生成新字符串類。 - 緩存字符串時(shí),根據(jù)字符串不同采取不同
hash
方式。
所以讓我們梳理一下,在我們創(chuàng)建字符串的時(shí)候,V8會(huì)先從內(nèi)存中(哈希表)查找是否有已經(jīng)創(chuàng)建的完全一致的字符串,如果存在,直接復(fù)用。如果不存在,則開(kāi)辟一塊新的內(nèi)存空間存進(jìn)這個(gè)字符串,然后把地址賦到變量中。這也是為什么我們不能直接用下標(biāo)的方式修改字符串: V8中的字符串都是不可變的。
拿出一個(gè)js的基本類型拷貝舉例講一下v8的實(shí)現(xiàn)邏輯和常規(guī)的大家理解的邏輯(雅文)
例:
var a = "劉瀟灑"; // V8讀取字符串后,去stringTable查找是否存在 不存在 hashTable 插入 '劉瀟灑' 并把'劉瀟灑'的引用存入 a var b = a; // 直接拷貝 '劉瀟灑' 的引用 b = "譚雅文"; // 查找 無(wú) 存入stringTable
疑問(wèn)點(diǎn):
const BasicVarGen = function () { this.s0 = 'IAmString' this.s1 = 'IAmString' } let a = new BasicVarGen() let b = new BasicVarGen() debugger a.s0 = 'different string' a.s2 = 'IAmString' a.s3 = a.s2+a.s0; // 疑問(wèn)點(diǎn): 字符串拼接做了哪些操作? a.s4 = a.s2+a.s
同時(shí)申請(qǐng)兩個(gè)拼接的字符串,內(nèi)容相同。
可以看到,雖然其內(nèi)容相同。但是地址并不相同。而且,地址前方的Map描述也發(fā)生了變化。
字符串拼接時(shí)如果以傳統(tǒng)方式(如
SeqString
)存儲(chǔ),拼接操作的時(shí)間復(fù)雜度為 O(n) ,采用 繩索結(jié)構(gòu)[Rope Structure
] (也就是ConsString
所采用的數(shù)據(jù)結(jié)構(gòu))可以減少拼接所花費(fèi)的時(shí)間。
如果字符串是這樣,那別的基本類型也是如此么?
三、如朕親臨的 ‘奇球'
說(shuō)完字符串,讓我們看看V8中另外一類典型的‘基本類型':oddBall
。
拓展自oddBall
的type
讓我們?cè)僮鲆粋€(gè)小實(shí)驗(yàn):
我們可以看到 上圖中列舉的基本類型,地址也是相同的。在賦值時(shí),也是就地復(fù)用。(而且這些拓展自oddBall
的基本類型,其地址是固定的,也就是說(shuō),在V8跑起來(lái)的第一時(shí)間,不管我們有沒(méi)有聲明這些基本類型,他們都已經(jīng)被創(chuàng)建完畢了。而我們聲明對(duì)象時(shí),賦的是他們的引用。這也可以解釋為什么我們說(shuō)基本類型是賦到棧中:在V8中,存放在 @73 的值,永遠(yuǎn)是空字符串,那么v8就可以等效把這些地址視為值本身。)
讓我們看看源碼,驗(yàn)證一下:
生成各種oddBall
類型的方法,可以看出返回的是一個(gè)地址
undefined
賦值給一個(gè)變量,其實(shí)賦的是地址
getRoot
方法
偏移量定義的地方
四、撲朔迷離的數(shù)字
之所以叫撲朔迷離的數(shù)字,是因?yàn)檫€沒(méi)有搞明白其分配與改變時(shí)內(nèi)存分配的機(jī)制。(其內(nèi)存是動(dòng)態(tài)的)
數(shù)字在V8中分為 smi
和 heapNumber
。
smi
直接存進(jìn)內(nèi)存范圍為 :-2³¹ 到 2³¹-1(2³¹≈2*10⁹)的整數(shù)
heapNumber
類似字符串 不可變范圍為 :所有非smi的數(shù)字
最低位用來(lái)表示是否為指針 最低位為1 則是一個(gè)指針
const o = { x: 42, // Smi y: 4.2, // HeapNumber };
o.x 中的 42 會(huì)被當(dāng)成 Smi 直接存儲(chǔ)在對(duì)象本身,而 o.y 中的 4.2 需要額外開(kāi)辟一個(gè)內(nèi)存實(shí)體存放,并將 o.y 的對(duì)象指針指向該內(nèi)存實(shí)體。
如果是 32 位操作系統(tǒng),用32位表示smi 可以理解,可是64位操作系統(tǒng)中,為什么 smi 范圍也是 -2³¹ 到 2³¹-1(2³¹≈2*10⁹)?
ECMAScript
標(biāo)準(zhǔn)約定 number
數(shù)字需要被當(dāng)成 64 位雙精度浮點(diǎn)數(shù)處理,但事實(shí)上,一直使用 64 位去存儲(chǔ)任何數(shù)字實(shí)際是非常低效的(空間低效,計(jì)算時(shí)間低效 smi
大量使用位運(yùn)算),所以 JavaScript
引擎并不總會(huì)使用 64 位去存儲(chǔ)數(shù)字,引擎在內(nèi)部可以采用其他內(nèi)存表示方式(如 32 位),只要保證數(shù)字外部所有能被監(jiān)測(cè)到的特性對(duì)齊 64 位的表現(xiàn)就行。
const cycleLimit = 50000 console.time('heapNumber') const foo = { x: 1.1 }; for (let i = 0; i < cycleLimit; ++i) { // 創(chuàng)建了多出來(lái)的heapNumber實(shí)例 foo.x += 1; } console.timeEnd('heapNumber') // slow console.time('smi') const bar = { x: 1.0 }; for (let i = 0; i < cycleLimit; ++i) { bar.x += 1; } console.timeEnd('smi') // fast
疑問(wèn)點(diǎn):
const BasicVarGen = function () { this.smi1 = 1 this.smi2 = 2 this.heapNumber1 = 1.1 this.heapNumber2 = 2.1 } let foo = new BasicVarGen() let bar = new BasicVarGen() debugger baz.obj1.heapNumber1 ++
在數(shù)字中,一個(gè)數(shù)字的值都沒(méi)有修改,其他的數(shù)字地址也都變了。
五、小結(jié):基本類型到底存在哪里?
字符串: 存在堆里,棧中為引用地址,如果存在相同字符串,則引用地址相同。
數(shù)字: 小整數(shù)存在棧中,其他類型存在堆中。
其他類型:引擎初始化時(shí)分配唯一地址,棧中的變量存的是唯一的引用。
這里只能算是大概講明白了基本類型存在哪里,
到此這篇關(guān)于深入內(nèi)存原理談JS中變量存儲(chǔ)在堆中還是棧中的文章就介紹到這了,更多相關(guān)JS中變量存儲(chǔ)在堆中還是棧中內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript中document.activeELement焦點(diǎn)元素介紹
這篇文章主要給大家分享 JavaScript中document.activeELement焦點(diǎn)元素介紹,下面文章圍繞了document.activeElement屬性展開(kāi)詳細(xì)內(nèi)容,需要的朋友可以參考一下,希望對(duì)大家有所幫助2021-11-11前端項(xiàng)目中監(jiān)聽(tīng)localStorage的變化
這篇文章主要為大家介紹了前端項(xiàng)目中監(jiān)聽(tīng)localStorage的變化的解決思路詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06

wasm+js實(shí)現(xiàn)文件獲取md5示例詳解

JavaScript中Reduce10個(gè)常用場(chǎng)景技巧

JS實(shí)現(xiàn)簡(jiǎn)單的操作桿旋轉(zhuǎn)示例詳解