一篇文章弄懂javascript記憶體洩漏
1、什麼是記憶體洩漏
在瞭解什麼是記憶體洩漏之前,我們應該要對記憶體是什麼有個概念,隨機存取儲存器(英語:Random Access Memory,縮寫:RAM)是與 CPU 直接交換資料的內部儲存器。它可以隨時讀寫,而且速度很快,通常作為作業系統或其他正在執行中的程式的臨時資料儲存介質。
什麼是記憶體洩漏? :
程式不再需要使用的記憶體,但是又沒有及時釋放,就叫做記憶體洩漏!
然後在理解洩漏之前,我們的瞭解下記憶體的管理,在一些底層語言中,如C語言,記憶體是需要開發者自己分配和釋放的,通過malloc、free等函式進行記憶體管理.
<pre class="custom">`char*buffer; //使用malloc申請記憶體空間 buffer=(char*)malloc(66); //dosomething... //不需要時,釋放掉記憶體空間引用 free(buffer);
上面這一小段程式碼就是C語言中關於記憶體的申請和釋放,但是眾所周知在javascript中是不需要開發者手動管理記憶體的,在Chrome中有V8引擎幫我們自動進行記憶體的分配和回收,這就是垃圾回收機制,但這並不代表我們在編寫程式碼是不需要考慮記憶體的事情,因為V8垃圾回收機制是有特定的規則的,瞭解這些規則可以讓我們避免寫出記憶體洩漏的爛程式碼.
那麼先了解下javascript垃圾回收機制常見的兩種方法吧:
- 引用計數演算法
- 標記清除演算法
引用計數法
IE使用的是引用計數演算法,這種方法無法解決迴圈引用的垃圾回收問題,容易造成記憶體洩漏
那麼什麼是引用計數演算法呢? 什麼又是迴圈引用問題呢?
所謂引用計數即,我們有一個變數每次被引用GC機制就會給這個變數計數加一,當引用減少就計數減一,如果計數為零,在下一次垃圾回收時,就會被釋放掉.
<pre class="custom">`let&nbs程式設計客棧p;obj={};//obj計數為0
leta=obj;//obj計數為1
letb=obj;//obj計數為2
leta=null;//obj計數為1
letb=null;//Obj計數為0,下次垃圾回收將obj記憶體釋放
以上程式碼演示的就是引用計數法垃圾回收機制,當存在迴圈引用的情況就沒救了
<pre class="custom">`letobj={}; letobj2={}; obj.o=obj2;//obj2計數為1 obj2.o=obj;//obj計數為1
這就是迴圈引用,所以垃圾回收機制並不會對obj,obj2進行記憶體釋放,變數常駐記憶體,導致記憶體洩漏.
那麼說完了引用計數法,我們再來看看主流瀏覽器目前所用的垃圾回收演算法 – 標記清除法,
從2012年起,所有現代瀏覽器都使用了標記清除垃圾回收演算法。所有對JavaScript垃圾回收演算法的改進都是基於標記-清除演算法的改進,並沒有改進標記-清除演算法本身和它對“物件是否不再需要”的簡化定義。
標記清除法
這個演算法假定設定一個叫做根(root)的物件(在Javascript裡,根是全域性物件)。垃圾回收器將定期從根開始,找所有從根開始引用的物件,然後繼續找這些物件引用的物件.
在開始說標記清除法之前,補說一個知識點,就是棧和堆的概念,看看下面的例子
<pre class="custom">`letobj={}; letobj2={}; obj=null; obj2=null;
我們知道,在javascript中,除了八大基本型別(截至目前為止是八種),剩下的都是物件型別,在js中物件型別都是引用型別,內容的實體是存在堆中的,如下面我畫的這張圖所示:
當我們重新賦值obj,obj1的時候記憶體結構會變成這樣
堆記憶體中的物件沒有人引用他們,但是他們還佔用這記憶體,這時候就需要我們的垃圾回收出場銷燬他們了,V8引擎的垃圾回收機制不僅銷燬掉堆記憶體中無人引用的空間,還會對堆記憶體進行碎片整理,V8的GC(垃圾回收)工作如下面動圖所示:
V8的GC大致可以分為以下幾個步驟
第一步,通過 GC Root 標記空間中活動物件和非活動物件。目前V8採用的是可訪問性演算法,從GC Root出發遍歷所有的物件,通過GC Root可以遍歷到的標記為可訪問的,稱為活動物件,必須保留在記憶體中,GC Root無法遍歷到的標記為不可訪問的,稱為非活動物件,這些不可訪問的物件將會被GC清理掉.
第二步,回收非活動物件所佔據的記憶體。其實就是在所有的標記完成之後,統一清理記憶體中所有被標記為可回收的物件。
第三步,做記憶體整理。一般來說,頻繁回收物件後,記憶體中就會存在大量不連續空間,我們把這些不連續的記憶體空間稱為記憶體碎片。
受代際假說的影響,V8引擎採用兩個垃圾回收器,主垃圾回收器–Major GC、副垃圾回收器–Minor GC(Scavenger),你可能會問什麼是代際假說:
第一個是大部分物件都是“朝生夕死”的,也就是說大部分物件在記憶體中存活的時間很短,比如函式內部宣告的變數,或者塊級作用域中的變數,當函式或者http://www.cppcns.com程式碼塊執行結束時,作用域中定義的變數就會被銷燬。因此這一類物件一經分配記憶體,很快就變得不可訪問;
第二個是不死的物件,會活得更久,比如全域性的 window、DOM、Web API 等物件。
這兩個回收器的作用如下:
- 主垃圾回收器 -Major GC,主要負責老生代的垃圾回收。
- 副垃圾回收器 -Minor GC (Scavenger),主要負責新生代的垃圾回收。
這裡又會引出新生代記憶體和老生代記憶體的概念,將堆記憶體分成兩塊區域
新生代的記憶體區域一般比較小,但是垃圾回收得會比較頻繁,而老生代記憶體區的特點就是物件佔用空間相對較大,物件存活時間較長,垃圾回收的頻率也較低.
對了補一句,垃圾回收時是會阻塞程序的.
2、常見的記憶體洩漏情況
瞭解垃圾回收和記憶體洩漏是什麼之後,我們來看一些常見的記憶體洩漏場景:
1. 意外的全域性變數
前面我們提到有些物件是常駐記憶體的,視為不死物件,如window物件,是瀏覽器中javascript的頂級物件,它的存在貫穿這個javascript的生命週期,如果我們不小心把龐大又用不上的變數掛到了window物件上,將會造成記憶體洩漏,當然這是一個很低階的錯誤.
<pre class="custom">`functiontest(){ //漏掉了宣告,http://www.cppcns.com將會自動掛載到window物件下 str=''; for(leti=0;i<100000;i++){ str+='xx'; } returnstr; } //test執行結束後,str應該就沒用了,但是它常駐在了記憶體中 test();
2. 濫用閉包
此處來順便了解下閉包的概念,閉包的概念網上很多說的都比較抽象,我個人理解的閉包是:
函式和其可操作的其他作用域變數的詞法環境稱為閉包
當然瞭如果我是個槓精,可能會說with語法是不是也算閉包呢? 按照定義with不是函式所以不屬於閉包.
MDN中對閉包的描述:
一個函式和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者說函式被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個內層函式中訪問到其外層函式的作用域。在 JavaScript 中,每當建立一個函式,閉包就會在函式建立的同時被創建出來。
閉包是靜態作用域(又稱為詞法作用域)語言獨有的功能.
<pre class="custom">`functionfn(){ constx='xx'; returnfunction(){ returnx; } } constgetX=fn(); console.log(getX());
如上面的例子就是一個閉包,fn執行結束之後內部變數並沒有銷燬,我們在全域性作用域下可以通過getX訪問到fn函式作用域內的變數x.
如果不好理解可以看下上面提到的靜態作用域(又稱詞法作用域),看到靜態作用域應該你會問,有沒有動態作用域呢,但是是有的,bash指令碼採用的就是動態作用域,javascript採用的是靜態作用域,閉包是靜態作用域採用的功能.
看兩個例子
<pre class="custom">`constx=123; functionfn(){ console.log(x); } functionfn2(){ constx=345; fn(); } fn2();//結果是123
靜態作用域: fn中輸出的x所處的作用域是在定義時確定的
再看個動態作用域的例子
<pre class="custom">`#test.sh value="global"; functionfn(){ echo$value; } functionfn2(){ localvalue="local"; fn; } fn2;#結果是local
動態作用域: fn中輸出的x所處的作用域是在呼叫時確定的
還有一點,只有濫用閉包才能叫記憶體洩漏,因為根據定義只有我們用不到了,而且沒有被銷燬的記憶體才叫記憶體洩漏,閉包中的值是我們用到的所以不應該叫做記憶體洩漏.
<pre class="custom">`functiongen程式設計客棧erateRandomMath(){
letx=&nbs程式設計客棧p;Math.random();
returnfunction(){
returnx;
}
}
如上面這個例子就是濫用閉包了,就該叫做記憶體洩漏
3. 被遺忘的定時器
這個沒什麼好說的,就是設定了定時器請記住在不要的時候使用clearInterval或者clearTImeout給他關一下.
4. DOM相關
給某個dom節點綁定了很多事件,使用過程中dom節點被移除但是被釋放記憶體
我們來看個例子
<pre class="custom">`<buttonclass="remove">removebbb</button> <divclass="box">bbb</div> <script> constbox=document.querySelector('.box'); document.querySelector('.remove').addEventListener('click',()=>{ document.body.removeChild(box); console.log(box); }) </script>
移除了box元素後,box仍然佔用記憶體,這也是記憶體洩漏,因為box用不到了但是沒有釋放記憶體
總結
到此這篇關於javascript記憶體洩漏的文章就介紹到這了,更多相關javascript記憶體洩漏內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!