一篇文章弄懂javascript內(nèi)存泄漏
1、什么是內(nèi)存泄漏
在了解什么是內(nèi)存泄漏之前, 我們應(yīng)該要對(duì)內(nèi)存是什么有個(gè)概念, 隨機(jī)存取存儲(chǔ)器(英語(yǔ):Random Access Memory,縮寫:RAM)是與 CPU 直接交換數(shù)據(jù)的內(nèi)部存儲(chǔ)器。它可以隨時(shí)讀寫, 而且速度很快,通常作為操作系統(tǒng)或其他正在運(yùn)行中的程序的臨時(shí)資料存儲(chǔ)介質(zhì)。
什么是內(nèi)存泄漏? :
程序不再需要使用的內(nèi)存, 但是又沒(méi)有及時(shí)釋放, 就叫做內(nèi)存泄漏!
然后在理解泄漏之前, 我們的了解下內(nèi)存的管理, 在一些底層語(yǔ)言中, 如C語(yǔ)言, 內(nèi)存是需要開發(fā)者自己分配和釋放的, 通過(guò)malloc、free等函數(shù)進(jìn)行內(nèi)存管理.
<pre class="custom">`char * buffer; // 使用malloc申請(qǐng)內(nèi)存空間 buffer = (char*) malloc (66); // do something ... // 不需要時(shí), 釋放掉內(nèi)存空間引用 free(buffer);
上面這一小段代碼就是C語(yǔ)言中關(guān)于內(nèi)存的申請(qǐng)和釋放, 但是眾所周知在javascript中是不需要開發(fā)者手動(dòng)管理內(nèi)存的, 在Chrome中有V8引擎幫我們自動(dòng)進(jìn)行內(nèi)存的分配和回收, 這就是垃圾回收機(jī)制, 但這并不代表我們?cè)诰帉懘a是不需要考慮內(nèi)存的事情, 因?yàn)閂8垃圾回收機(jī)制是有特定的規(guī)則的, 了解這些規(guī)則可以讓我們避免寫出內(nèi)存泄漏的爛代碼.
那么先了解下javascript垃圾回收機(jī)制常見(jiàn)的兩種方法吧:
- 引用計(jì)數(shù)算法
- 標(biāo)記清除算法
引用計(jì)數(shù)法
IE使用的是引用計(jì)數(shù)算法, 這種方法無(wú)法解決循環(huán)引用的垃圾回收問(wèn)題, 容易造成內(nèi)存泄漏
那么什么是引用計(jì)數(shù)算法呢? 什么又是循環(huán)引用問(wèn)題呢?
所謂引用計(jì)數(shù)即, 我們有一個(gè)變量每次被引用GC機(jī)制就會(huì)給這個(gè)變量計(jì)數(shù)加一, 當(dāng)引用減少就計(jì)數(shù)減一, 如果計(jì)數(shù)為零, 在下一次垃圾回收時(shí), 就會(huì)被釋放掉.
<pre class="custom">`let obj = {}; // obj計(jì)數(shù)為0 let a = obj; // obj計(jì)數(shù)為1 let b = obj; // obj計(jì)數(shù)為2 let a = null; // obj計(jì)數(shù)為1 let b = null; // Obj計(jì)數(shù)為0, 下次垃圾回收將obj內(nèi)存釋放
以上代碼演示的就是引用計(jì)數(shù)法垃圾回收機(jī)制, 當(dāng)存在循環(huán)引用的情況就沒(méi)救了
<pre class="custom">`let obj = {}; let obj2 = {}; obj.o = obj2; // obj2計(jì)數(shù)為1 obj2.o = obj; // obj計(jì)數(shù)為1
這就是循環(huán)引用, 所以垃圾回收機(jī)制并不會(huì)對(duì)obj, obj2進(jìn)行內(nèi)存釋放, 變量常駐內(nèi)存, 導(dǎo)致內(nèi)存泄漏.
那么說(shuō)完了引用計(jì)數(shù)法, 我們?cè)賮?lái)看看主流瀏覽器目前所用的垃圾回收算法 – 標(biāo)記清除法,
從2012年起,所有現(xiàn)代瀏覽器都使用了標(biāo)記清除垃圾回收算法。所有對(duì)JavaScript垃圾回收算法的改進(jìn)都是基于標(biāo)記-清除算法的改進(jìn),并沒(méi)有改進(jìn)標(biāo)記-清除算法本身和它對(duì)“對(duì)象是否不再需要”的簡(jiǎn)化定義。
標(biāo)記清除法
這個(gè)算法假定設(shè)置一個(gè)叫做根(root)的對(duì)象(在Javascript里,根是全局對(duì)象)。垃圾回收器將定期從根開始,找所有從根開始引用的對(duì)象,然后繼續(xù)找這些對(duì)象引用的對(duì)象.
在開始說(shuō)標(biāo)記清除法之前, 補(bǔ)說(shuō)一個(gè)知識(shí)點(diǎn), 就是棧和堆的概念, 看看下面的例子
<pre class="custom">`let obj = {}; let obj2 = {}; obj = null; obj2 = null;
我們知道, 在javascript中, 除了八大基本類型(截至目前為止是八種), 剩下的都是對(duì)象類型, 在js中對(duì)象類型都是引用類型, 內(nèi)容的實(shí)體是存在堆中的, 如下面我畫的這張圖所示:
當(dāng)我們重新賦值obj, obj1的時(shí)候內(nèi)存結(jié)構(gòu)會(huì)變成這樣
堆內(nèi)存中的對(duì)象沒(méi)有人引用他們, 但是他們還占用這內(nèi)存, 這時(shí)候就需要我們的垃圾回收出場(chǎng)銷毀他們了, V8引擎的垃圾回收機(jī)制不僅銷毀掉堆內(nèi)存中無(wú)人引用的空間, 還會(huì)對(duì)堆內(nèi)存進(jìn)行碎片整理, V8的GC(垃圾回收)工作如下面動(dòng)圖所示:
V8的GC大致可以分為以下幾個(gè)步驟
第一步,通過(guò) GC Root 標(biāo)記空間中活動(dòng)對(duì)象和非活動(dòng)對(duì)象。目前V8采用的是可訪問(wèn)性算法, 從GC Root出發(fā)遍歷所有的對(duì)象, 通過(guò)GC Root可以遍歷到的標(biāo)記為可訪問(wèn)的, 稱為活動(dòng)對(duì)象,必須保留在內(nèi)存中, GC Root無(wú)法遍歷到的標(biāo)記為不可訪問(wèn)的, 稱為非活動(dòng)對(duì)象, 這些不可訪問(wèn)的對(duì)象將會(huì)被GC清理掉.
第二步,回收非活動(dòng)對(duì)象所占據(jù)的內(nèi)存。其實(shí)就是在所有的標(biāo)記完成之后,統(tǒng)一清理內(nèi)存中所有被標(biāo)記為可回收的對(duì)象。
第三步,做內(nèi)存整理。一般來(lái)說(shuō),頻繁回收對(duì)象后,內(nèi)存中就會(huì)存在大量不連續(xù)空間,我們把這些不連續(xù)的內(nèi)存空間稱為內(nèi)存碎片。
受代際假說(shuō)的影響, V8引擎采用兩個(gè)垃圾回收器, 主垃圾回收器–Major GC、副垃圾回收器–Minor GC(Scavenger), 你可能會(huì)問(wèn)什么是代際假說(shuō):
第一個(gè)是大部分對(duì)象都是“朝生夕死”的,也就是說(shuō)大部分對(duì)象在內(nèi)存中存活的時(shí)間很短,比如函數(shù)內(nèi)部聲明的變量,或者塊級(jí)作用域中的變量,當(dāng)函數(shù)或者代碼塊執(zhí)行結(jié)束時(shí),作用域中定義的變量就會(huì)被銷毀。因此這一類對(duì)象一經(jīng)分配內(nèi)存,很快就變得不可訪問(wèn);
第二個(gè)是不死的對(duì)象,會(huì)活得更久,比如全局的 window、DOM、Web API 等對(duì)象。
這兩個(gè)回收器的作用如下:
- 主垃圾回收器 -Major GC,主要負(fù)責(zé)老生代的垃圾回收。
- 副垃圾回收器 -Minor GC (Scavenger),主要負(fù)責(zé)新生代的垃圾回收。
這里又會(huì)引出新生代內(nèi)存和老生代內(nèi)存的概念, 將堆內(nèi)存分成兩塊區(qū)域
新生代的內(nèi)存區(qū)域一般比較小, 但是垃圾回收得會(huì)比較頻繁, 而老生代內(nèi)存區(qū)的特點(diǎn)就是對(duì)象占用空間相對(duì)較大, 對(duì)象存活時(shí)間較長(zhǎng), 垃圾回收的頻率也較低.
對(duì)了補(bǔ)一句, 垃圾回收時(shí)是會(huì)阻塞進(jìn)程的.
2、常見(jiàn)的內(nèi)存泄漏情況
了解垃圾回收和內(nèi)存泄漏是什么之后, 我們來(lái)看一些常見(jiàn)的內(nèi)存泄漏場(chǎng)景:
1. 意外的全局變量
前面我們提到有些對(duì)象是常駐內(nèi)存的, 視為不死對(duì)象, 如window對(duì)象, 是瀏覽器中javascript的頂級(jí)對(duì)象, 它的存在貫穿這個(gè)javascript的生命周期, 如果我們不小心把龐大又用不上的變量掛到了window對(duì)象上, 將會(huì)造成內(nèi)存泄漏, 當(dāng)然這是一個(gè)很低級(jí)的錯(cuò)誤.
<pre class="custom">`function test() { // 漏掉了聲明, 將會(huì)自動(dòng)掛載到window對(duì)象下 str = ''; for (let i = 0; i < 100000; i++) { str += 'xx'; } return str; } // test執(zhí)行結(jié)束后, str應(yīng)該就沒(méi)用了, 但是它常駐在了內(nèi)存中 test();
2. 濫用閉包
此處來(lái)順便了解下閉包的概念, 閉包的概念網(wǎng)上很多說(shuō)的都比較抽象, 我個(gè)人理解的閉包是:
函數(shù)和其可操作的其他作用域變量的詞法環(huán)境稱為閉包
當(dāng)然了如果我是個(gè)杠精, 可能會(huì)說(shuō)with語(yǔ)法是不是也算閉包呢? 按照定義with不是函數(shù)所以不屬于閉包.
MDN中對(duì)閉包的描述:
一個(gè)函數(shù)和對(duì)其周圍狀態(tài)(lexical environment,詞法環(huán)境)的引用捆綁在一起(或者說(shuō)函數(shù)被引用包圍),這樣的組合就是閉包(closure)。也就是說(shuō),閉包讓你可以在一個(gè)內(nèi)層函數(shù)中訪問(wèn)到其外層函數(shù)的作用域。在 JavaScript 中,每當(dāng)創(chuàng)建一個(gè)函數(shù),閉包就會(huì)在函數(shù)創(chuàng)建的同時(shí)被創(chuàng)建出來(lái)。
閉包是靜態(tài)作用域(又稱為詞法作用域)語(yǔ)言獨(dú)有的功能.
<pre class="custom">`function fn() { const x = 'xx'; return function() { return x; } } const getX = fn(); console.log(getX());
如上面的例子就是一個(gè)閉包, fn執(zhí)行結(jié)束之后內(nèi)部變量并沒(méi)有銷毀, 我們?cè)谌肿饔糜蛳驴梢酝ㄟ^(guò)getX訪問(wèn)到fn函數(shù)作用域內(nèi)的變量x.
如果不好理解可以看下上面提到的靜態(tài)作用域(又稱詞法作用域), 看到靜態(tài)作用域應(yīng)該你會(huì)問(wèn), 有沒(méi)有動(dòng)態(tài)作用域呢, 但是是有的, bash腳本采用的就是動(dòng)態(tài)作用域, javascript采用的是靜態(tài)作用域, 閉包是靜態(tài)作用域采用的功能.
看兩個(gè)例子
<pre class="custom">`const x = 123; function fn() { console.log(x); } function fn2() { const x = 345; fn(); } fn2(); // 結(jié)果是123
靜態(tài)作用域: fn中輸出的x所處的作用域是在定義時(shí)確定的
再看個(gè)動(dòng)態(tài)作用域的例子
<pre class="custom">`# test.sh value="global"; function fn() { echo $value; } function fn2() { local value="local"; fn; } fn2; # 結(jié)果是local
動(dòng)態(tài)作用域: fn中輸出的x所處的作用域是在調(diào)用時(shí)確定的
還有一點(diǎn), 只有濫用閉包才能叫內(nèi)存泄漏, 因?yàn)楦鶕?jù)定義只有我們用不到了, 而且沒(méi)有被銷毀的內(nèi)存才叫內(nèi)存泄漏, 閉包中的值是我們用到的所以不應(yīng)該叫做內(nèi)存泄漏.
<pre class="custom">`function generateRandomMath() { let x = Math.random(); return function() { return x; } }
如上面這個(gè)例子就是濫用閉包了, 就該叫做內(nèi)存泄漏
3. 被遺忘的定時(shí)器
這個(gè)沒(méi)什么好說(shuō)的, 就是設(shè)置了定時(shí)器請(qǐng)記住在不要的時(shí)候使用clearInterval或者clearTImeout給他關(guān)一下.
4. DOM相關(guān)
給某個(gè)dom節(jié)點(diǎn)綁定了很多事件, 使用過(guò)程中dom節(jié)點(diǎn)被移除但是被釋放內(nèi)存
我們來(lái)看個(gè)例子
<pre class="custom">`<button class="remove">remove bbb</button> <div class="box">bbb</div> <script> const box = document.querySelector('.box'); document.querySelector('.remove').addEventListener('click', () => { document.body.removeChild(box); console.log(box); }) </script>
移除了box元素后, box仍然占用內(nèi)存, 這也是內(nèi)存泄漏, 因?yàn)閎ox用不到了但是沒(méi)有釋放內(nèi)存
總結(jié)
到此這篇關(guān)于javascript內(nèi)存泄漏的文章就介紹到這了,更多相關(guān)javascript內(nèi)存泄漏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決LayUI加上form.render()下拉框和單選以及復(fù)選框不出來(lái)的問(wèn)題
今天小編就為大家分享一篇解決LayUI加上form.render()下拉框和單選以及復(fù)選框不出來(lái)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09JS對(duì)象與json字符串相互轉(zhuǎn)換實(shí)現(xiàn)方法示例
這篇文章主要介紹了JS對(duì)象與json字符串相互轉(zhuǎn)換實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了js對(duì)象與json字符串相互轉(zhuǎn)換的相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下2018-06-06echarts學(xué)習(xí)筆記之圖表自適應(yīng)問(wèn)題詳解
最近發(fā)現(xiàn)一個(gè)問(wèn)題,echarts圖初始化后不能自適應(yīng)瀏覽器的縮放,所以下面這篇文章就來(lái)給大家介紹了關(guān)于echarts圖表自適應(yīng)問(wèn)題的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11JavaScript獲取網(wǎng)頁(yè)表單提交方式的方法
這篇文章主要介紹了JavaScript獲取網(wǎng)頁(yè)表單提交方式的方法,可判斷表單提交方式是get還是post,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-04-04nuxt+axios實(shí)現(xiàn)打包后動(dòng)態(tài)修改請(qǐng)求地址的方法
這篇文章主要介紹了nuxt+axios實(shí)現(xiàn)打包后動(dòng)態(tài)修改請(qǐng)求地址的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04純js實(shí)現(xiàn)無(wú)縫滾動(dòng)功能代碼實(shí)例
這篇文章主要介紹了純js實(shí)現(xiàn)無(wú)縫滾動(dòng)功能代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02js實(shí)現(xiàn)Element中input組件的部分功能并封裝成組件(實(shí)例代碼)
這篇文章主要介紹了純生js實(shí)現(xiàn)Element中input組件的部分功能(慢慢完善)并封裝成組件,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03