您好,登錄后才能下訂單哦!
介紹
瀏覽器的 Javascript 具有自動垃圾回收機制(GC:Garbage Collecation),也就是說,執行環境會負責管理代碼執行過程中使用的內存。其原理是:垃圾收集器會定期(周期性)找出那些不在繼續使用的變量,然后釋放其內存。但是這個過程不是實時的,因為其開銷比較大并且GC時停止響應其他操作,所以垃圾回收器會按照固定的時間間隔周期性的執行。
不再使用的變量也就是生命周期結束的變量,當然只可能是局部變量,全局變量的生命周期直至瀏覽器卸載頁面才會結束。局部變量只在函數的執行過程中存在,而在這個過程中會為局部變量在棧或堆上分配相應的空間,以存儲它們的值,然后在函數中使用這些變量,直至函數結束,而閉包中由于內部函數的原因,外部函數并不能算是結束。
還是上代碼說明吧:
function?fn1()?{ ?var?obj?=?{name:?'hanzichi',?age:?10}; } function?fn2()?{ ?var?obj?=?{name:'hanzichi',?age:?10}; ?return?obj; } var?a?=?fn1(); var?b?=?fn2(); 復制代碼
我們來看代碼是如何執行的。首先聲明了兩個函數,分別叫做 fn1 和 fn2,當 fn1 被調用時,進入 fn1 的環境,會開辟一塊內存存放對象 {name: 'hanzichi', age: 10},而當調用結束后,出了fn1的環境,那么該塊內存會被 JS 引擎中的垃圾回收器自動釋放;在 fn2 被調用的過程中,返回的對象被全局變量 b 所指向,所以該塊內存并不會被釋放。
這里問題就出現了:到底哪個變量是沒有用的?所以垃圾收集器必須跟蹤到底哪個變量沒用,對于不再有用的變量打上標記,以備將來收回其內存。用于標記的無用變量的策略可能因實現而有所區別,通常情況下有兩種實現方式:標記清除和引用計數。引用計數不太常用,標記清除較為常用。
2. 標記清除
js中最常用的垃圾回收方式就是標記清除。當變量進入環境時,例如,在函數中聲明一個變量,就將這個變量標記為“進入環境”。從邏輯上講,永遠不能釋放進入環境的變量所占用的內存,因為只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記為“離開環境”。
function?test(){ var?a?=?10?;?//?被標記?,進入環境? var?b?=?20?;?//?被標記?,進入環境 } test();?//?執行完畢?之后?a、b又被標離開環境,被回收。 復制代碼
垃圾回收器在運行的時候會給存儲在內存中的所有變量都加上標記(當然,可以使用任何標記方式)。然后,它會去掉環境中的變量以及被環境中的變量引用的變量的標記(閉包)。而在此之后再被加上標記的變量將被視為準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。最后,垃圾回收器完成內存清除工作,銷毀那些帶標記的值并回收它們所占用的內存空間。 到目前為止,IE9+、Firefox、Opera、Chrome、Safari 的 JS 實現使用的都是標記清除的垃圾回收策略或類似的策略,只不過垃圾收集的時間間隔互不相同。
3. 引用計數
引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明了一個變量并將一個引用類型值賦給該變量時,則這個值的引用次數就是 1。如果同一個值又被賦給另一個變量,則該值的引用次數加 1。相反,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數減 1。當這個值的引用次數變成 0 時,則說明沒有辦法再訪問這個值了,因而就可以將其占用的內存空間回收回來。這樣,當垃圾回收器下次再運行時,它就會釋放那些引用次數為 0 的值所占用的內存。
function?test()?{ ?var?a?=?{};?//?a指向對象的引用次數為1 ?var?b?=?a;?//?a指向對象的引用次數加1,為2 ?var?c?=?a;?//?a指向對象的引用次數再加1,為3 ?var?b?=?{};?//?a指向對象的引用次數減1,為2 } 復制代碼
Netscape Navigator3 是最早使用引用計數策略的瀏覽器,但很快它就遇到一個嚴重的問題:循環引用。循環引用指的是對象 A 中包含一個指向對象B的指針,而對象 B 中也包含一個指向對象 A 的引用。
function?fn()?{ ?var?a?=?{}; ?var?b?=?{}; ?a.pro?=?b; ?b.pro?=?a; } fn(); 復制代碼
以上代碼 a 和 b 的引用次數都是 2,fn 執行完畢后,兩個對象都已經離開環境,在標記清除方式下是沒有問題的,但是在引用計數策略下,因為 a 和 b 的引用次數不為 0,所以不會被垃圾回收器回收內存,如果 fn 函數被大量調用,就會造成內存泄露。在 IE7 與 IE8 上,內存直線上升。
我們知道,IE 中有一部分對象并不是原生 JS 對象。例如,其內存泄露 DOM 和 BOM 中的對象就是使用 C++ 以 COM 對象的形式實現的,而 COM 對象的垃圾回收機制采用的就是引用計數策略。因此,即使IE的js引擎采用標記清除策略來實現,但 JS 訪問的COM對象依然是基于引用計數策略的。換句話說,只要在 IE 中涉及 COM 對象,就會存在循環引用的問題。
var?element?=?document.getElementById("some_element"); var?myObject?=?new?Object(); myObject.e?=?element; element.o?=?myObject; 復制代碼
這個例子在一個 DOM 元素 element 與一個原生js對象 myObject 之間創建了循環引用。其中,變量 myObject 有一個屬性 e 指向 element 對象;而變量 element 也有一個屬性 o 回指 myObject。由于存在這個循環引用,即使例子中的 DOM 從頁面中移除,它也永遠不會被回收。
橙色是指直接被 js變量所引用,在內存里
紅色是指間接被 js變量所引用,如上圖,refB 被 refA 間接引用,導致即使 refB 變量被清空,也是不會被回收的
子元素 refB 由于 parentNode 的間接引用,只要它不被刪除,它所有的父元素(圖中紅色部分)都不會被刪除
另一個例子:
window.onload=function?outerFunction(){ ?var?obj?=?document.getElementById("element"); ?obj.onclick=function?innerFunction(){}; }; 復制代碼
這段代碼看起來沒什么問題,但是 obj 引用了 document.getElementById('element'),而 document.getElementById('element') 的 onclick 方法會引用外部環境中的變量,自然也包括 obj,是不是很隱蔽啊。(在比較新的瀏覽器中在移除Node的時候已經會移除其上的event了,但是在老的瀏覽器,特別是 IE 上會有這個 bug)
解決辦法:
最簡單的方式就是自己手工解除循環引用,比如剛才的函數可以這樣
myObject.element?=?null; element.o?=?null; window.onload=function?outerFunction(){ ?var?obj?=?document.getElementById("element"); ?obj.onclick=function?innerFunction(){}; ?obj=null; }; 復制代碼
將變量設置為 null 意味著切斷變量與它此前引用的值之間的連接。當垃圾回收器下次運行時,就會刪除這些值并回收它們占用的內存。
要注意的是,IE9+ 并不存在循環引用導致 DOM 內存泄露問題,可能是微軟做了優化,或者 DOM 的回收方式已經改變。
4. 內存管理
4.1 什么時候觸發垃圾回收?
垃圾回收器周期性運行,如果分配的內存非常多,那么回收工作也會很艱巨,確定垃圾回收時間間隔就變成了一個值得思考的問題。IE6的垃圾回收是根據內存分配量運行的,當環境中存在256個變量、4096個對象、64k 的字符串任意一種情況的時候就會觸發垃圾回收器工作,看起來很科學,不用按一段時間就調用一次,有時候會沒必要,這樣按需調用不是很好嗎?但是如果環境中就是有這么多變量等一直存在,現在腳本如此復雜,很正常,那么結果就是垃圾回收器一直在工作,這樣瀏覽器就沒法兒玩兒了。
微軟在IE7中做了調整,觸發條件不再是固定的,而是動態修改的,初始值和 IE6 相同,如果垃圾回收器回收的內存分配量低于程序占用內存的15%,說明大部分內存不可被回收,設的垃圾回收觸發條件過于敏感,這時候把臨街條件翻倍,如果回收的內存高于85%,說明大部分內存早就該清理了,這時候把觸發條件置回。這樣就使垃圾回收工作職能了很多
4.2 合理的GC方案
1. 基礎方案
Javascript引擎基礎GC方案是(simple GC):mark and sweep(標記清除),即:
遍歷所有可訪問的對象。
回收已不可訪問的對象。
2. GC的缺陷
和其他語言一樣,JS 的 GC 策略也無法避免一個問題:GC 時,停止響應其他操作,這是為了安全考慮。而 Javascript 的 GC 在 100ms 甚至以上,對一般的應用還好,但對于 JS 游戲,動畫對連貫性要求比較高的應用,就麻煩了。這就是新引擎需要優化的點:避免GC造成的長時間停止響應。
3. GC優化策略
David 大叔主要介紹了2個優化方案,而這也是最主要的2個優化方案了:
分代回收(Generation GC) 這個和Java回收策略思想是一致的,也是V8所主要采用的。目的是通過區分“臨時”與“持久”對象;多回收“臨時對象”區(young generation),少回收“持久對象”區(tenured generation),減少每次需遍歷的對象,從而減少每次GC的耗時。如圖:
這里需要補充的是:對于 tenured generation 對象,有額外的開銷:把它從 young generation 遷移到 tenured generation,另外,如果被引用了,那引用的指向也需要修改。 這里主要內容可以參考深入淺出Node中關于內存的介紹,很詳細~
增量GC?這個方案的思想很簡單,就是“每次處理一點,下次再處理一點,如此類推”。如圖:
這種方案,雖然耗時短,但中斷較多,帶來了上下文切換頻繁的問題。因為每種方案都其適用場景和缺點,因此在實際應用中,會根據實際情況選擇方案。
比如:低 (對象/s) 比率時,中斷執行GC的頻率,simple GC更低些;如果大量對象都是長期“存活”,則分代處理優勢也不大。
5. Vue 中的內存泄漏問題
JS 程序的內存溢出后,會使某一段函數體永遠失效(取決于當時的 JS 代碼運行到哪一個函數),通常表現為程序突然卡死或程序出現異常。
這時我們就要對該 JS 程序進行內存泄漏的排查,找出哪些對象所占用的內存沒有釋放。這些對象通常都是開發者以為釋放掉了,但事實上仍被某個閉包引用著,或者放在某個數組里面。
5.1 泄漏點
DOM/BOM 對象泄漏;
script 中存在對 DOM/BOM 對象的引用導致;
JS 對象泄漏;
通常由閉包導致,比如事件處理回調,導致 DOM 對象和腳本中對象雙向引用,這個是常見的泄漏原因;
5.2 代碼關注點
主要關注的就是各種事件綁定場景,比如:
DOM 中的 addEventLisner 函數及派生的事件監聽,比如 Jquery 中的 on 函數,Vue 組件實例的 $on 函數;
其它 BOM 對象的事件監聽, 比如 websocket 實例的 on 函數;
避免不必要的函數引用;
如果使用 render 函數,避免在 HTML 標簽中綁定 DOM/BOM 事件;
5.3 如何處理
如果在 mounted/created 鉤子中使用 JS 綁定了 DOM/BOM 對象中的事件,需要在 beforeDestroy 中做對應解綁處理;
如果在 mounted/created 鉤子中使用了第三方庫初始化,需要在 beforeDestroy 中做對應銷毀處理(一般用不到,因為很多時候都是直接全局 Vue.use);
如果組件中使用了 setInterval,需要在 beforeDestroy 中做對應銷毀處理;
5.4 在 vue 組件中處理 addEventListener
調用 addEventListener 添加事件監聽后在 beforeDestroy 中調用 removeEventListener 移除對應的事件監聽。為了準確移除監聽,盡量不要使用匿名函數或者已有的函數的綁定來直接作為事件監聽函數。
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 觀察者模式引起的內存泄漏
在 spa 應用中使用觀察者模式的時候如果給觀察者注冊了被觀察的方法,而沒有在離開組件的時候及時移除,可能造成重復注冊而內存泄漏;
舉個栗子: 進入組件的時候 ob.addListener("enter", _func),如果離開組件 beforeDestroy 的時候沒有 ob.removeListener("enter", _func),就會導致內存泄漏
5.6 上下文綁定引起的內存泄漏
有時候使用 bind/apply/call 上下文綁定方法的時候,會有內存泄漏的隱患。
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 查看內存中 ClassA 的實例數,發現有兩個實例,a 和 b。雖然 a 設置成 null 了,但是 b 的方法中 bind 的閉包上下文 self 綁定了 a,因此雖然 a 釋放,但是 b/b.func 沒有釋放,閉包的 self 一直存在并保持對 a 的引用。
網上的帖子大多深淺不一,甚至有些前后矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。