C、C++語言需要手動管理內存的分配與釋放(常用方法:malloc(), calloc(), realloc()和free()等)。而JavaScript與Java、C#相似,內置了垃圾回收器,能自動管理內存的分配與釋放。
內存生命周期:
- 分配內存
- 使用分配的內存(讀與寫操作)
- 當應用程序不再需要時,釋放掉已分配的內存
雖然垃圾回收器能能自動管理內存分配、釋放,但並不意味着開發者不再需要關注內存管理。因為一些不好的編碼會導致內存泄露,即應用程序不再需要的內存沒有被釋放掉。因此了解內存管理是很重要的。
Javascript中的內存分配
當聲明變量時,JavaScript會自動為變量分配內存
var numberVar = 100; //為整數分配內存 var stringVar = 'node simplified'; // 為字符串分配內存 var objectVar = {a: 1}; // 為對象分配內存 var a = [1, null, 'abra']; // 為數組分配內存 function f(a) { return a + 2; } // 為函數分配內存
GC(Garbage collection)
垃圾回收是追蹤並釋放應用程序不再使用的內存過程。垃圾回收器通過算法來實現追蹤應用程序不再使用的內存。主要涉及的垃圾回收算法如下:
- Reference-counting garbage collection(引用計數)
- Mark-and-sweep algorithm(標記清除)
Reference-counting garbage collection(引用計數)
引用計數算法是一種最基礎的垃圾回收算法,當一個對象的引用數為零時,會被自動回收。該算法將一個對象的引用數為0時視為應用程序不再需要的內存。
!function (){ var o1 = {a: {b: 2}},// 兩個對象被創建。假如分別用A:{a: {b: 2}},B:{b: 2}表示,對象B被對象A的屬性a引用,對象A被賦值給變量o1。A和B的引用數都為1,因此不能被回收。 o2 = o1; // 將對象A賦給變量o2。此時A引用數為2,B引用數1。 o1 = 1;// 將變量o1對對象A引用切斷。此時A引用數為1,B引用數1。 var oa = o2.a; // 將對象B賦值給變量oa。此時A引用數為1,B引用數2。 o2 = 'foo'; // 將變量o2對對象A引用切斷。此時A引用數為0,B引用數1。因為對象A的a屬性被變量oa引用,因此對象A不能被釋放。 oa = null; // 將變量oa對對象B引用切斷。此時A引用數為0,B引用數0。A與B會被回收。 }()
引用計數的限制:循環引用
循環引用存在一個限制。如下實例,兩個對象相互引用,形成一個循環引用。正常情況下,當函數執行完后,對應的內存會被釋放掉。而引用計數算法會將循環引用對象的引用數都視為至少為1,因此不能被回收。
function f() { var o = {}; var o2 = {}; o.a = o2; // o references o2 o2.a = o; // o2 references o return 'azerty'; } f();
常見問題
IE6-7的DOM對象是基於計數引用算法進行垃圾回收的。而循環引用通常會導致內存泄露:
var div; window.onload = function() { div = document.getElementById('myDivElement'); div.circularReference = div; div.lotsOfData = new Array(10000).join('*'); };
如上述實例,DOM元素div通過自身的“circularReference”屬性循環引用自己。如果沒有顯式將該屬性刪除或設為null,計數引用垃圾回收器會始終持有至少一個引用。即使DOM元素從DOM樹種移除,DOM元素的內存會一直存在。如果DOM元素持有一些數據(如實例中“lotsData”屬性),該數據對應的內存也無法被釋放。了解更多參考--->IE<8循環引用導致的內存泄露
Mark-and-sweep algorithm(標記清除)
該算法將“對象不再需要”的定義簡化為“對象不可到達”。 這個算法假設有一組被稱為roots的對象(在JavaScript中,root就是全局對象)。垃圾回收器會定期地從這些roots開始,查找所有從根開始引用的對象,然后再查找這些對象引用的對象……。從roots開始,垃圾回收器會查找所有可到達對象,並回收不可到達的對象。
為了確定對象是否需要,該算法要確定對象是否可到達。由如下步驟組成:
- 垃圾回收器會創建一組roots,roots通常是持有引用的全局變量。在JavaScript中,window對象就可作為root的全局變量。
- 垃圾回收器會檢查所有的roots並標記為活躍狀態。然后遞歸遍歷所有的子變量。只要從root不能到達的都被標記為垃圾。
- 所有沒有被標記為活躍狀態的內存塊都被視為垃圾。垃圾回收器就可以釋放這部分內存並把釋放的內存返回給操作系統。
這個算法比引用計數算法更優,因為對於引用計數算法“零引用的對象”總是不可到達的,但反之則不一定,如循環引用。而標記清除算法不存在循環引用的問題。
截至2012年,所有現代瀏覽器都內置了標記清除垃圾回收器。在過去幾年里所有對JavaScript垃圾回收算法的改進(generational/incremental/concurrent/parallel garbage collection)都是基於標記清除算法來實現的,但並沒有改變標記清除算法本身和它對“對象不再需要”定義的簡化。
循環引用不再是問題
前面循環引用的實例中,在函數執行完后,兩個對象不再被全局對象可訪問的對象引用。因此這兩個對象被垃圾回收器標記為不可到達,接着被回收掉。
限制:需要明確無法到達的對象
對於這個限制,實踐中很少遇見,所以開發者不太會去關心垃圾回收機制。
參考文章:
- Memory Management And Garbage Collection In Javascript
- Memory Management
- How JavaScript works: memory management + how to handle 4 common memory leaks
- Memory Management Reference
原文地址: https://github.com/cucygh/js-leakage-patterns/blob/master/JavaScript%E5%86%85%E5%AD%98%E9%82%A3%E7%82%B9%E4%BA%8B/JavaScript%E5%86%85%E5%AD%98%E9%82%A3%E7%82%B9%E4%BA%8B.md