<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>

          淺談JS內(nèi)存機(jī)制

          共 2823字,需瀏覽 6分鐘

           ·

          2022-04-23 17:11

          術(shù)??堅(jiān)??

          前言

          隨著web的發(fā)展與普及,前端頁(yè)面不僅只加載在瀏覽器上,也慢慢流行于各種app的webview里。尤其在如今設(shè)備性能越來(lái)越好的條件下,前端頁(yè)面更是開(kāi)始在app中擔(dān)任重要的角色。如此一來(lái),前端頁(yè)面的停留時(shí)間變得更長(zhǎng),我們理應(yīng)越發(fā)重視前端的內(nèi)存管理,防止內(nèi)存泄露,提高頁(yè)面的性能。

          想要打造高性能前端應(yīng)用,防止崩潰,就必須得搞清楚JS的內(nèi)存機(jī)制,其實(shí)就是弄清楚JS內(nèi)存的分配與回收。

          JS數(shù)據(jù)存儲(chǔ)機(jī)制

          內(nèi)存空間

          從圖中可以看出, 在 JavaScript 的執(zhí)行過(guò)程中, 主要有三種類型內(nèi)存空間,分別是代碼空間、??臻g和堆空間。

          代碼空間:用來(lái)存放可執(zhí)行代碼

          棧空間:一塊連續(xù)的內(nèi)存區(qū)域,容量較小,讀取速度快,被設(shè)計(jì)成先進(jìn)后出結(jié)構(gòu)

          堆空間:不連續(xù)的內(nèi)存區(qū)域,容量較大,用于儲(chǔ)存大數(shù)據(jù),讀取速度慢

          數(shù)據(jù)類型

          JavaScript 發(fā)展至今總共有八種數(shù)據(jù)類型,其中 Object 類型稱為引用類型,其余七種稱為基本類型,Object 是由其余七種基本類型組成的kv結(jié)構(gòu)數(shù)據(jù)。

          棧空間和堆空間

          ??臻g其實(shí)就是 JavaScript 中的調(diào)用棧,是用來(lái)儲(chǔ)存執(zhí)行上下文,以及存儲(chǔ)執(zhí)行上下文中的一些基本類型中的小數(shù)據(jù),如下圖所示:

          image.png

          變量環(huán)境: 存放var聲明與函數(shù)聲明的變量空間,編譯時(shí)就能確定,不受塊級(jí)作用域影響

          詞法環(huán)境: 存放let與const聲明的變量空間,編譯時(shí)不能完全確定,受塊級(jí)作用域影響

          而堆空間,則是用來(lái)儲(chǔ)存大數(shù)據(jù)如引用類型,然后把他們的引用地址保存到??臻g的變量中,所以多了這一道中轉(zhuǎn),JavaScript 對(duì)堆空間數(shù)據(jù)的讀取自然會(huì)比??臻g數(shù)據(jù)的要慢,可以用下圖表示兩者關(guān)系:

          通常情況下,??臻g都不會(huì)設(shè)置太大,這是因?yàn)?JavaScript 引擎需要用棧來(lái)維護(hù)程序執(zhí)行期間上下文的狀態(tài),如果??臻g大了的話,所有的數(shù)據(jù)都存放在??臻g里面,那么會(huì)影響到上下文切換的效率,進(jìn)而又影響到整個(gè)程序的執(zhí)行效率。

          閉包

          內(nèi)部函數(shù)總是可以訪問(wèn)其外部函數(shù)中聲明的變量,當(dāng)通過(guò)調(diào)用一個(gè)外部函數(shù)返回一個(gè)內(nèi)部函數(shù)后,即使該外部函數(shù)已經(jīng)執(zhí)行結(jié)束了,但是內(nèi)部函數(shù)引用外部函數(shù)的變量依然保存在內(nèi)存中,我們就把這些變量的集合稱為閉包

          閉包中的數(shù)據(jù)會(huì)組成一個(gè)對(duì)象,然后保存在堆空間中,如:

          可以利用開(kāi)發(fā)者工具查看閉包情況,其中括號(hào)中的名稱就是產(chǎn)生閉包的函數(shù)名。一般我們會(huì)認(rèn)為閉包是返回的內(nèi)部函數(shù)引用的變量集合,但閉包有一個(gè)較為迷惑的情況,如下:

          可以理解為,如果函數(shù)存在閉包,其所有內(nèi)部函數(shù)都會(huì)擁有一個(gè)指向這個(gè)閉包的引用,即所有內(nèi)部函數(shù)會(huì)共享同一個(gè)閉包,只要任意內(nèi)部函數(shù)有引用外部函數(shù)中聲明的變量,這個(gè)變量都會(huì)被納入閉包內(nèi),而且最內(nèi)部的函數(shù)會(huì)持有所有外部的閉包。

          堆棧存放的數(shù)據(jù)類型

          原始類型的數(shù)據(jù)是存放在棧中,引用類型的數(shù)據(jù)是存放在堆中?

          上面這句話是用來(lái)描述棧中數(shù)據(jù)的存儲(chǔ)情況,調(diào)用棧中的引用類型存放在堆中,相信大家都沒(méi)有問(wèn)題,但是原始類型真的都存放在棧中嗎?

          數(shù)字

          V8把數(shù)字分成兩種類型:smi 和 heapNumber

          smi是范圍為 :-231 到 231-1的整數(shù),在棧中直接存值;除了smi,其余數(shù)字類型都是heapNumber,需要另外開(kāi)辟堆空間進(jìn)行儲(chǔ)存,變量保存其引用。

          var?times?=?50000;
          var?smi_in_stack?=?1;
          var?heap_number?=?1.1;

          //?about?1.5~1.6ms,?fast
          console.time('smi_in_stack');
          for?(let?i?=?0;?i?times;?i++)?{
          ??smi_in_stack++;
          }
          console.timeEnd('smi_in_stack');

          //?about?2.1~2.5ms,?slow
          console.time('heap_number');
          for?(let?i?=?0;?i?times;?i++)?{
          ??heap_number++;
          }
          console.timeEnd('heap_number');

          同時(shí)我們可以通過(guò)heap snapshots觀察到heap_number的存在,所以驗(yàn)證了棧中的heapNumber值是存在堆中,smi值是直接存在棧中。

          更基本的基本類型

          V8定義了一種 oddball[1] 類型,屬于 oddball 類型的有null、undefined、true和false

          function?BasicType()?{
          ??this.oddBall1?=?true;
          ??this.oddBall2?=?false;
          ??this.oddBall3?=?undefined;
          ??this.oddBall4?=?null;
          ??this.oddBall5?=?'';
          }
          const?obj1?=?new?BasicType();
          const?obj2?=?new?BasicType();

          這里可以看到oddball類型以及空字符串的堆引用全部都是一個(gè)固定值,代表在V8跑起來(lái)的第一時(shí)間,不管我們有沒(méi)有聲明這些基本類型,他們都已經(jīng)在堆中被創(chuàng)建完畢了。由此猜想棧中這些類型使用的也是堆中的地址。

          function?Obj()?{
          ??this.string?=?'str';
          ??this.num1?=?1;
          ??this.num2?=?1.1;
          ??this.bigInt?=?BigInt('1');
          ??this.symbol?=?Symbol('1');
          }
          const?obj?=?new?Obj();
          debugger;
          obj.string?=?'other?str';
          obj.num1?=?2;
          obj.num2?=?1;
          obj.bigInt?=?BigInt('2');
          obj.symbol?=?Symbol('2');

          debugger后內(nèi)存快照

          其中bigInt、string、symbol的內(nèi)存地址都進(jìn)行了更換,由此可以猜想是因?yàn)檫@三種類型占用的內(nèi)存大小不是一個(gè)固定值,需要根據(jù)其值進(jìn)行動(dòng)態(tài)分配,所以內(nèi)存地址會(huì)進(jìn)行更換;而heapNumber的內(nèi)存地址并沒(méi)有發(fā)生變化,這個(gè)更換值的操作還是在原來(lái)的內(nèi)存空間中進(jìn)行。因?yàn)闂J且粔K連續(xù)的內(nèi)存空間,不希望運(yùn)行中會(huì)產(chǎn)生內(nèi)存碎片,由此可以得出bigInt、string、symbol這些內(nèi)存大小不固定的類型在棧中也是保存其堆內(nèi)存的引用。同時(shí)我們?cè)跅V锌梢月暶骱艽蟮膕tring,如果string存放在棧中明顯也不合理

          故??臻g中的基本類型儲(chǔ)存位置如下:

          類型儲(chǔ)存位置
          Numbersmi儲(chǔ)存棧中,heapNumber儲(chǔ)存堆中
          String
          Boolean
          Null
          undefined
          BigInit
          Symbol

          上述結(jié)論主要是從heap snapshots和棧的特性中得出,畢竟最正確的答案是在源碼中獲得,如有不當(dāng),請(qǐng)指正。

          JS內(nèi)存回收

          棧內(nèi)存回收

          function?fn1()?{
          ??//....
          ??function?fn2()?{
          ????//...
          ??}
          ??fn2();
          }
          fn1();

          調(diào)用棧中有一個(gè)記錄當(dāng)前執(zhí)行狀態(tài)的指針(稱為 ESP),隨著函數(shù)的執(zhí)行,函數(shù)執(zhí)行上下文被壓入調(diào)用棧中,執(zhí)行上下文中的數(shù)據(jù)會(huì)按照前面說(shuō)的JS數(shù)據(jù)存儲(chǔ)機(jī)制被分配到堆棧中,ESP會(huì)指向最后壓棧的執(zhí)行上下文,如左圖所示的fn2函數(shù)。當(dāng)fn2函數(shù)調(diào)用完畢,JS 會(huì)把ESP指針下移至fn1函數(shù),這個(gè)指針下移的操作就是銷毀fn1函數(shù)執(zhí)行上下文的過(guò)程。最后fn1函數(shù)執(zhí)行上下文所占用的區(qū)域會(huì)變成無(wú)效區(qū)域,下一個(gè)函數(shù)執(zhí)行上下文壓入調(diào)用棧的時(shí)候會(huì)直接覆蓋其內(nèi)存空間。簡(jiǎn)而言之,只要函數(shù)調(diào)用結(jié)束,該棧內(nèi)存就會(huì)自動(dòng)被回收,不需要我們操心。剛剛我們也聊到閉包,如果出現(xiàn)閉包的情況,閉包的數(shù)據(jù)就會(huì)組成一個(gè)對(duì)象保存在堆空間里。

          堆內(nèi)存回收

          內(nèi)存垃圾回收領(lǐng)域中有個(gè)重要術(shù)語(yǔ):代際假說(shuō),其有以下兩個(gè)特點(diǎn):

          1. 大部分對(duì)象在內(nèi)存中存在的時(shí)間很短,簡(jiǎn)單來(lái)說(shuō),就是很多對(duì)象一經(jīng)分配內(nèi)存,很快就變得不可訪問(wèn);
          1. 不死的對(duì)象,會(huì)活得更久。

          基于代際假說(shuō),JS 把堆空間分成新生代和老生代兩個(gè)區(qū)域,新生代中存放的是生存時(shí)間短的對(duì)象,通常只支持 1~8M 的容量;老生代中存放的生存時(shí)間長(zhǎng)的對(duì)象,一些大的數(shù)據(jù)也會(huì)被直接分配到老生區(qū)中。而針對(duì)這兩個(gè)區(qū)域,JS 存在兩個(gè)垃圾回收器:主垃圾處理器和副垃圾處理器。這里先說(shuō)說(shuō)垃圾回收一般都有相同的執(zhí)行流程:

          1. 標(biāo)記空間中活動(dòng)對(duì)象和非活動(dòng)對(duì)象
          1. 回收非活動(dòng)對(duì)象所占據(jù)的內(nèi)存
          1. 內(nèi)存整理,這步是可選的,因?yàn)橛械睦厥掌鞴ぷ鬟^(guò)程會(huì)產(chǎn)生內(nèi)存碎片,這時(shí)就需要內(nèi)存整理防止不夠連續(xù)空間分配給大數(shù)據(jù)

          副垃圾回收器

          副垃圾回收器主要是采用 Scavenge 算法進(jìn)行新生區(qū)的垃圾回收,它把新生區(qū)劃分為兩個(gè)區(qū)域:對(duì)象區(qū)域和空閑區(qū)域,新加入的對(duì)象都會(huì)存放到對(duì)象區(qū)域,當(dāng)對(duì)象區(qū)域快被寫滿時(shí),會(huì)對(duì)對(duì)象區(qū)域進(jìn)行垃圾標(biāo)記,把存活對(duì)象復(fù)制并有序排列至空閑區(qū)域,完成后讓這兩個(gè)區(qū)域角色互轉(zhuǎn),由此便能無(wú)限循環(huán)進(jìn)行垃圾回收。同時(shí)存在對(duì)象晉升策略,也就是經(jīng)過(guò)兩次垃圾回收依然還存活的對(duì)象,會(huì)被移動(dòng)到老生區(qū)中。

          主垃圾回收器

          由于老生區(qū)空間大,數(shù)據(jù)大,所以不適用 Scavenge 算法,主要是采用標(biāo)記-整理算法,其工作流程是從一組根元素開(kāi)始,遞歸遍歷這組根元素,在這個(gè)遍歷過(guò)程中,能到達(dá)的元素稱為活動(dòng)對(duì)象,沒(méi)有到達(dá)的元素就可以判斷為垃圾數(shù)據(jù)。接著讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。垃圾回收工作是需要占用主線程的,必須暫停JS腳本執(zhí)行等待垃圾回收完成后恢復(fù),這種行為稱為全停頓。 由于老生代內(nèi)存大,全停頓對(duì)性能的影響非常大,所以出現(xiàn)了增量標(biāo)記的策略進(jìn)行老生區(qū)的垃圾回收。

          JS內(nèi)存泄漏

          由于棧內(nèi)存會(huì)隨著函數(shù)調(diào)用結(jié)束而被釋放(覆蓋),所以JS中的內(nèi)存泄漏一般發(fā)生在堆中。之前有同學(xué)分享過(guò)一篇關(guān)于內(nèi)存泄漏的文章 ,里面講到一些常見(jiàn)內(nèi)存泄漏的原因和監(jiān)測(cè)手段,這里我就不贅述,但是可以根據(jù)最近的IM工作講一些實(shí)踐:

          確認(rèn)是否有內(nèi)存泄漏的情況

          1. 本地打包一個(gè)去掉壓縮、擁有sourcemap及沒(méi)有任何console的生產(chǎn)版本(console會(huì)保留對(duì)象引用,阻礙銷毀;去掉壓縮和保留sourcemap有利于定位源碼)
          1. 啟動(dòng)本地服務(wù)器,使cef訪問(wèn)本地項(xiàng)目
          1. 不斷操作和記錄heap snapshots,觀察snapshots和timeline情況
          1. 最終內(nèi)存從22.5m上升至34.6m,conversation實(shí)例從443上升至1117,message實(shí)例從443上升至1287,而該用戶實(shí)際只有221個(gè)會(huì)話
          1. 不斷在會(huì)話間切換,通過(guò)timeline看到有內(nèi)存沒(méi)被釋放,而且生成detached dom

          通過(guò)上述觀測(cè),可以判斷為有內(nèi)存泄漏情況。

          確定內(nèi)存泄漏排查方式

          IM頁(yè)分為:會(huì)話列表,會(huì)話頂欄,消息列表,輸入框四部分。使用逐一排查法縮小排查范圍,排查各個(gè)部分內(nèi)存情況。如:先保留會(huì)話列表,注釋其余三個(gè)部分,操作會(huì)話列表并使用timeline和heap snapshots進(jìn)行內(nèi)存排查。按照這一方法逐步排查四個(gè)部分組件,并針對(duì)各個(gè)組件進(jìn)行優(yōu)化??梢院?jiǎn)單歸納成一個(gè)通用步驟:

          1. 使用timeline進(jìn)行錄制,觀察是否像上面那樣有不被釋放的內(nèi)存區(qū)域
          1. 選擇不被釋放的區(qū)域進(jìn)行查看,先找自己項(xiàng)目中的錨點(diǎn)物:像我們IM數(shù)據(jù)都是用conversation和messsage對(duì)象進(jìn)行儲(chǔ)存,所以可以先進(jìn)行這兩個(gè)對(duì)象的搜索查看
          1. 如果沒(méi)有好的錨點(diǎn)物也沒(méi)關(guān)系,接著查看detached dom(畢竟很多事件綁定在dom中,事件中引用著數(shù)據(jù),造成無(wú)法被釋放)和 string

          有些detached dom可能是react虛擬dom的數(shù)據(jù),但像上面的Detached HTMLAudioElement會(huì)隨著操作一直增加,所以這個(gè)是不正常的。

          像這里string的重復(fù),經(jīng)排查是有相同conversation和message對(duì)象引起

          堆快照里包含太多運(yùn)行時(shí)、上下文等信息,實(shí)在太難從中找到有用的信息,所以會(huì)把目標(biāo)放在錨點(diǎn)物、detached dom和string上

          1. 利用heap snapshot 的comparison模式過(guò)濾出操作階段內(nèi)存變更情況,更有利于查找影響位置

          上面是個(gè)人進(jìn)行內(nèi)存泄露排查整理的方法,如果你有更好的方法,歡迎交流∠(°ゝ°)

          React中一個(gè)需要注意的內(nèi)存泄漏問(wèn)題

          現(xiàn)象: 當(dāng)組件被銷毀后,仍有一些異步事件調(diào)用組件中setState方法

          原理: 組件銷毀后,再調(diào)用setstate方法會(huì)保留相關(guān)引用,造成內(nèi)存泄漏

          //?測(cè)試代碼
          const?[test,?setTest]?=?useState(null);
          useEffect(()?=>?{
          ??(async?()?=>?{
          ????//?這里表達(dá)一個(gè)異步操作如:xhr、fetch、promise等等
          ????await?sleep(3000);
          ????const?obj?=?new?TestObj();
          ????setTest(obj);
          ??})();
          },?[]);

          如果把代碼改成這樣,就不會(huì)造成內(nèi)存泄漏:

          const?[test,?setTest]?=?useState(null);
          useEffect(()?=>?{
          ??let?unMounted?=?false;
          ??(async?()?=>?{
          ????await?sleep(3000);
          ????if?(unMounted)?return;
          ????const?obj?=?new?TestObj();
          ????setTest(obj);
          ??})();
          ??return?()?=>?{
          ????unMounted?=?true;
          ??};
          },?[]);

          這是在開(kāi)發(fā)環(huán)境測(cè)試的,翻看源碼發(fā)現(xiàn)react只會(huì)在開(kāi)發(fā)模式保留這些引用,然后拋出warning來(lái)提醒開(kāi)發(fā)者這里可能有內(nèi)存泄漏的問(wèn)題(如這些setState是注冊(cè)在全局事件里或者setInterval里的調(diào)用),生產(chǎn)環(huán)境是不會(huì)對(duì)其進(jìn)行引用,所以不需要額外進(jìn)行處理也不會(huì)造成內(nèi)存泄漏

          react18更是直接把這個(gè)報(bào)錯(cuò)給干掉,以免誤導(dǎo)開(kāi)發(fā)者使用剛剛說(shuō)的類似手段來(lái)進(jìn)行避免報(bào)錯(cuò),這里有做解釋:https://github.com/facebook/react/pull/22114

          總結(jié)

          本文先是講述js類型在內(nèi)存空間的儲(chǔ)存位置,接著探討堆棧中的內(nèi)存是如何進(jìn)行回收,最后描述內(nèi)存泄漏確定和排查的方法,也補(bǔ)充一個(gè)react中有關(guān)setState造成“內(nèi)存泄漏”的例子。內(nèi)存泄漏在復(fù)雜應(yīng)用中是難以避免的,個(gè)人排查也只能是解決一些比較明顯的內(nèi)存泄漏現(xiàn)象。所以為了更好地解決這個(gè)應(yīng)用內(nèi)內(nèi)存泄漏問(wèn)題,必須做好線上監(jiān)控,利用廣大用戶操作數(shù)據(jù),發(fā)現(xiàn)內(nèi)存泄漏問(wèn)題,進(jìn)而不斷改善應(yīng)用的性能。

          參考資料

          1. https://developer.chrome.com/docs/devtools/memory-problems/memory-101/
          1. https://www.cnblogs.com/goloving/p/15352261.html
          1. https://hashnode.com/post/does-javascript-use-stack-or-heap-for-memory-allocation-or-both-cj5jl90xl01nh1twuv8ug0bjk
          1. https://www.ditdot.hr/en/causes-of-memory-leaks-in-javascript-and-how-to-avoid-them

          參考資料

          [1]

          oddball: https://github.com/v8/v8/blob/c736a452575f406c9a05a8c202b0708cb60d43e5/src/objects.h#L9368

          - END -

          ???

          便內(nèi),對(duì)^_^

          ?、點(diǎn)、?~。

          關(guān)號(hào)?趣談前端?~

          瀏覽 32
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  久久手机免费视频 | 久久秘 一区二区三区四区 | 一区二区无码区 | 日韩 欧美 国产高清91 | 又粗又大又黄又爽无遮挡 |