<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)存泄漏與垃圾回收機制

          共 6887字,需瀏覽 14分鐘

           ·

          2022-04-15 16:54

          前言

          不管什么程序語言,內(nèi)存生命周期基本是一致的:

          1. 分配你所需要的內(nèi)存
          2. 使用分配到的內(nèi)存(讀、寫)
          3. 不需要時將其釋放\歸還

          所有語言 第二部分都是明確的,第一和第三部分在 底層語言 中是明確的。但在像 JavaScript 這些高級語言中,大部分都是隱含的。

          注意:JavaScript 是一種高級的解釋執(zhí)行的編程語言,是一種屬于網(wǎng)絡的高級腳本語言。

          在 Chrome 瀏覽器中,V8 被限制了內(nèi)存的使用(64位約1.4G/1464MB , 32位約0.7G/732MB),限制的主要原因是 V8 最初為瀏覽器而設計,不太可能遇到用大量內(nèi)存的場景;更深層原因是 V8 垃圾回收機制的限制:清理大量的內(nèi)存垃圾很耗時間,這樣會引起 JavaScript 線程暫停執(zhí)行,導致性能和應用直線下降。

          JavaScript 是在創(chuàng)建變量(對象,字符串等)時自動進行了分配內(nèi)存,并且在不使用它們時“自動”釋放。釋放的過程稱為垃圾回收。

          這個“自動”是混亂的根源,并讓 JavaScript 開發(fā)者錯誤的感覺他們可以不關心內(nèi)存管理,進而引發(fā)內(nèi)存泄漏。

          一、什么是內(nèi)存泄漏

          程序的運行需要內(nèi)存。只要程序提出要求,操作系統(tǒng)或者運行時(runtime)就必須供給內(nèi)存。

          對于持續(xù)運行的服務進程,必須及時釋放不再用到的內(nèi)存。否則,內(nèi)存占用越來越高,輕則影響系統(tǒng)性能,重則導致進程崩潰。

          不再用到的內(nèi)存,沒有及時釋放,就叫做內(nèi)存泄漏(memory leak)。

          二、常見的內(nèi)存泄漏

          寫得不好的JavaScript可能出現(xiàn)難以察覺且有害的內(nèi)存泄漏問題。

          在內(nèi)存有限的設備上,或者在函數(shù)會被調(diào)用很多次的情況下,內(nèi)存泄漏可能是個大問題。JavaScript中的內(nèi)存泄漏大部分是由不合理的引用導致的。

          下面來講一講常見的內(nèi)存泄漏:

          1. 意外聲明的全局變量

          意外聲明全局變量是最常見但也最容易修復的內(nèi)存泄漏問題。下面的代碼沒有使用任何關鍵字聲明變量:

          function?setName()?{
          ??name?=?'yuanyuan';
          }

          此時,解釋器會把變量 name 當作 window 的屬性來創(chuàng)建(相當于 window.name = 'yuanyuan' )。可想而知,在 window 對象上創(chuàng)建的屬性,只要 window 本身不被清理就不會消失。這個問題很容易解決,只要在變量聲明前頭加上 varletconst 關鍵字即可,這樣變量就會在函數(shù)執(zhí)行完畢后離開作用域。

          2. 被遺忘的定時器

          定時器也可能會悄悄地導致內(nèi)存泄漏。下面的代碼中,定時器的回調(diào)通過閉包引用了外部變量:

          let?name?=?'yuanyuan';?
          setInterval(()?=>?{
          ??console.log(name);?
          },?100);

          只要定時器一直運行,回調(diào)函數(shù)中引用的 name 就會一直占用內(nèi)存。垃圾回收程序當然知道這一點,因而就不會清理外部變量。

          3. 使用不當?shù)拈]包

          使用 JavaScript 閉包很容易在不知不覺間造成內(nèi)存泄漏。請看下面的例子:

          let?outer?=?function()?{?
          ??let?name?=?'yuanyuan';?
          ??return?function()?{?
          ????return?name;?
          ??};?
          };

          這會導致分配給 name 的內(nèi)存被泄漏。以上代碼創(chuàng)建了一個內(nèi)部閉包,只要 outer 函數(shù)存在就不能清理 name ,因為閉包一直在引用著它。假如 name 的內(nèi)容很大(不止是一個小字符串),那可能就是個大問題了。

          4. 未清理的 DOM 引用

          DOM 元素的生命周期正常情況下取決于是否掛載在 DOM 樹上,當元素從 DOM 樹上移除時,就可以被銷毀回收了。

          但如果某個 DOM 元素在 JS 中也持有它的引用,想要徹底刪除這個元素,就需要把兩個引用都清除,這樣才能正常回收它。

          //?在對象中引用?DOM
          var?elements?=?{
          ??btn:?document.getElementById('btn'),
          }
          function?doSomeThing()?{
          ??elements.btn.click()
          }

          function?removeBtn()?{
          ??//?移除?DOM?樹中的?btn
          ??document.body.removeChild(document.getElementById('button'))
          ??//?但是此時全局變量?elements?還是保留了對?btn?的引用,?btn?還是存在于內(nèi)存中,不能被?GC?回收
          }

          雖然別的地方刪除了,但是對象中還存在對 dom 的引用。

          解決方法是刪除 DOM 節(jié)點時,也要釋放 JS 對節(jié)點的引用:

          elements.btn?=?null;

          三、垃圾回收機制

          有些語言(比如 C 語言)必須手動釋放內(nèi)存,程序員負責內(nèi)存管理。

          char?*?buffer;
          buffer?=?(char*)?malloc(42);

          //?Do?something?with?buffer

          free(buffer);

          上面是 C 語言代碼,malloc方法用來申請內(nèi)存,使用完畢之后,必須自己用free方法釋放內(nèi)存。

          這很麻煩,所以大多數(shù)語言提供自動內(nèi)存管理,減輕程序員的負擔,這被稱為"垃圾回收機制"(garbage collector)。

          JavaScript 是使用垃圾回收的語言,也就是說執(zhí)行環(huán)境負責在代碼執(zhí)行時管理內(nèi)存。在C和C++等語言中,跟蹤內(nèi)存使用對開發(fā)者來說是個很大的負擔,也是很多問題的來源。JavaScript為開發(fā)者卸下了這個負擔,通過自動內(nèi)存管理實現(xiàn)內(nèi)存分配和閑置資源回收。基本思路很簡單:確定哪個變量不會再使用,然后釋放它占用的內(nèi)存。這個過程是周期性的,即垃圾回收程序每隔一定時間(或者說在代碼執(zhí)行過程中某個預定的收集時間)就會自動運行。垃圾回收過程是一個近似且不完美的方案,因為某塊內(nèi)存是否還有用,屬于“不可判定的”問題,意味著靠算法是解決不了的。

          我們以函數(shù)中局部變量的正常生命周期為例。函數(shù)中的局部變量會在函數(shù)執(zhí)行時存在。此時,棧(或堆)內(nèi)存會分配空間以保存相應的值。函數(shù)在內(nèi)部使用了變量,然后退出。此時,就不再需要那個局部變量了,它占用的內(nèi)存可以釋放,供后面使用。這種情況下顯然不再需要局部變量了,但并不是所有時候都會這么明顯。垃圾回收程序必須跟蹤記錄哪個變量還會使用,以及哪個變量不會再使用,以便回收內(nèi)存。如何標記未使用的變量也許有不同的實現(xiàn)方式。不過,在瀏覽器的發(fā)展史上,用到過兩種主要的標記策略:標記清理和引用計數(shù)。

          1. 標記清理

          JavaScript 最常用的垃圾回收策略是標記清理(mark-and-sweep)。當變量進入上下文,比如在函數(shù)內(nèi)部聲明一個變量時,這個變量會被加上存在于上下文中的標記。而不在上下文中的變量,比如在函數(shù)外部聲明的全局變量,邏輯上講,永遠不應該釋放它們的內(nèi)存,因為只要上下文中的代碼在運行,就有可能用到它們。當變量離開上下文時,也會被加上離開上下文的標記。

          給變量加標記的方式有很多種。比如,當變量進入上下文時,反轉(zhuǎn)某一位;或者可以維護“在上下文中”和“不在上下文中”兩個變量列表,可以把變量從一個列表轉(zhuǎn)移到另一個列表。標記過程的實現(xiàn)并不重要,關鍵是策略。

          垃圾回收程序運行的時候,會標記內(nèi)存中存儲的所有變量(記住,標記方法有很多種)。然后,它會將所有在上下文中的變量,以及被在上下文中的變量引用的變量的標記去掉。在此之后再被加上標記的變量就是待刪除的了,原因是任何在上下文中的變量都訪問不到它們了。隨后垃圾回收程序做一次內(nèi)存清理,銷毀帶標記的所有值并收回它們的內(nèi)存。

          到了2008年,IE、Firefox、Opera、Chrome 和 Safari 都在自己的 JavaScript 實現(xiàn)中采用標記清理(或其變體),只是在運行垃圾回收的頻率上有所差異。

          下面在介紹引用計數(shù)時會通過代碼來更深入的了解和對比兩種策略。

          2. 引用計數(shù)

          另一種沒那么常用的垃圾回收策略是引用計數(shù)(referencecounting)。其思路是對每個值都記錄它被引用的次數(shù)。聲明變量并給它賦一個引用值時,這個值的引用數(shù)為1。如果同一個值又被賦給另一個變量,那么引用數(shù)加1。類似地,如果保存對該值引用的變量被其他值給覆蓋了,那么引用數(shù)減1。當一個值的引用數(shù)為0時,就說明沒辦法再訪問到這個值了,因此可以安全地收回其內(nèi)存了。垃圾回收程序下次運行的時候就會釋放引用數(shù)為0的值的內(nèi)存。

          引用計數(shù)最早由Netscape Navigator 3.0采用,但很快就遇到了嚴重的問題:循環(huán)引用。所謂循環(huán)引用,就是對象A有一個指針指向?qū)ο驜,而對象B也引用了對象A。比如:

          function?problem()?{?
          ?let?objectA?=?new?Object();?
          ?let?objectB?=?new?Object();?
          ?
          ?objectA.someOtherObject?=?objectB;?
          ?objectB.anotherObject?=?objectA;?
          }

          在這個例子中, objectAobjectB 通過各自的屬性相互引用,意味著它們的引用數(shù)都是2。在標記清理策略下,這不是問題,因為在函數(shù)結束后,這兩個對象都不在作用域中。而在引用計數(shù)策略下, objectAobjectB 在函數(shù)結束后還會存在,因為它們的引用數(shù)永遠不會變成0。如果函數(shù)被多次調(diào)用,則會導致大量內(nèi)存永遠不會被釋放。為此,Netscape在4.0版放棄了引用計數(shù),轉(zhuǎn)而采用標記清理。事實上,引用計數(shù)策略的問題還不止于此。

          在IE8及更早版本的IE中,并非所有對象都是原生JavaScript對象。BOM和DOM中的對象是C++實現(xiàn)的組件對象模型(COM,Component Object Model)對象,而COM對象使用引用計數(shù)實現(xiàn)垃圾回收。因此,即使這些版本IE的JavaScript引擎使用標記清理,JavaScript存取的COM對象依舊使用引用計數(shù)。換句話說,只要涉及COM對象,就無法避開循環(huán)引用問題。下面這個簡單的例子展示了涉及COM對象的循環(huán)引用問題:

          let?element?=?document.getElementById("some_element");?
          let?myObject?=?new?Object();
          myObject.element?=?element;?
          element.someObject?=?myObject;

          這個例子在一個DOM對象( element )和一個原生JavaScript對象( myObject )之間制造了循環(huán)引用。myObject 變量有一個名為 element 的屬性指向DOM對象 element ,而 element 對象有一個 someObject 屬性指回 myObject 對象。由于存在循環(huán)引用,因此DOM元素的內(nèi)存永遠不會被回收,即使它已經(jīng)被從頁面上刪除了 也是如此。

          為避免類似的循環(huán)引用問題,應該在確保不使用的情況下切斷原生JavaScript對象與DOM元素之間的連接。比如,通過以下代碼可以清除前面的例子中建立的循環(huán)引用:

          myObject.element?=?null;?
          element.someObject?=?null;

          把變量設置為 null 實際上會切斷變量與其之前引用值之間的關系。當下次垃圾回收程序運行時,這些值就會被刪除,內(nèi)存也會被回收。

          為了補救這一點,IE9把BOM和DOM對象都改成了JavaScript對象,這同時也避免了由于存在兩套垃圾回收算法而導致的問題,還消除了常見的內(nèi)存泄漏現(xiàn)象。

          回過頭可以發(fā)現(xiàn),使用標記清理策略的話,循環(huán)引用就不再是問題了。

          在上面的示例中,函數(shù)調(diào)用返回之后,兩個對象從全局對象出發(fā)無法獲取。因此,他們將會被垃圾回收器回收。第二個示例同樣,一旦 div 和其事件處理無法從根獲取到,他們將會被垃圾回收器回收。

          四、內(nèi)存管理方案

          JavaScript變量可以保存兩種類型的值:原始值和引用值。原始值是 6 種原始數(shù)據(jù)類型:UndefinedNullBooleanNumberStringSymbol

          原始值和引用值有以下特點:

          • 原始值大小固定,因此保存在棧內(nèi)存上。
          • 從一個變量到另一個變量復制原始值會創(chuàng)建該值的第二個副本。
          • 引用值是對象,存儲在堆內(nèi)存上。
          • 包含引用值的變量實際上只包含指向相應對象的一個指針,而不是對象本身。
          • 從一個變量到另一個變量復制引用值只會復制指針,因此結果是兩個變量都指向同一個對象。
          • typeof 操作符可以確定值的原始類型,而 instanceof 操作符用于確保值的引用類型。

          任何變量(不管包含的是原始值還是引用值)都存在于某個執(zhí)行上下文中(也稱為作用域)。這個上下文(作用域)決定了變量的生命周期,以及它們可以訪問代碼的哪些部分。

          執(zhí)行上下文可以總結如下:

          • 執(zhí)行上下文分全局上下文、函數(shù)上下文和塊級上下文。
          • 代碼執(zhí)行流每進入一個新上下文,都會創(chuàng)建一個作用域鏈,用于搜索變量和函數(shù)。
          • 函數(shù)或塊的局部上下文不僅可以訪問自己作用域內(nèi)的變量,而且也可以訪問任何包含上下文乃至全局上下文中的變量。
          • 全局上下文只能訪問全局上下文中的變量和函數(shù),不能直接訪問局部上下文中的任何數(shù)據(jù)。
          • 變量的執(zhí)行上下文用于確定什么時候釋放內(nèi)存。

          JavaScript是使用垃圾回收的編程語言,開發(fā)者不需要操心內(nèi)存分配和回收。

          JavaScript的垃圾回收程序可以總結如下:

          • 離開作用域的值會被自動標記為可回收,然后在垃圾回收期間被刪除。
          • 主流的垃圾回收算法是標記清理,即先給當前不使用的值加上標記,再回來回收它們的內(nèi)存。
          • 引用計數(shù)是另一種垃圾回收策略,需要記錄值被引用了多少次。
          • JavaScript 引擎不再使用這種算法,但某些舊版本的IE仍然會受這種算法的影響,原因是JavaScript會訪問非原生JavaScript對象(如DOM元素)。
          • 引用計數(shù)在代碼中存在循環(huán)引用時會出現(xiàn)問題。
          • 解除變量的引用不僅可以消除循環(huán)引用,而且對垃圾回收也有幫助。為促進內(nèi)存回收,全局對象、全局對象的屬性和循環(huán)引用都應該在不需要時解除引用。

          在使用垃圾回收的編程環(huán)境中,開發(fā)者通常無須關心內(nèi)存管理。不過,JavaScript運行在一個內(nèi)存管理與垃圾回收都很特殊的環(huán)境。分配給瀏覽器的內(nèi)存通常比分配給桌面軟件的要少很多,分配給移動瀏覽器的就更少了。這更多出于安全考慮而不是別的,就是為了避免運行大量JavaScript的網(wǎng)頁耗盡系統(tǒng)內(nèi)存而導致操作系統(tǒng)崩潰。這個內(nèi)存限制不僅影響變量分配,也影響調(diào)用棧以及能夠同時在一個線程中執(zhí)行的語句數(shù)量。

          ● 解除引用

          將內(nèi)存占用量保持在一個較小的值可以讓頁面性能更好。優(yōu)化內(nèi)存占用的最佳手段就是保證在執(zhí)行代碼時只保存必要的數(shù)據(jù)。如果數(shù)據(jù)不再必要,那么把它設置為 null ,從而釋放其引用。這也可以叫作解除引用。這個建議最適合全局變量和全局對象的屬性。局部變量在超出作用域后會被自動解除引用,如下面的例子所示:

          function?createPerson(name)?{
          ????let?localPerson?=?new?Object();
          ????localPerson.name?=?name;
          ????return?localPerson;
          }
          let?globalPerson?=?createPerson("Nicholas");?//?解除globalPerson對值的引用
          globalPerson?=?null;

          在上面的代碼中,變量 globalPerson 保存著createPerson() 函數(shù)調(diào)用返回的值。在 createPerson() 內(nèi)部, localPerson 創(chuàng)建了一個對象并給它添加了一個 name 屬性。然后, localPerson 作為函數(shù)值被返回,并被賦值給globalPersonlocalPersoncreatePerson() 執(zhí)行完成超出上下文后會自動被解除引用,不需要顯式處理。但globalPerson 是一個全局變量,應該在不再需要時手動解除其引用,最后一行就是這么做的。

          不過要注意,解除對一個值的引用并不會自動導致相關內(nèi)存被回收。解除引用的關鍵在于確保相關的值已經(jīng)不在上下文里了,因此它在下次垃圾回收時會被回收。

          ● 通過 constlet 聲明提升性能

          ES6增加這兩個關鍵字不僅有助于改善代碼風格,而且同樣有助于改進垃圾回收的過程。因為 constlet 都以塊(而非函數(shù))為作用域,所以相比于使用 var ,使用這兩個新關鍵字可能會更早地讓垃圾回收程序介入,盡早回收應該回收的內(nèi)存。在塊作用域比函數(shù)作用域更早終止的情況下,這就有可能發(fā)生。

          參考

          • JavaScript高級程序設計(第4版)
          • 內(nèi)存管理
          • JavaScript 內(nèi)存泄漏教程


          瀏覽 115
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  家庭乱伦影视 | 成人做爰黄 片免费观看视频视频 | 人人妻超碰| 成人黄色电影在线 | 中国一区二区在线观看 |