寫在前面
工欲善其事,必先利其器,本文之器非器具之器,乃容器也,言歸正傳,作為一個前端打工人,左手剛 const 定義常量,忠貞不二,轉(zhuǎn)頭就 new 幾個對象,玩的火熱,真是個優(yōu)秀的 jser,風(fēng)騷的操作背后,必有日夜不輟的 QWER,外加一個走 A,廢話不多說,瀏覽器內(nèi)核是啥玩意?還不知道都有啥瀏覽器內(nèi)核?那就先來看看瀏覽器內(nèi)核。
瀏覽器內(nèi)核
提到瀏覽器內(nèi)核,Blink、Weikit、Gecko、Trident 張口就來,這些只是各個瀏覽器內(nèi)核的組成部分之一渲染引擎,對應(yīng)的還有 JavaScript引擎,簡單羅列一下:
| 瀏覽器 | 渲染引擎 | Javascript 引擎 |
|---|---|---|
| Chrome | Blink(13 年之前使用的是 Safari 的 Webkit, Blink 是谷歌與歐朋一起搞的) | V8 |
| Safari | Webkit | JavaScriptCore |
| Firefox | Gecko | SpiderMonkey--OdinMonkey |
| IE | Trident | Chakra |
渲染引擎和 JS 引擎相互協(xié)作,打造出瀏覽器顯示的頁面,看下圖:
簡單看看就行,不重要,既然是講垃圾回收( Garbage Collection 簡稱 GC ),那就要先去回收站了,回收站有個學(xué)名叫:內(nèi)存,計算機五大硬件之一存儲器的核心之一,見下圖:
說句更不重要的,JS 是沒有能力管理內(nèi)存和垃圾回收的,一切都要依賴各個瀏覽器的 JS 引擎,所以為了逼格更高一點,就不要說 JS 垃圾回收了,你看,我說 V8 垃圾回收,是不是厲害多了(摸了摸越來越?jīng)]有阻力的腦袋)。
內(nèi)存分配
棧
簡單說,棧內(nèi)存,小且存儲連續(xù),操作起來簡單方便,一般由系統(tǒng)自動分配,自動回收,所以文章內(nèi)所說的垃圾回收,都是基于堆內(nèi)存。
堆
堆內(nèi)存,大(相對棧來說)且不連續(xù)。
V8 中內(nèi)存分類
在講內(nèi)存分配之前,先了解一下弱分代假說,V8 的垃圾回收主要建立在這個假說之上。
概念:
- 絕大部分的對象生命周期都很短,即存活時間很短
- 生命周期很長的對象,基本都是常駐對象
基于以上兩個概念,將內(nèi)存分為新生代 (new space)與老生代 (old space)兩個區(qū)域。劃重點,記一下。
垃圾回收
新生代
新生代(32 位系統(tǒng)分配 16M 的內(nèi)存空間,64 位系統(tǒng)翻倍 32M,不同瀏覽器可能不同,但是應(yīng)該差不了多少)。
新生代對應(yīng)存活時間很短的假說概念,這個空間的操作,非常頻繁,絕大多數(shù)對象在這里經(jīng)歷一次生死輪回,基本消亡,沒消亡的會晉升至老生代內(nèi)。
新生代算法為 Scavenge 算法,典型犧牲空間換時間的敗家玩意,怎么說呢?首先他將新生代分為兩個相等的半空間( semispace ) from space 與 to space,來看看這個敗家玩意,是怎么操作的,他使用寬度優(yōu)先算法,是寬度優(yōu)先,記住了不。兩個空間,同一時間內(nèi),只會有一個空間在工作( from space ),另一個在休息( to space )。
首先,V8 引擎中的垃圾回收器檢測到 from space 空間快達到上限了,此時要進行一次垃圾回收了
然后,從根部開始遍歷,不可達對象(即無法遍歷到的對象)將會被標記,并且復(fù)制未被標記的對象,放到 to space 中
最后,清除 from space 中的數(shù)據(jù),同時將 from space 置為空閑狀態(tài),即變成 to space,相應(yīng)的 to space 變成 from space,俗稱翻轉(zhuǎn)
也是,你說空間都給他了,他愛咋地處理就咋地處理唄,總不可能強迫王校長開二手奧拓吧,當然了,對于小對象,這么來一次,時間的優(yōu)勢那是杠杠的,雖然浪費了一半空間,但是問題不大,能 hold 住。
當然優(yōu)秀的 V8 是不可能容忍,一個對象來回的在 form space 和 to space 中蹦跶的,當經(jīng)歷一次 form => to 翻轉(zhuǎn)之后,發(fā)現(xiàn)某些未被標記的對象居然還在,會直接扔到老生代里面去,好似后浪參加比賽,晉級了,優(yōu)秀的嘞。
除了上面一種情況,還有一個情況也會晉級,當一個對象,在被復(fù)制的時候,大于 to space 空間的 25% 的時候,也會晉級了,這種自帶背景的選手,那是不敢動的,直接晉級到老生代。
老生代
老生代( 32 位操作系統(tǒng)分配大約 700M 內(nèi)存空間,64 位翻倍 1.4G,一樣,每個瀏覽器可能會有差異,但是差不了多少)。
老生代比起新生代可是要復(fù)雜的多,所謂能者多勞,空間大了,責(zé)任就大了,老生代可以分為以下幾個區(qū)域:
- old object space 即大家口中的老生代,不是全部老生代,這里的對象大部分是由新生代晉升而來
- large object space 大對象存儲區(qū)域,其他區(qū)域無法存儲下的對象會被放在這里,基本是超過 1M 的對象,這種對象不會在新生代對象中分配,直接存放到這里,當然了,這么大的數(shù)據(jù),復(fù)制成本很高,基本就是在這里等待命運的降臨不可能接受僅僅是知其然,而不知其所以然
- Map space 這個玩意,就是存儲對象的映射關(guān)系的,其實就是隱藏類,啥是隱藏類?就不告訴你(不知道的大佬已經(jīng)去百度了)
- code space 簡單點說,就是存放代碼的地方,編譯之后的代碼,是根據(jù)大佬們寫的代碼編譯出來的代碼
看個圖,休息一下:
講了這么多基本概念,聊聊最后的老生代回收算法,老生代回收算法為:標記和清除/整理(mark-sweep/mark-compact)。
在標記的過程中,引入了概念:三色標記法,三色為:
- 白:未被標記的對象,即不可達對象(沒有掃描到的對象),可回收
- 灰:已被標記的對象(可達對象),但是對象還沒有被掃描完,不可回收
- 黑:已被掃描完(可達對象),不可回收
當然,既然要標記,就需要提供記錄的坑位,在 V8 中分配的每一個內(nèi)存頁中創(chuàng)建了一個 marking bitmap 坑位。
大致的流程為:
- 首先將所有的非根部對象全部標記為白色,然后使用深度優(yōu)先遍歷,是深度優(yōu)先哈,和新生代不一樣哈,按深度優(yōu)先搜索沿途遍歷,將訪問到的對象,直接壓入棧中,同時將標記結(jié)果放在 marking bitmap (灰色) 中,一個對象遍歷完成,直接出棧,同時在 marking bitmap 中記錄為黑色,直到棧空為止,來張圖,休息一下
標記完成后,接下來就是等待垃圾回收器來清除了,清除完了之后,會在原來的內(nèi)存區(qū)域留下一大堆不連續(xù)的空間,小對象還好說,這個時候如果來一個稍微大一點的對象,沒有內(nèi)存可以放的下這個傻大個了,怎么辦?只能觸發(fā) GC,但是吧,原來清除的不連續(xù)的空間加起來又可以放的下這個傻大個,很可惜啊,啟動一次 GC 性能上也是嗖嗖的往下掉啊;V8 能容許這樣的事發(fā)生?肯定不存在嘛!
所以在清除完之后,新生代中對象,再一次分配到老生代并且內(nèi)存不足的時候,會優(yōu)先觸發(fā)標記整理(mark-compact), 在標記結(jié)束后,他會將可達對象(黑色),移到內(nèi)存的另一端,其他的內(nèi)存空間就不會被占用,直接釋放,等下次再有對象晉升的時候,輕松放下。
看到這里各位大佬可能會有疑問,那要是我 GC 搞完之后,再來個對象,滿了咋辦,你說咋辦,直接崩好不好,這個時候就需要大佬們寫代碼的時候,要珍惜內(nèi)存了,對內(nèi)存就像珍惜你的女朋友一樣,啥?沒有女朋友?那就沒辦法了,原則上是決不了這個問題的。
基本的內(nèi)存和垃圾回收是交代完了,其中還有一些概念,還是要說一下的,接著往下看!
寫屏障
想一個問題,當 GC 想回收新生代中的內(nèi)容的時候,某些對象,只有一個指針指向了他,好巧不巧的是,這個指針還是老生代那邊對象指過來的,怎么搞?我想回收這個玩意,難道要遍歷一下老生代中的對象嗎?這不是開玩笑嗎?為了回收這一個玩意,我需要遍歷整個老生代,代價著實太大,搞不起,搞不起,那怎么辦哩?
V8 引擎中有個概念稱作寫屏障,在寫入對象的地方有個緩存列表,這個列表內(nèi)記錄了所有老生代指向新生代的情況,當然了新生成的對象,并不會被記錄,只有老生代指向新生代的對象,才會被寫入這個緩存列表。
在新生代中觸發(fā) GC 遇到這樣的對象的時候,會首先讀一下緩存列表,這相比遍歷老生代所有的對象,代價實在是太小了,這操作值得一波 666,很優(yōu)秀,當然了,關(guān)于 V8 引擎內(nèi)在的優(yōu)化,還有很多很多,各位大佬可以慢慢去了解。
全停頓(stop-the-world)
關(guān)于全停頓,本沒有必要單獨來講,但是,I happy 就 good。
在以往,新/老生代都包括在內(nèi),為了保證邏輯和垃圾回收的情況不一致,需要停止 JS 的運行,專門來遍歷去遍歷/復(fù)制,標記/清除,這個停頓就是:全停頓。
這就比較惡心了,新生代也就算了,本身內(nèi)存不大,時間上也不明顯,但是在老生代中,如果遍歷的對象太多,太大,用戶在此時,是有可能明顯感到頁面卡頓的,體驗嘎嘎差。
所以在 V8 引擎在名為 Orinoco 項目中,做了三個事情,當然只針對老生代,新生代這個后浪還是可以的,效率賊拉的高,優(yōu)化空間不大。三個事情分別是:
- 增量標記
將原來一口氣去標記的事情,做成分步去做,每次內(nèi)存占用達到一定的量或者多次進入寫屏障的時候,就暫時停止 JS 程序,做一次最多幾十毫秒的標記 marking,當下次 GC 的時候,反正前面都標記好了,開始清除就行了
- 并行回收
從字面意思看并行,就是在一次全量垃圾回收的過程中,就是 V8 引擎通過開啟若干輔助線程,一起來清除垃圾,可以極大的減少垃圾回收的時間,很優(yōu)秀,手動點贊
- 并發(fā)回收
并發(fā)就是在 JS 主線程運行的時候,同時開啟輔助線程,清理和主線程沒有任何邏輯關(guān)系的垃圾,當然,需要寫屏障來保障
小結(jié)
V8 引擎做的優(yōu)化有很多,還有比如多次( 2 次)在新生代中能夠存活下來的對象,會被記錄下來,在下次 GC 的時候,會被直接晉升到老生代,還有比如新晉升的對象,直接標記為黑色,這是因為新晉升的對象存活下來的概率非常高,這兩種情況就算是不再使用,再下下次的時候也會被清除掉,影響不大,但是這個過程,第一種就省了新生代中的一次復(fù)制輪回,第二種就省了 marking 的過程,在此類對象比較多的情況下,還是比較有優(yōu)勢的。
最后一句
終于,寫完了,本來想著寫的更詳細一些,但是那樣篇幅會很大,下次吧,有機會的話再寫寫 V8 執(zhí)行的過程或者 V8 創(chuàng)建對象都干了些啥玩意什么什么的,其實 V8 引擎(或者各個 JS 引擎)這個東西太龐大了,我了解的也是冰山一角,所以文章肯定有不準確的地方,歡迎大佬們嚴正指正,積極交流。