根據GitHut stats的統計數據顯示,javascript語言在Github中的活躍項目倉庫數量和總的push數量已經登上了榜首的位置,並且在愈來愈多的領域裏咱們都能看見javascript持續活躍的身影和不斷前行的腳步
javascript
在這篇文章中我將主要探討javascript內存管理,調用棧以及如何處理內存泄漏問題。 在後續的文章中我會繼續介紹事件循環以及js引擎的執行機制。java
javascript自帶垃圾回收機制,它能夠自動分配內存並回收再也不使用的內存,這也使不少開發者認爲咱們沒有必要去關注js的內存管理。可是我相信咱們在平時開發過程當中都或多或少的遇到過內存泄漏問題,理解javascript的內存管理機制能夠幫助咱們解決此類問題並寫出更好的代碼,並且做爲一名程序員,咱們也應當保持足夠的好奇心去了解咱們寫出的代碼在底層的運行原理。node
在進入具體探討以前,咱們先來看下內存究竟是什麼。內存從物理意義上是指由一系列晶體管構成的能夠存儲數據的迴路,從邏輯的角度咱們能夠將內存看做是一個巨大的可讀寫的比特數組。它存儲着咱們編寫的代碼以及咱們在代碼中定義的各種變量。對於不少靜態類型編程語言來講,在代碼進入編譯階段時編譯器會根據變量聲明時指定的類型提早申請分配給該變量的內存(好比,整型變量對應的是4個字節,浮點數對應8個字節)。內存區域分爲棧空間和堆空間兩部分,對於能夠肯定大小的變量,它們會被存儲在棧空間中,好比:git
int n; // 4 bytes
int x[4]; // array of 4 elements, each 4 bytes
double m; // 8 bytes
複製代碼
還有一種類型的變量,不能在編譯階段就肯定其須要多大的存儲區域,其佔用內存大小是在運行時肯定的,好比:程序員
int n = readInput(); // n的大小依賴於用戶的輸入
複製代碼
對於這一類型的變量,它們會被存儲在堆空間中。內存靜態分配(static allocation)與動態分配(Dynamic allocation)的區別以下所示:
算法
對於任何編程語言,內存的生命週期都基本一致,以下所示:
express
var n = 374; // allocates memory for a number
var s = 'sessionstack'; // allocates memory for a string
var o = {
a: 1,
b: null
}; // allocates memory for an object and its contained values
var a = [1, null, 'str']; // (like object) allocates memory for the
// array and its contained values
function f(a) {
return a + 3;
} // allocates a function (which is a callable object)
// function expressions also allocate an object
someElement.addEventListener('click', function() {
someElement.style.backgroundColor = 'blue';
}, false);
// 函數調用觸發的內存分配
var d = new Date(); // allocates a Date object
var e = document.createElement('div'); // allocates a DOM element
// 方法的調用也能夠觸發
var s1 = 'sessionstack';
var s2 = s1.substr(0, 3); // s2 is a new string
// Since strings are immutable,
// JavaScript may decide to not allocate memory,
// but just store the [0, 3] range.
var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2);
// new array with 4 elements being
// the concatenation of a1 and a2 elements
複製代碼
在討論該算法前,咱們先來看下什麼是引用(reference)。所謂引用是指一個對象與另外一個對象的鏈接關係,若是對象A能夠隱式或顯式的訪問對象B,那麼咱們就能夠說對象A擁有一個對對象B的引用,好比在javascript中一個object能夠經過__proto__訪問到其prototype對象(隱式),也能夠直接訪問其屬性(顯式)。
對於引用計數算法來講,它斷定一個目標是能夠被回收的標誌就是該目標再也不存在與其餘對象的引用關係,好比:編程
var o1 = {
o2: {
x: 1
}
};
// 2 objects are created.
// 'o2' is referenced by 'o1' object as one of its properties.
// None can be garbage-collected
var o3 = o1; // the 'o3' variable is the second thing that
// has a reference to the object pointed by 'o1'.
o1 = 1; // now, the object that was originally in 'o1' has a
// single reference, embodied by the 'o3' variable
var o4 = o3.o2; // reference to 'o2' property of the object.
// This object has now 2 references: one as
// a property.
// The other as the 'o4' variable
o3 = '374'; // The object that was originally in 'o1' has now zero
// references to it.
// It can be garbage-collected.
// However, what was its 'o2' property is still
// referenced by the 'o4' variable, so it cannot be
// freed.
o4 = null; // what was the 'o2' property of the object originally in
// 'o1' has zero references to it.
// It can be garbage collected.
複製代碼
在上述示例中,o4就是能夠被回收的,引用計數在大多數狀況下都是沒什麼問題的,可是當咱們遇到循環引用它就會遇到麻煩,好比:數組
function f() {
var o1 = {};
var o2 = {};
o1.p = o2; // o1 references o2
o2.p = o1; // o2 references o1. This creates a cycle.
}
f();
複製代碼
在上述示例中,o1,o2互相引用,使得彼此都不能被釋放。瀏覽器
標記清除判斷某個對象是否能夠被回收的標誌是該對象不能再被訪問到。其執行過程總共分爲三步:
內存泄漏是指再也不使用的內存區域沒有被回收,致使這一塊內存區域被白白浪費。雖然咱們有前面提到的垃圾回收算法,可是咱們在平常開發過程當中仍然會時時遇到內存泄漏的問題,主要有如下三種類型:
function foo(arg) {
bar = "some text";
}
複製代碼
在這段代碼中咱們在函數foo裏給變量bar賦值了一個字符串,可是咱們沒有在該函數做用域內先聲明它,在javascript執行過程當中,bar會被掛載到全局變量window中(假設當前是瀏覽器環境),看成是window的屬性。這帶來的問題是即便函數foo執行完畢,該變量仍然是可訪問到的,其佔用的內存不會獲得釋放,從而致使內存泄漏。
咱們在平常開發過程當中要儘可能避免使用全局變量,除了污染全局做用域的問題,內存泄漏也是一個不容忽視的因素。
2. 閉包
內部函數能夠訪問其外部做用域內的變量,閉包爲咱們編寫javascript代碼帶來史無前例的靈活性,可是閉包也有可能會帶來內存泄漏的風險。好比下面這段代碼:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // a reference to 'originalThing'
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);
複製代碼
在函數replaceThing中,函數unused會造成一個閉包並含有對originalThing的引用,一旦replaceThing執行,theThing會被賦予一個對象做爲新值,在該對象中也會定義一個新的閉包someMethod, 這兩個閉包是在相同的父級做用域中建立的,所以它們會共享外部做用域。因爲someMethod方法能夠經過theThing在replaceThing外部訪問到,即便unused沒有被調用,它對變量originalThing的引用會使該做用域不會被回收。由於每一個閉包做用域都含有對longstr的間接引用,這種狀態下會致使大量的內存泄漏。
3. dom引用
先來看下面這一段代碼:
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
// The image is a direct child of the body element.
document.body.removeChild(document.getElementById('image'));
// At this point, we still have a reference to #button in the
//global elements object. In other words, the button element is
//still in memory and cannot be collected by the GC.
}
複製代碼
在上述代碼中咱們在兩個地方保存了對image元素的引用,當函數removeImage執行時儘管image元素被刪除,可是全局變量elements中仍然存在對button元素的引用,內存回收時不會將該元素回收。
除此以外還有另外一種狀況也值得引發注意。若是你存在對某個table cell(td 標籤)的引用,當你在dom樹中刪除它所屬的table但該引用並無刪除時也一樣會發生內存泄漏,垃圾回收器並不會像你所想的那樣回收全部只保留cell,而是會將整個table都保存在內存中,由於該table是cell的父節點,該cell依然會保持對其父節點的引用。
調用棧是內存中的一塊存儲區域,它負責記錄程序當前的執行位置,咱們能夠經過一個示例來看下調用棧的工做模式,先來看以下代碼:
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
複製代碼
當這段代碼開始執行時,調用棧會隨着函數的調用發生變化
function foo() {
throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
foo();
}
function start() {
bar();
}
start();
複製代碼
當這段代碼執行時報錯提示以下所示:
function foo() {
foo();
}
foo();
複製代碼
它會不斷的調用自身,其調用棧存儲示意圖和執行報錯以下所示:
本篇文章主要探討了javascript中的內存管理策略,介紹了內存的分配,內存的回收以及三種容易致使內存泄漏的場景還有代碼執行用到的調用棧等等,屬於javascript中比較基礎但卻容易忽視的知識點,但願對您有所幫助。