<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          由一道騰訊面試題聊聊 V8 引擎的垃圾回收

          共 7775字,需瀏覽 16分鐘

           ·

          2021-03-14 03:00

          騰訊題目:

          function test({  
            var a = 1;  
            var b = {};  
            var c = {a: a};  
            return c;
          }  

          執(zhí)行test后,說一說,堆棧發(fā)生了啥變化,最后c的內(nèi)存會被回收嗎?

          題目來自:https://github.com/sisterAn/JavaScript-Algorithms/issues/157

          前言

          我們知道,JavaScript之所以能在瀏覽器環(huán)境和NodeJS環(huán)境運行,都是因為有V8引擎在幕后保駕護航。從編譯、內(nèi)存分配、運行以及垃圾回收等整個過程,都離不開它。

          在寫這篇文章之前,我也在網(wǎng)上看了很多博客,包括一些英文原版的內(nèi)容,于是想通過這篇文章來做一個歸納整理,文中加入了我自己的思考,以及純手工制作流程圖~~

          希望這篇文章能幫到你,同時本文也會收錄到我自己的個人網(wǎng)站。

          為什么要有垃圾回收

          在C語言和C++語言中,我們?nèi)绻胍_辟一塊堆內(nèi)存的話,需要先計算需要內(nèi)存的大小,然后自己通過malloc函數(shù)去手動分配,在用完之后,還要時刻記得用free函數(shù)去清理釋放,否則這塊內(nèi)存就會被永久占用,造成內(nèi)存泄露。

          但是我們在寫JavaScript的時候,卻沒有這個過程,因為人家已經(jīng)替我們封裝好了,V8引擎會根據(jù)你當前定義對象的大小去自動申請分配內(nèi)存。

          不需要我們?nèi)ナ謩庸芾韮?nèi)存了,所以自然要有垃圾回收,否則的話只分配不回收,豈不是沒多長時間內(nèi)存就被占滿了嗎,導致應用崩潰。

          垃圾回收的好處是不需要我們?nèi)ス芾韮?nèi)存,把更多的精力放在實現(xiàn)復雜應用上,但壞處也來自于此,不用管理了,就有可能在寫代碼的時候不注意,造成循環(huán)引用等情況,導致內(nèi)存泄露。

          內(nèi)存結(jié)構(gòu)分配

          由于V8最開始就是為JavaScript在瀏覽器執(zhí)行而打造的,不太可能遇到使用大量內(nèi)存的場景,所以它可以申請的最大內(nèi)存就沒有設置太大,在64位系統(tǒng)下大約為1.4GB,在32位系統(tǒng)下大約為700MB。

          在NodeJS環(huán)境中,我們可以通過**process.memoryUsage()**來查看內(nèi)存分配。

          clipboard.png

          process.memoryUsage返回一個對象,包含了 Node 進程的內(nèi)存占用信息。該對象包含四個字段,含義如下:

          clipboard.png
          rss(resident set size):所有內(nèi)存占用,包括指令區(qū)和堆棧

          heapTotal:V8引擎可以分配的最大堆內(nèi)存,包含下面的 heapUsed

          heapUsed:V8引擎已經(jīng)分配使用的堆內(nèi)存

          external:V8管理C++對象綁定到JavaScript對象上的內(nèi)存

          以上所有內(nèi)存單位均為字節(jié)(Byte)。

          如果說想要擴大Node可用的內(nèi)存空間,可以使用Buffer等堆外內(nèi)存內(nèi)存,這里不詳細說明了,大家有興趣可以去看一些資料。

          下面是Node的整體架構(gòu)圖,有助于大家理解上面的內(nèi)容:

          clipboard.png
          Node Standard Library: 是我們每天都在用的標準庫,如Http, Buffer 模塊

          Node Bindings: 是溝通JS 和 C++的橋梁,封裝V8和Libuv的細節(jié),向上層提供基礎API服務

          第三層是支撐 Node.js 運行的關鍵,由 C/C++ 實現(xiàn):
          1. V8 是Google開發(fā)的JavaScript引擎,提供JavaScript運行環(huán)境,可以說它就是 Node.js 的發(fā)動機
          2. Libuv 是專門為Node.js開發(fā)的一個封裝庫,提供跨平臺的異步I/O能力
          3. C-ares:提供了異步處理 DNS 相關的能力
          4. http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、數(shù)據(jù)壓縮等其他的能力

          垃圾回收機制

          如何判斷是否可以回收

          1.1 標記清除

          當變量進入環(huán)境(例如,在函數(shù)中聲明一個變量)時,就將這個變量標記為“進入環(huán)境”。從邏輯上講,永遠不能釋放進入環(huán)境的變量所占用的內(nèi)存,因為只要執(zhí)行流進入相應的環(huán)境,就可能會用到它們。而當變量離開環(huán)境時,則將其標記為“離開環(huán)境”。

          可以使用任何方式來標記變量。比如,可以通過翻轉(zhuǎn)某個特殊的位來記錄一個變量何時進入環(huán)境,或者使用一個“進入環(huán)境的”變量列表及一個“離開環(huán)境的”變量列表來跟蹤哪個變量發(fā)生了變化。如何標記變量并不重要,關鍵在于采取什么策略。

          • (1)垃圾收集器在運行的時候會給存儲在內(nèi)存中的所有變量都加上標記(當然,可以使用任何標記方式)。
          • (2)然后,它會去掉運行環(huán)境中的變量以及被環(huán)境中變量所引用的變量的標記
          • (3)此后,依然有標記的變量就被視為準備刪除的變量,原因是在運行環(huán)境中已經(jīng)無法訪問到這些變量了。
          • (4)最后,垃圾收集器完成內(nèi)存清除工作,銷毀那些帶標記的值并回收它們所占用的內(nèi)存空間。

          目前,IE、Firefox、Opera、Chrome和Safari的JavaScript實現(xiàn)使用的都是標記清除式的垃圾回收策略(或類似的策略),只不過垃圾收集的時間間隔互有不同。

          clipboard.png

          活動對象就是上面的root,如果不清楚活動對象的可以先查一下資料,當一個對象和其關聯(lián)對象不再通過引用關系被當前root引用了,這個對象就會被垃圾回收。

          1.2 引用計數(shù)

          引用計數(shù)的垃圾收集策略不太常見。含義是跟蹤記錄每個值被引用的次數(shù)。當聲明了一個變量并將一個引用類型值賦給該變量時,則這個值的引用次數(shù)就是1。

          如果同一個值又被賦給另一個變量,則該值的引用次數(shù)加1。相反,如果包含對這個值引用的變量改變了引用對象,則該值引用次數(shù)減1。

          當這個值的引用次數(shù)變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其占用的內(nèi)存空間回收回來。

          這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數(shù)為0的值所占用的內(nèi)存。

          Netscape Navigator 3.0是最早使用引用計數(shù)策略的瀏覽器,但很快它就遇到了一個嚴重的問題:循環(huán)引用。

          循環(huán)引用是指對象A中包含一個指向?qū)ο驜的指針,而對象B中也包含一個指向?qū)ο驛的引用,看個例子:

          function foo ({
              var objA = new Object();
              var objB = new Object();
              
              objA.otherObj = objB;
              objB.anotherObj = objA;
          }

          這個例子中,objA和objB通過各自的屬性相互引用,也就是說,這兩個對象的引用次數(shù)都是2。

          在采用標記清除策略的實現(xiàn)中,由于函數(shù)執(zhí)行后,這兩個對象都離開了作用域,因此這種相互引用不是問題。

          但在采用引用次數(shù)策略的實現(xiàn)中,當函數(shù)執(zhí)行完畢后,objA和objB還將繼續(xù)存在,因為它們的引用次數(shù)永遠不會是0。

          加入這個函數(shù)被重復多次調(diào)用,就會導致大量內(nèi)存無法回收。為此,Netscape在Navigator 4.0中也放棄了引用計數(shù)方式,轉(zhuǎn)而采用標記清除來實現(xiàn)其垃圾回收機制。

          還要注意的是,我們大部分人時刻都在寫著循環(huán)引用的代碼,看下面這個例子,相信大家都這樣寫過:

          var el = document.getElementById('#el');
          el.onclick = function (event{
              console.log('element was clicked');
          }

          我們?yōu)橐粋€元素的點擊事件綁定了一個匿名函數(shù),我們通過event參數(shù)是可以拿到相應元素el的信息的。

          大家想想,這是不是就是一個循環(huán)引用呢?el有一個屬性onclick引用了一個函數(shù)(其實也是個對象),函數(shù)里面的參數(shù)又引用了el,這樣el的引用次數(shù)一直是2,即使當前這個頁面關閉了,也無法進行垃圾回收。

          如果這樣的寫法很多很多,就會造成內(nèi)存泄露。我們可以通過在頁面卸載時清除事件引用,這樣就可以被回收了:

          var el = document.getElementById('#el');
          el.onclick = function (event{
              console.log('element was clicked');
          }

          // ...
          // ...

          // 頁面卸載時將綁定的事件清空
          window.onbeforeunload = function(){
              el.onclick = null;
          }

          V8垃圾回收策略

          自動垃圾回收有很多算法,由于不同對象的生存周期不同,所以無法只用一種回收策略來解決問題,這樣效率會很低。

          所以,V8采用了一種代回收的策略,將內(nèi)存分為兩個生代:新生代(new generation)老生代(old generation)。

          新生代中的對象為存活時間較短的對象,老生代中的對象為存活時間較長或常駐內(nèi)存的對象,分別對新老生代采用不同的垃圾回收算法來提高效率,對象最開始都會先被分配到新生代(如果新生代內(nèi)存空間不夠,直接分配到老生代),新生代中的對象會在滿足某些條件后,被移動到老生代,這個過程也叫晉升,后面我會詳細說明。

          分代內(nèi)存

          默認情況下,32位系統(tǒng)新生代內(nèi)存大小為16MB,老生代內(nèi)存大小為700MB,64位系統(tǒng)下,新生代內(nèi)存大小為32MB,老生代內(nèi)存大小為1.4GB。

          新生代平均分成兩塊相等的內(nèi)存空間,叫做semispace,每塊內(nèi)存大小8MB(32位)或16MB(64位)。

          新生代

          1. 分配方式

          新生代存的都是生存周期短的對象,分配內(nèi)存也很容易,只保存一個指向內(nèi)存空間的指針,根據(jù)分配對象的大小遞增指針就可以了,當存儲空間快要滿時,就進行一次垃圾回收。

          2. 算法

          新生代采用Scavenge垃圾回收算法,在算法實現(xiàn)時主要采用Cheney算法。

          Cheney算法將內(nèi)存一分為二,叫做semispace,一塊處于使用狀態(tài),一塊處于閑置狀態(tài)。

          clipboard.png

          處于使用狀態(tài)的semispace稱為From空間,處于閑置狀態(tài)的semispace稱為To空間。

          我畫了一套詳細的流程圖,接下來我會結(jié)合流程圖來詳細說明Cheney算法是怎么工作的。垃圾回收在下面我統(tǒng)稱為 GC(Garbage Collection)。

          step1. 在From空間中分配了3個對象A、B、C

          step2. GC進來判斷對象B沒有其他引用,可以回收,對象A和C依然為活躍對象

          step3. 將活躍對象A、C從From空間復制到To空間

          step4. 清空From空間的全部內(nèi)存

          step5. 交換From空間和To空間

          step6. 在From空間中又新增了2個對象D、E

          step7. 下一輪GC進來發(fā)現(xiàn)對象D沒有引用了,做標記

          step8. 將活躍對象A、C、E從From空間復制到To空間

          step9. 清空From空間全部內(nèi)存

          step10. 繼續(xù)交換From空間和To空間,開始下一輪

          通過上面的流程圖,我們可以很清楚的看到,進行From和To交換,就是為了讓活躍對象始終保持在一塊semispace中,另一塊semispace始終保持空閑的狀態(tài)。

          Scavenge由于只復制存活的對象,并且對于生命周期短的場景存活對象只占少部分,所以它在時間效率上有優(yōu)異的體現(xiàn)。Scavenge的缺點是只能使用堆內(nèi)存的一半,這是由劃分空間和復制機制所決定的。

          由于Scavenge是典型的犧牲空間換取時間的算法,所以無法大規(guī)模的應用到所有的垃圾回收中。但我們可以看到,Scavenge非常適合應用在新生代中,因為新生代中對象的生命周期較短,恰恰適合這個算法。

          3. 晉升

          當一個對象經(jīng)過多次復制仍然存活時,它就會被認為是生命周期較長的對象。這種較長生命周期的對象隨后會被移動到老生代中,采用新的算法進行管理。

          對象從新生代移動到老生代的過程叫作晉升。

          對象晉升的條件主要有兩個:

          1. 對象從From空間復制到To空間時,會檢查它的內(nèi)存地址來判斷這個對象是否已經(jīng)經(jīng)歷過一次Scavenge回收。如果已經(jīng)經(jīng)歷過了,會將該對象從From空間移動到老生代空間中,如果沒有,則復制到To空間。總結(jié)來說,如果一個對象是第二次經(jīng)歷從From空間復制到To空間,那么這個對象會被移動到老生代中。
          2. 當要從From空間復制一個對象到To空間時,如果To空間已經(jīng)使用了超過25%,則這個對象直接晉升到老生代中。設置25%這個閾值的原因是當這次Scavenge回收完成后,這個To空間會變?yōu)镕rom空間,接下來的內(nèi)存分配將在這個空間中進行。如果占比過高,會影響后續(xù)的內(nèi)存分配。

          老生代

          1. 介紹

          在老生代中,存活對象占較大比重,如果繼續(xù)采用Scavenge算法進行管理,就會存在兩個問題:

          1. 由于存活對象較多,復制存活對象的效率會很低。
          2. 采用Scavenge算法會浪費一半內(nèi)存,由于老生代所占堆內(nèi)存遠大于新生代,所以浪費會很嚴重。

          所以,V8在老生代中主要采用了Mark-SweepMark-Compact相結(jié)合的方式進行垃圾回收。

          2. Mark-Sweep

          Mark-Sweep是標記清除的意思,它分為標記和清除兩個階段。

          與Scavenge不同,Mark-Sweep并不會將內(nèi)存分為兩份,所以不存在浪費一半空間的行為。Mark-Sweep在標記階段遍歷堆內(nèi)存中的所有對象,并標記活著的對象,在隨后的清除階段,只清除沒有被標記的對象。

          也就是說,Scavenge只復制活著的對象,而Mark-Sweep只清除死了的對象?;顚ο笤谛律兄徽驾^少部分,死對象在老生代中只占較少部分,這就是兩種回收方式都能高效處理的原因。

          我們還是通過流程圖來看一下:

          step1. 老生代中有對象A、B、C、D、E、F

          clipboard.png

          step2. GC進入標記階段,將A、C、E標記為存活對象

          clipboard.png

          step3. GC進入清除階段,回收掉死亡的B、D、F對象所占用的內(nèi)存空間

          clipboard.png

          可以看到,Mark-Sweep最大的問題就是,在進行一次清除回收以后,內(nèi)存空間會出現(xiàn)不連續(xù)的狀態(tài)。這種內(nèi)存碎片會對后續(xù)的內(nèi)存分配造成問題。

          如果出現(xiàn)需要分配一個大內(nèi)存的情況,由于剩余的碎片空間不足以完成此次分配,就會提前觸發(fā)垃圾回收,而這次回收是不必要的。

          2. Mark-Compact

          為了解決Mark-Sweep的內(nèi)存碎片問題,Mark-Compact就被提出來了。

          **Mark-Compact是標記整理的意思,**是在Mark-Sweep的基礎上演變而來的。Mark-Compact在標記完存活對象以后,會將活著的對象向內(nèi)存空間的一端移動,移動完成后,直接清理掉邊界外的所有內(nèi)存。如下圖所示:

          step1. 老生代中有對象A、B、C、D、E、F(和Mark—Sweep一樣)

          clipboard.png

          step2. GC進入標記階段,將A、C、E標記為存活對象(和Mark—Sweep一樣)

          clipboard.png

          step3. GC進入整理階段,將所有存活對象向內(nèi)存空間的一側(cè)移動,灰色部分為移動后空出來的空間

          clipboard.png

          step4. GC進入清除階段,將邊界另一側(cè)的內(nèi)存一次性全部回收

          clipboard.png

          3. 兩者結(jié)合

          在V8的回收策略中,Mark-Sweep和Mark-Conpact兩者是結(jié)合使用的。

          由于Mark-Conpact需要移動對象,所以它的執(zhí)行速度不可能很快,在取舍上,V8主要使用Mark-Sweep,在空間不足以對從新生代中晉升過來的對象進行分配時,才使用Mark-Compact。

          總結(jié)

          V8的垃圾回收機制分為新生代和老生代。

          新生代主要使用Scavenge進行管理,主要實現(xiàn)是Cheney算法,將內(nèi)存平均分為兩塊,使用空間叫From,閑置空間叫To,新對象都先分配到From空間中,在空間快要占滿時將存活對象復制到To空間中,然后清空From的內(nèi)存空間,此時,調(diào)換From空間和To空間,繼續(xù)進行內(nèi)存分配,當滿足那兩個條件時對象會從新生代晉升到老生代。

          老生代主要采用Mark-Sweep和Mark-Compact算法,一個是標記清除,一個是標記整理。兩者不同的地方是,Mark-Sweep在垃圾回收后會產(chǎn)生碎片內(nèi)存,而Mark-Compact在清除前會進行一步整理,將存活對象向一側(cè)移動,隨后清空邊界的另一側(cè)內(nèi)存,這樣空閑的內(nèi)存都是連續(xù)的,但是帶來的問題就是速度會慢一些。在V8中,老生代是Mark-Sweep和Mark-Compact兩者共同進行管理的。

          以上就是本文的全部內(nèi)容,書寫過程中參考了很多中外文章,參考書籍包括樸大大的《深入淺出NodeJS》以及《JavaScript高級程序設計》等。我們這里并沒有對具體的算法實現(xiàn)進行探討,感興趣的朋友可以繼續(xù)深入研究一下。

          最后,謝謝大家能夠讀到這里,如果文中有任何不明確或錯誤的地方,歡迎給我留言~~

          文章來源:https://segmentfault.com/a/1190000014383214

          歡迎將解答提交至https://github.com/sisterAn/JavaScript-Algorithms/issues/157

          最后

          歡迎關注【前端瓶子君】??ヽ(°▽°)ノ?
          回復「算法」,加入前端算法源碼編程群,每日一刷(工作日),每題瓶子君都會很認真的解答喲
          回復「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對你有幫助,在看」是最大的支持
          》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持
          瀏覽 53
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久久久久久久黄色视频 | 黄色导航在线观看 | 淫色大吊人妖乱伦视频 | 一级性爱AV | 欧美后门菊门交3p |