<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垃圾回收機制

          共 753字,需瀏覽 2分鐘

           ·

          2022-04-30 02:00

          術(shù)????


          對于C/C++等底層語言,內(nèi)存需要手動進行申請,使用完后手動進行釋放。而對于javascript語言使用者來說,因為有垃圾回收器的工作,在使用中通常不需要關(guān)心內(nèi)存的使用情況。但有時不當(dāng)?shù)拇a會意外的導(dǎo)致變量未被垃圾回收器回收,積少成多后造成內(nèi)存泄漏,潛在的提高應(yīng)用卡頓的風(fēng)險。本文從垃圾回收器的工作原理進行分析,總結(jié)可能造成內(nèi)存泄漏的幾個典型場景,避免工作中出現(xiàn)內(nèi)存泄漏造成應(yīng)用卡頓。

          內(nèi)存生命周期

          無論哪種編程語言,內(nèi)存的生命周期都是差不多的:申請內(nèi)存、使用內(nèi)存(讀寫)、釋放或歸還內(nèi)存。

          為什么需要垃圾回收

          顯而易見,用戶設(shè)備內(nèi)存是有限的,只申請不釋放,內(nèi)存被占滿時,就無法給新創(chuàng)建的對象分配內(nèi)存。這里類比我們?nèi)ス臼程贸燥埖膱鼍埃捍蝻埡笳业娇瘴唬ㄉ暾垉?nèi)存)、在空位吃飯(使用內(nèi)存)、吃完飯收拾餐盤放回回收區(qū)(釋放內(nèi)存)。想象一下,我們吃完不收拾餐盤(釋放內(nèi)存),后來的人就沒有餐桌可以吃飯了(程序崩潰)。

          對于大多數(shù)學(xué)校和公司食堂,都是使用者吃完飯釋放餐桌(收拾餐盤放回回收區(qū)),和C/C++等底層語言類似,使用者申請內(nèi)存空間,使用完畢再釋放內(nèi)存。

          如果我們?nèi)ネ膺叢宛^吃飯,也是同樣的流程。只不過不需要自己找餐桌,由引導(dǎo)服務(wù)員給分配,使用后,不需要關(guān)心留在餐桌上的餐盤,由回收餐盤服務(wù)員去回收。對于JS 來說,垃圾回收器(Garbage Collector)就在做類似于餐盤服務(wù)員垃圾回收的工作:將不再使用的內(nèi)存進行釋放回收,從而能夠循環(huán)利用有限的內(nèi)存空間。

          function?grow()?{

          ???var?x?=?[]

          ???let?str?=?new?Array(100000).join('x');
          ???//?1億個
          ???for?(let?i=0;?i<100000000;?i++)?{
          ??????x.push(str)
          ???}
          }

          document.getElementById('grow').addEventListener('click',?grow);

          以上面這段代碼為例,點擊 grow按鈕后,會向數(shù)組x中存入大量的(一億個)字符串,然后這個tab就崩潰了。看到下圖,大概率是內(nèi)存超過了瀏覽器單 tab的內(nèi)存上限。以chrome為例,其單tab內(nèi)存上限在32位系統(tǒng)上為 512M,64位系統(tǒng)上為1.4GB左右。

          變量存儲方式

          JS中變量分為原始類型和引用類型,不同的變量類型存儲方式不同。我們先回顧一下 JS 是如何存儲變量的。原始類型直接存儲在棧(Stack)中,引用類型存儲在堆(Heap)中。

          var?a?=?1;

          function?doSomething()?{
          ????let?b?=?2;
          ????let?obj?=?{?c:?3}
          ????console.log(a,?b);
          }

          doSomething();

          以上面的一段代碼為例,全局執(zhí)行上下文中存在一個值類型變量 a,doSomething 函數(shù)執(zhí)行上下文中存在一個值類型變量b,一個引用類型變量obj。從下面的內(nèi)存分配圖可以看到,值類型直接存儲在棧中,引用類型存儲在堆中。

          棧內(nèi)存垃圾回收

          棧內(nèi)存回收相對來說很簡單,函數(shù)執(zhí)行完畢后,該函數(shù)執(zhí)行上下文從棧中彈出,存儲在執(zhí)行上下文中的變量立即被回收掉。還是以上面的一段代碼為例,當(dāng) doSomething 執(zhí)行完畢后,內(nèi)存結(jié)構(gòu)如下圖:

          doSomething 執(zhí)行上下文被彈出,該執(zhí)行上下文中所有變量都被銷毀回收。對于值類型b來說,就直接釋放了其占用的內(nèi)存,對于引用類型obj來說,銷毀的只是變量obj對堆內(nèi)存地址 1001 的引用,obj的值 { c: 3 } 依然存在于堆內(nèi)存中。那么堆內(nèi)存中的變量如何進行回收呢?

          堆內(nèi)存垃圾回收

          代際假說(Generational Hypothesis)

          代際假說認(rèn)為,大部分新對象的生存時間比較短,在一次垃圾回收周期內(nèi)被回收。

          基于此,V8 將堆內(nèi)存分為新生代和老生代。新生代又將內(nèi)存分為 Nursery 和 Intermediate兩個區(qū)域。新對象存放到Nursery區(qū)域中,經(jīng)過一次垃圾回收,存活的對象被復(fù)制到 Intermediate 區(qū)域。經(jīng)過兩次垃圾回收仍然存活的對象將被移動到老生代中。有點像我們上學(xué)的過程,從幼兒園到小學(xué)到中學(xué)。

          主垃圾回收器(Major GC)

          垃圾回收器有一些基本的任務(wù):識別活動對象(marking)、回收或重用垃圾對象內(nèi)存(sweeping)、整理碎片內(nèi)存(defragment)。

          標(biāo)記階段(Marking)

          標(biāo)記階段通過變量是否可達(dá)(reachability),判斷是否為活動對象。通常為從一個根對象進行遞歸遍歷,所有遍歷到的對象都是可達(dá)的,為活動對象。沒有遍歷到的對象為非活動對象,需要進行回收。

          var?obj1?=?{?a:?1};
          var?obj2?=?=?{?b:?2};

          執(zhí)行如下代碼后obj2失去對 1002 的引用,在垃圾回收器遍歷完之后發(fā)現(xiàn)沒有對 1002 這塊內(nèi)存的引用變量,標(biāo)記為其非活動變量。

          obj2?=?null;

          清除階段(Sweeping)

          GC會維護一個 freeList 列表,將非活動對象占用的內(nèi)存片段地址添加到 freeList。有新對象申請內(nèi)存時,freeList里有合適大小的內(nèi)存塊,會優(yōu)先分配給新對象。

          整理階段(Defragmenting)

          這個階段是可選的。內(nèi)存在經(jīng)過垃圾回收之后,活動對象將內(nèi)存塊分割的很零碎,這個時候會進行整理,將活動對象復(fù)制到相同連續(xù)的內(nèi)存區(qū)域內(nèi)。

          副垃圾回收器(Minor GC)

          副垃圾回收器負(fù)責(zé)新生代垃圾回收。主要有四個步驟:標(biāo)記、復(fù)制、更新指針、切換角色。新生代將內(nèi)存分為 from space(Nursery) 和 to space (Intermediate)。當(dāng)有新對象申請內(nèi)存,會分配from space 區(qū)域中的地址,to space 區(qū)域為備用區(qū)域。

          標(biāo)記階段同主垃圾回收器,將可達(dá)對象標(biāo)記為活動對象。

          復(fù)制階段將from space中標(biāo)記的活動對象復(fù)制到 to space區(qū)域,并給活動對象做標(biāo)記,此時其已經(jīng)位于 intermediate中,下一次垃圾回收時如果仍為活動對象,就要被復(fù)制到老生代中。

          將活動對象復(fù)制到 to space 中之后,需要更新指針引用地址,這樣原引用才能保證正確的指向。

          最后切換 from space 和 to space 的角色。在下一次垃圾回收周期后,存活兩次的對象會被復(fù)制到老生代區(qū)域。

          GC執(zhí)行時機

          在最初,GC運行在主線程,與 JS交替執(zhí)行。在GC執(zhí)行階段,主線程停止JS代碼執(zhí)行,這稱為全停頓(Stop-the-World)。如果垃圾回收器需要處理(標(biāo)記-復(fù)制-整理)的對象比較多,就需要比較長的時間才能完成一次周期內(nèi)的任務(wù)。在這期間如果有更高優(yōu)的任務(wù)需要執(zhí)行,是無法及時響應(yīng)的,比如用戶輸入、動畫的執(zhí)行,給用戶的感覺就是卡頓。

          提高GC執(zhí)行效率

          Goal: Free Main Thread

          Orinoco是 Google 垃圾回收器(Garbage Collector)的項目代號,致力于研究如何提高垃圾回收效率。經(jīng)過多年的發(fā)展,產(chǎn)出了三種能有效提高垃圾回收效率的方案:并行(Parallel)、增量標(biāo)記(Incremental)、并發(fā)(Concurrent)。

          并行(Parallel)

          在主線程執(zhí)行垃圾回收任務(wù)的同時,開幾個輔助線程同時進行,這樣可以大大減少主線程全停頓(Stop the World)的時間。

          增量(increment)

          將主線程垃圾回收任務(wù)分成多個小任務(wù),與JS交替執(zhí)行。這種方式并沒有縮短GC工作的時間,但是給了JS響應(yīng)高優(yōu)任務(wù)的時間,避免了出現(xiàn)卡頓。

          并發(fā)(concurrent)

          并發(fā)是主線程專注執(zhí)行JS, 開啟輔助線程進行垃圾回收。這種方式?jīng)]有了全停頓,完全解放主線程,實現(xiàn)了 Free Main Thread 的目標(biāo)。

          幾個典型場景

          通過了解V8垃圾回收機制,我們知道垃圾回收器會和JS線程爭奪資源和時間。V8也在不斷通過更先進的技術(shù)來減少全停頓(Stop the World)的時間。對于我們開發(fā)者來說,能做的就是盡量減少GC的工作負(fù)擔(dān)。總結(jié)來說就是,變量不用之后立即釋放。下面我們總結(jié)了幾種容易造成內(nèi)存泄漏的bad case,大家在工作中可以規(guī)避。

          減少全局變量

          下面這段代碼,函數(shù)作用域中變量未使用關(guān)鍵字聲明,導(dǎo)致非嚴(yán)格模式下掛載到全局作用域。這樣foo()函數(shù)執(zhí)行完畢之后,由于 window.bar的引用一直存在,導(dǎo)致被GC識別為活動對象。這樣只要程序在運行,該對象的內(nèi)存就會一直存在無法被回收,增加垃圾回收器的工作負(fù)擔(dān)。

          //?非嚴(yán)格模式下,bar會被掛在全局上
          function?foo(arg)?{
          ????bar?=?{?a:?1?};
          ????this.obj?=?{?b:?1};
          ????console.log(bar,?obj);
          }

          foo();

          對于這種情況建議開啟嚴(yán)格模式,或者使用 lint工具檢查這種錯誤。

          及時清理對DOM的引用

          有了React和Vue這種UI庫,我們就很少直接操作DOM了。在我們業(yè)務(wù)中,需要對富文本內(nèi)的一些內(nèi)容進行操作中,有很多直接操作DOM的場景。在操作完DOM之后,需及時清掉對DOM節(jié)點的引用,不然也會造成對內(nèi)存的泄露。


          ??type="text"?id="input">
          ??"node">


          ??

          事件監(jiān)聽&計時器

          在我們業(yè)務(wù)中經(jīng)常需要在組件掛載后給元素添加事件監(jiān)聽。這時需要在組件卸載時將監(jiān)聽事件移除,來避免無用的內(nèi)存消耗。

          componentDidMount()?{
          ????this.myScaleBar?.addEventListener('mousedown',?this.handleMouseDown);
          ????document.addEventListener('mousemove',?this.handleMouseMove);
          ????document.addEventListener('mouseup',?this.handleMouseUp);
          }

          componentDidMount()?{
          ????this.myScaleBar?.addEventListener('mousedown',?this.handleMouseDown);
          ????document.addEventListener('mousemove',?this.handleMouseMove);
          ????document.addEventListener('mouseup',?this.handleMouseUp);
          }

          如何查看是否存在內(nèi)存泄漏

          chrome devtools 中的 performance 面板可以記錄內(nèi)存使用的timeLine, 在錄制之前選中內(nèi)存,報告中會有內(nèi)存的使用情況。我們主要關(guān)注JS堆中內(nèi)存的使用情況。

          我們以下面這段代碼為例,通過點擊grow按鈕,會向grow 函數(shù)內(nèi)的變量x 內(nèi)添加大量的長度為100000的字符串。


          "en">

          ???內(nèi)存測試


          ???

          ??????"grow">grow
          ???


          ???


          記錄開始后先點擊【強制垃圾回收】,然后點擊grow,記錄一段時間后再點擊【強制垃圾回收】后查看報告??梢钥吹降诙卫厥张c操作之前的內(nèi)存相等,說明沒有垃圾泄漏。

          我們再稍微改一下代碼,看一下內(nèi)存的使用情況。

          function?grow()?{
          ??x?=?[];

          ??let?str?=?new?Array(100000).join('x');

          ??for?(let?i=0;?i<100000000;?i++)?{
          ????x.push(str)
          ??}
          }

          記錄發(fā)現(xiàn)強制垃圾回收之后,內(nèi)存的占用要高于grow函數(shù)執(zhí)行之前。與上面第一次記錄的區(qū)別是,grow內(nèi)變量 x 的聲明沒有使用關(guān)鍵字聲明,非嚴(yán)格模式下直接掛載到window上。這樣grow函數(shù)執(zhí)行完畢,全局對 x依然 的引用,GC無法回收 x 占用的內(nèi)存。

          總結(jié)

          V8垃圾回收器幫助JS使用者周期性的回收不再使用的內(nèi)存。過多的對象會對垃圾回收器造成額外的負(fù)擔(dān),甚至影響到主線程JS的執(zhí)行,造成頁面的卡頓。作為開發(fā)者應(yīng)該有意識的減少全局變量的數(shù)量、及時移除不再使用DOM引用、事件監(jiān)聽及計時器,來減少垃圾回收器的負(fù)擔(dān)。

          參考資料

          [1]

          Trash talk: the Orinoco garbage collector · V8: https://v8.dev/blog/trash-talk

          [2]

          代際假說: https://www.memorymanagement.org/glossary/g.html#term-generational-hypothesis

          [3]

          代際垃圾回收器: https://www.memorymanagement.org/glossary/g.html#term-generational-garbage-collection

          ?? 謝謝支持

          以上便是本次分享的全部內(nèi)容,希望對你有所幫助^_^

          喜歡的話別忘了?分享、點贊、收藏?三連哦~。

          歡迎關(guān)注公眾號?趣談前端?收貨大廠一手好文章~

          ???H5-Dooring,讓H5制作更簡單


          目前H5-Dooring架構(gòu)升級, 已支持多種搭建布局模式, 如網(wǎng)格布局,?自由布局, 可以一鍵切換布局模式:



          歡迎體驗:?http://h5.dooring.cn/h5_plus

          ???

          便內(nèi),^_^

          ?、、?~。

          關(guān)號?趣談前端?獲前端~

          瀏覽 33
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  吴梦梦无码一区二区三区首发新作 | 插入白丝袜舞蹈生妹妹的嫩穴网站 | 手机A……V在线观看 | 国产精彩视频在线 | 1000精品无码 |