摘要:介紹瀏覽器的具有自動(dòng)垃圾回收機(jī)制,也就是說,執(zhí)行環(huán)境會(huì)負(fù)責(zé)管理代碼執(zhí)行過程中使用的內(nèi)存。中的內(nèi)存泄漏問題程序的內(nèi)存溢出后,會(huì)使某一段函數(shù)體永遠(yuǎn)失效取決于當(dāng)時(shí)的代碼運(yùn)行到哪一個(gè)函數(shù),通常表現(xiàn)為程序突然卡死或程序出現(xiàn)異常。
1. 介紹
瀏覽器的 Javascript 具有自動(dòng)垃圾回收機(jī)制(GC:Garbage Collecation),也就是說,執(zhí)行環(huán)境會(huì)負(fù)責(zé)管理代碼執(zhí)行過程中使用的內(nèi)存。其原理是:垃圾收集器會(huì)定期(周期性)找出那些不在繼續(xù)使用的變量,然后釋放其內(nèi)存。但是這個(gè)過程不是實(shí)時(shí)的,因?yàn)槠溟_銷比較大并且GC時(shí)停止響應(yīng)其他操作,所以垃圾回收器會(huì)按照固定的時(shí)間間隔周期性的執(zhí)行。
不再使用的變量也就是生命周期結(jié)束的變量,當(dāng)然只可能是局部變量,全局變量的生命周期直至瀏覽器卸載頁面才會(huì)結(jié)束。局部變量只在函數(shù)的執(zhí)行過程中存在,而在這個(gè)過程中會(huì)為局部變量在棧或堆上分配相應(yīng)的空間,以存儲(chǔ)它們的值,然后在函數(shù)中使用這些變量,直至函數(shù)結(jié)束,而閉包中由于內(nèi)部函數(shù)的原因,外部函數(shù)并不能算是結(jié)束。
還是上代碼說明吧:
function fn1() { var obj = {name: "hanzichi", age: 10}; } function fn2() { var obj = {name:"hanzichi", age: 10}; return obj; } var a = fn1(); var b = fn2();
我們來看代碼是如何執(zhí)行的。首先定義了兩個(gè)function,分別叫做fn1和fn2,當(dāng)fn1被調(diào)用時(shí),進(jìn)入fn1的環(huán)境,會(huì)開辟一塊內(nèi)存存放對(duì)象{name: "hanzichi", age: 10},而當(dāng)調(diào)用結(jié)束后,出了fn1的環(huán)境,那么該塊內(nèi)存會(huì)被js引擎中的垃圾回收器自動(dòng)釋放;在fn2被調(diào)用的過程中,返回的對(duì)象被全局變量b所指向,所以該塊內(nèi)存并不會(huì)被釋放。
這里問題就出現(xiàn)了:到底哪個(gè)變量是沒有用的?所以垃圾收集器必須跟蹤到底哪個(gè)變量沒用,對(duì)于不再有用的變量打上標(biāo)記,以備將來收回其內(nèi)存。用于標(biāo)記的無用變量的策略可能因?qū)崿F(xiàn)而有所區(qū)別,通常情況下有兩種實(shí)現(xiàn)方式:標(biāo)記清除和引用計(jì)數(shù)。引用計(jì)數(shù)不太常用,標(biāo)記清除較為常用。
2. 標(biāo)記清除js中最常用的垃圾回收方式就是標(biāo)記清除。當(dāng)變量進(jìn)入環(huán)境時(shí),例如,在函數(shù)中聲明一個(gè)變量,就將這個(gè)變量標(biāo)記為“進(jìn)入環(huán)境”。從邏輯上講,永遠(yuǎn)不能釋放進(jìn)入環(huán)境的變量所占用的內(nèi)存,因?yàn)橹灰獔?zhí)行流進(jìn)入相應(yīng)的環(huán)境,就可能會(huì)用到它們。而當(dāng)變量離開環(huán)境時(shí),則將其標(biāo)記為“離開環(huán)境”。
function test(){ var a = 10 ; //被標(biāo)記 ,進(jìn)入環(huán)境 var b = 20 ; //被標(biāo)記 ,進(jìn)入環(huán)境 } test(); //執(zhí)行完畢 之后 a、b又被標(biāo)離開環(huán)境,被回收。
垃圾回收器在運(yùn)行的時(shí)候會(huì)給存儲(chǔ)在內(nèi)存中的所有變量都加上標(biāo)記(當(dāng)然,可以使用任何標(biāo)記方式)。然后,它會(huì)去掉環(huán)境中的變量以及被環(huán)境中的變量引用的變量的標(biāo)記(閉包)。而在此之后再被加上標(biāo)記的變量將被視為準(zhǔn)備刪除的變量,原因是環(huán)境中的變量已經(jīng)無法訪問到這些變量了。最后,垃圾回收器完成內(nèi)存清除工作,銷毀那些帶標(biāo)記的值并回收它們所占用的內(nèi)存空間。
到目前為止,IE9+、Firefox、Opera、Chrome、Safari的js實(shí)現(xiàn)使用的都是標(biāo)記清除的垃圾回收策略或類似的策略,只不過垃圾收集的時(shí)間間隔互不相同。
引用計(jì)數(shù)的含義是跟蹤記錄每個(gè)值被引用的次數(shù)。當(dāng)聲明了一個(gè)變量并將一個(gè)引用類型值賦給該變量時(shí),則這個(gè)值的引用次數(shù)就是1。如果同一個(gè)值又被賦給另一個(gè)變量,則該值的引用次數(shù)加1。相反,如果包含對(duì)這個(gè)值引用的變量又取得了另外一個(gè)值,則這個(gè)值的引用次數(shù)減1。當(dāng)這個(gè)值的引用次數(shù)變成0時(shí),則說明沒有辦法再訪問這個(gè)值了,因而就可以將其占用的內(nèi)存空間回收回來。這樣,當(dāng)垃圾回收器下次再運(yùn)行時(shí),它就會(huì)釋放那些引用次數(shù)為0的值所占用的內(nèi)存。
function test(){ var a = {} ; //a的引用次數(shù)為0 var b = a ; //a的引用次數(shù)加1,為1 var c =a; //a的引用次數(shù)再加1,為2 var b ={}; //a的引用次數(shù)減1,為1 }
Netscape Navigator3是最早使用引用計(jì)數(shù)策略的瀏覽器,但很快它就遇到一個(gè)嚴(yán)重的問題:循環(huán)引用。循環(huán)引用指的是對(duì)象A中包含一個(gè)指向?qū)ο驜的指針,而對(duì)象B中也包含一個(gè)指向?qū)ο驛的引用。
function fn() { var a = {}; var b = {}; a.pro = b; b.pro = a; } fn();
以上代碼a和b的引用次數(shù)都是2,fn()執(zhí)行完畢后,兩個(gè)對(duì)象都已經(jīng)離開環(huán)境,在標(biāo)記清除方式下是沒有問題的,但是在引用計(jì)數(shù)策略下,因?yàn)閍和b的引用次數(shù)不為0,所以不會(huì)被垃圾回收器回收內(nèi)存,如果fn函數(shù)被大量調(diào)用,就會(huì)造成內(nèi)存泄露。在IE7與IE8上,內(nèi)存直線上升。
我們知道,IE中有一部分對(duì)象并不是原生js對(duì)象。例如,其內(nèi)存泄露DOM和BOM中的對(duì)象就是使用C++以COM對(duì)象的形式實(shí)現(xiàn)的,而COM對(duì)象的垃圾回收機(jī)制采用的就是引用計(jì)數(shù)策略。因此,即使IE的js引擎采用標(biāo)記清除策略來實(shí)現(xiàn),但js訪問的COM對(duì)象依然是基于引用計(jì)數(shù)策略的。換句話說,只要在IE中涉及COM對(duì)象,就會(huì)存在循環(huán)引用的問題。
var element = document.getElementById("some_element"); var myObject = new Object(); myObject.e = element; element.o = myObject;
這個(gè)例子在一個(gè)DOM元素(element)與一個(gè)原生js對(duì)象(myObject)之間創(chuàng)建了循環(huán)引用。其中,變量myObject有一個(gè)屬性e指向element對(duì)象;而變量element也有一個(gè)屬性o回指myObject。由于存在這個(gè)循環(huán)引用,即使例子中的DOM從頁面中移除,它也永遠(yuǎn)不會(huì)被回收。
舉個(gè)栗子:
黃色是指直接被 js變量所引用,在內(nèi)存里
紅色是指間接被 js變量所引用,如上圖,refB 被 refA 間接引用,導(dǎo)致即使 refB 變量被清空,也是不會(huì)被回收的
子元素 refB 由于 parentNode 的間接引用,只要它不被刪除,它所有的父元素(圖中紅色部分)都不會(huì)被刪除
另一個(gè)例子:
window.onload=function outerFunction(){ var obj = document.getElementById("element"); obj.onclick=function innerFunction(){}; };
這段代碼看起來沒什么問題,但是obj引用了document.getElementById("element"),而document.getElementById("element")的onclick方法會(huì)引用外部環(huán)境中的變量,自然也包括obj,是不是很隱蔽啊。(在比較新的瀏覽器中在移除Node的時(shí)候已經(jīng)會(huì)移除其上的event了,但是在老的瀏覽器,特別是ie上會(huì)有這個(gè)bug)
解決辦法:
最簡單的方式就是自己手工解除循環(huán)引用,比如剛才的函數(shù)可以這樣
myObject.element = null; element.o = null; window.onload=function outerFunction(){ var obj = document.getElementById("element"); obj.onclick=function innerFunction(){}; obj=null; };
將變量設(shè)置為null意味著切斷變量與它此前引用的值之間的連接。當(dāng)垃圾回收器下次運(yùn)行時(shí),就會(huì)刪除這些值并回收它們占用的內(nèi)存。
要注意的是,IE9+并不存在循環(huán)引用導(dǎo)致Dom內(nèi)存泄露問題,可能是微軟做了優(yōu)化,或者Dom的回收方式已經(jīng)改變
4. 內(nèi)存管理 4.1 什么時(shí)候觸發(fā)垃圾回收?垃圾回收器周期性運(yùn)行,如果分配的內(nèi)存非常多,那么回收工作也會(huì)很艱巨,確定垃圾回收時(shí)間間隔就變成了一個(gè)值得思考的問題。IE6的垃圾回收是根據(jù)內(nèi)存分配量運(yùn)行的,當(dāng)環(huán)境中存在256個(gè)變量、4096個(gè)對(duì)象、64k的字符串任意一種情況的時(shí)候就會(huì)觸發(fā)垃圾回收器工作,看起來很科學(xué),不用按一段時(shí)間就調(diào)用一次,有時(shí)候會(huì)沒必要,這樣按需調(diào)用不是很好嗎?但是如果環(huán)境中就是有這么多變量等一直存在,現(xiàn)在腳本如此復(fù)雜,很正常,那么結(jié)果就是垃圾回收器一直在工作,這樣瀏覽器就沒法兒玩兒了。
微軟在IE7中做了調(diào)整,觸發(fā)條件不再是固定的,而是動(dòng)態(tài)修改的,初始值和IE6相同,如果垃圾回收器回收的內(nèi)存分配量低于程序占用內(nèi)存的15%,說明大部分內(nèi)存不可被回收,設(shè)的垃圾回收觸發(fā)條件過于敏感,這時(shí)候把臨街條件翻倍,如果回收的內(nèi)存高于85%,說明大部分內(nèi)存早就該清理了,這時(shí)候把觸發(fā)條件置回。這樣就使垃圾回收工作職能了很多
4.2 合理的GC方案 1. 基礎(chǔ)方案Javascript引擎基礎(chǔ)GC方案是(simple GC):mark and sweep(標(biāo)記清除),即:
遍歷所有可訪問的對(duì)象。
回收已不可訪問的對(duì)象。
2. GC的缺陷和其他語言一樣,javascript的GC策略也無法避免一個(gè)問題:GC時(shí),停止響應(yīng)其他操作,這是為了安全考慮。而Javascript的GC在100ms甚至以上,對(duì)一般的應(yīng)用還好,但對(duì)于JS游戲,動(dòng)畫對(duì)連貫性要求比較高的應(yīng)用,就麻煩了。這就是新引擎需要優(yōu)化的點(diǎn):避免GC造成的長時(shí)間停止響應(yīng)。
3. GC優(yōu)化策略David大叔主要介紹了2個(gè)優(yōu)化方案,而這也是最主要的2個(gè)優(yōu)化方案了:
分代回收(Generation GC)
這個(gè)和Java回收策略思想是一致的,也是V8所主要采用的。目的是通過區(qū)分“臨時(shí)”與“持久”對(duì)象;多回收“臨時(shí)對(duì)象”區(qū)(young generation),少回收“持久對(duì)象”區(qū)(tenured generation),減少每次需遍歷的對(duì)象,從而減少每次GC的耗時(shí)。如圖:
這里需要補(bǔ)充的是:對(duì)于tenured generation對(duì)象,有額外的開銷:把它從young generation遷移到tenured generation,另外,如果被引用了,那引用的指向也需要修改。
這里主要內(nèi)容可以參考深入淺出Node中關(guān)于內(nèi)存的介紹,很詳細(xì)~
增量GC
這個(gè)方案的思想很簡單,就是“每次處理一點(diǎn),下次再處理一點(diǎn),如此類推”。如圖:
這種方案,雖然耗時(shí)短,但中斷較多,帶來了上下文切換頻繁的問題。
因?yàn)槊糠N方案都其適用場(chǎng)景和缺點(diǎn),因此在實(shí)際應(yīng)用中,會(huì)根據(jù)實(shí)際情況選擇方案。
比如:低 (對(duì)象/s) 比率時(shí),中斷執(zhí)行GC的頻率,simple GC更低些;如果大量對(duì)象都是長期“存活”,則分代處理優(yōu)勢(shì)也不大。
5. vue中的內(nèi)存泄漏問題JS程序的內(nèi)存溢出后,會(huì)使某一段函數(shù)體永遠(yuǎn)失效(取決于當(dāng)時(shí)的JS代碼運(yùn)行到哪一個(gè)函數(shù)),通常表現(xiàn)為程序突然卡死或程序出現(xiàn)異常。
這時(shí)我們就要對(duì)該JS程序進(jìn)行內(nèi)存泄漏的排查,找出哪些對(duì)象所占用的內(nèi)存沒有釋放。這些對(duì)象通常都是開發(fā)者以為釋放掉了,但事實(shí)上仍被某個(gè)閉包引用著,或者放在某個(gè)數(shù)組里面。
5.1 泄漏點(diǎn)DOM/BOM 對(duì)象泄漏
script 中存在對(duì)DOM/BOM 對(duì)象的引用導(dǎo)致
js 對(duì)象泄漏
通常由閉包導(dǎo)致,比如事件處理回調(diào),導(dǎo)致DOM對(duì)象和腳本中對(duì)象雙向引用,這個(gè)時(shí)常見的泄漏原因
5.2 代碼關(guān)注點(diǎn)DOM中的 addEventLisner 函數(shù)及派生的事件監(jiān)聽, 比如 Jquery 中的 on 函數(shù), vue 組件實(shí)例的 $on 函數(shù),第三方庫中的初始化函數(shù)
其它BOM對(duì)象的事件監(jiān)聽, 比如websocket 實(shí)例的on 函數(shù)
避免不必要的函數(shù)引用
如果使用 render 函數(shù),避免在html標(biāo)簽中綁定DOM/BOM 事件
5.3 如何處理如果在mounted/created 鉤子中綁定了 DOM/BOM 對(duì)象中的事件,需要在 beforeDestroy 中做對(duì)應(yīng)解綁處理
如果在mounted/created 鉤子中使用了第三方庫初始化,需要在beforeDestroy 中做對(duì)應(yīng)銷毀處理
如果組件中使用了定時(shí)器,需要在beforeDestroy中做對(duì)應(yīng)銷毀處理
模板中不要使用表達(dá)式來綁定到特定的處理函數(shù),這個(gè)邏輯應(yīng)該放在處理函數(shù)中?
如果在mounted/created 鉤子中使用了$on,需要在beforeDestroy 中做對(duì)應(yīng)解綁$off處理
某些組件在模板中使用事件綁定可能會(huì)出現(xiàn)泄漏,使用$on 替換模板中的綁定
5.4 在vue組件中處理addEventListenercreated/mounted 生命期鉤子函數(shù)中定義事件響應(yīng)函數(shù)為對(duì)象實(shí)例的方法,使用 => 函數(shù)來綁定作用域
調(diào)用 addEventListener 添加事件監(jiān)聽后在 beforeDestroy 中調(diào)用 removeEventListener 移除對(duì)應(yīng)的事件監(jiān)聽,注意前面定義的響應(yīng)函數(shù)方法需要作為第二個(gè)參數(shù)傳入
然后用 delete 從對(duì)象實(shí)例移除定義的響應(yīng)方法,或者將屬性設(shè)置為 null/undefined
為了準(zhǔn)確移除監(jiān)聽,不要使用匿名函數(shù)或者已有的函數(shù)的綁定來直接作為事件監(jiān)聽函數(shù)
mounted() { const box = document.getElementById("time-line") this.width = box.offsetWidth this.resizefun = () => { this.width = box.offsetWidth } window.addEventListener("resize", this.resizefun) }, beforeDestroy() { window.removeEventListener("resize", this.resizefun) this.resizefun = null }5.5 觀察者模式引起的內(nèi)存泄漏
在spa應(yīng)用中使用觀察者模式的時(shí)候如果給觀察者注冊(cè)了被觀察的方法,而沒有在離開組件的時(shí)候及時(shí)移除,可能造成重復(fù)注冊(cè)而內(nèi)存泄漏;
舉個(gè)栗子:
進(jìn)入組件的時(shí)候ob.addListener("enter", _func),如果離開組件beforeDestroy的時(shí)候沒有ob.removeListener("enter", _func),就會(huì)導(dǎo)致內(nèi)存泄漏
更詳細(xì)的栗子參考:德州撲克栗子
5.6 上下文綁定引起的內(nèi)存泄漏有時(shí)候使用 bind/apply/call 上下文綁定方法的時(shí)候,會(huì)有內(nèi)存泄漏的隱患。
var ClassA = function(name) { this.name = name this.func = null } var a = new ClassA("a") var b = new ClassA("b") b.func = bind(function() { console.log("I am " + this.name) }, a) b.func() // 輸出: I am a a = null // 釋放a //b = null; // 釋放b //b.func = null; // 釋放b.func function bind(func, self) { //模擬上下文綁定 return function() { return func.apply(self) } }
使用chrome dev tool > memory > profiles 查看內(nèi)存中ClassA的實(shí)例數(shù),發(fā)現(xiàn)有兩個(gè)實(shí)例,a和b。雖然a設(shè)置成null了,但是b的方法中bind的閉包上下文self綁定了a,因此雖然a釋放,但是b/b.func沒有釋放,閉包的self一直存在并保持對(duì)a的引用。
網(wǎng)上的帖子大多深淺不一,甚至有些前后矛盾,在下的文章都是學(xué)習(xí)過程中的總結(jié),如果發(fā)現(xiàn)錯(cuò)誤,歡迎留言指出~
參考:
跟我學(xué)習(xí)javascript的垃圾回收機(jī)制與內(nèi)存管理
App之性能優(yōu)化
Vue Web App 內(nèi)存泄漏-調(diào)試和分析
搞定JavaScript內(nèi)存泄漏
推介閱讀:
雅虎網(wǎng)站頁面性能優(yōu)化的34條黃金守則
用 Chrome 開發(fā)者工具分析 javascript 的內(nèi)存回收(GC)
JS內(nèi)存泄漏排查方法——Chrome Profiles
Javascript典型內(nèi)存泄漏及chrome的排查方法
PS:歡迎大家關(guān)注我的公眾號(hào)【前端下午茶】,一起加油吧~
另外可以加入「前端下午茶交流群」微信群,長按識(shí)別下面二維碼即可加我好友,備注加群,我拉你入群~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/90737.html
摘要:解決方式是,當(dāng)我們不使用它們的時(shí)候,手動(dòng)切斷鏈接淘汰把和對(duì)象轉(zhuǎn)為了真正的對(duì)象,避免了使用這種垃圾收集策略,消除了以下常見的內(nèi)存泄漏的主要原因。以上參考資料高程垃圾收集類內(nèi)存泄漏及如何避免內(nèi)存泄露及解決方案詳解類內(nèi)存泄漏及如何避免 showImg(http://ww1.sinaimg.cn/large/005Y4rCogy1ft1ikzcqzqj30ka0et77a.jpg); 前言 起...
摘要:垃圾回收內(nèi)存管理實(shí)踐先通過一個(gè)來看看在中進(jìn)行垃圾回收的過程是怎樣的內(nèi)存泄漏識(shí)別在環(huán)境里提供了方法用來查看當(dāng)前進(jìn)程內(nèi)存使用情況,單位為字節(jié)中保存的進(jìn)程占用的內(nèi)存部分,包括代碼本身?xiàng)6选? showImg(https://segmentfault.com/img/remote/1460000019894672?w=640&h=426);作者 | 五月君Node.js 技術(shù)棧 | https:...
摘要:如果沒有引用指向該對(duì)象零引用,對(duì)象將被垃圾回收機(jī)制回收。經(jīng)過增量標(biāo)記改進(jìn)后,垃圾回收的最大停頓時(shí)間可以減少到原來的左右。解除引用的真正作用是讓值脫離執(zhí)行環(huán)境,以便垃圾收集器下次運(yùn)行時(shí)將其回收。 前言 在講 JS 的垃圾回收(Garbage Collection)之前,我們回顧上一篇《JS專題之memoization》,memoization 的原理是以參數(shù)作為 key,函數(shù)結(jié)果作為 v...
摘要:局部變量只在函數(shù)執(zhí)行過程中存在。此時(shí),局部變量就沒有存在的必要了,因此可以釋放它們的內(nèi)存以供將來使用。總結(jié)一般情況下,局部變量的生命周期為函數(shù)對(duì)象執(zhí)行到執(zhí)行結(jié)束,全局變量的生命周期為瀏覽器打開和關(guān)閉。 垃圾收集 JavaScript具有自動(dòng)垃圾收集機(jī)制,也就是說,執(zhí)行環(huán)境會(huì)負(fù)責(zé)管理代碼執(zhí)行中使用的內(nèi)存。在C和C++語言中,開發(fā)人員一項(xiàng)基本任務(wù)就是手工跟蹤內(nèi)存的使用情況,這是造成許多問題...
摘要:所謂的內(nèi)存泄漏簡單來說是不再用到的內(nèi)存,沒有及時(shí)釋放。如果一個(gè)值不再需要了,引用數(shù)卻不為,垃圾回收機(jī)制無法釋放這塊內(nèi)存,從而導(dǎo)致內(nèi)存泄漏。 前言 程序的運(yùn)行需要內(nèi)存。只要程序提出要求,操作系統(tǒng)或者運(yùn)行時(shí)就必須供給內(nèi)存。所謂的內(nèi)存泄漏簡單來說是不再用到的內(nèi)存,沒有及時(shí)釋放。為了更好避免內(nèi)存泄漏,我們先介紹Javascript垃圾回收機(jī)制。 在C與C++等語言中,開發(fā)人員可以直接控制內(nèi)存的...
閱讀 3952·2021-11-18 13:21
閱讀 4759·2021-09-27 14:01
閱讀 3110·2019-08-30 15:53
閱讀 2388·2019-08-30 15:43
閱讀 1730·2019-08-30 13:10
閱讀 1508·2019-08-29 18:39
閱讀 887·2019-08-29 15:05
閱讀 3340·2019-08-29 14:14