隨着如今的編程語言功能愈來愈成熟、複雜,內存管理也容易被你們忽略。本文將會討論JavaScript中的內存泄漏以及如何處理,方便你們在使用JavaScript編碼時,更好的應對內存泄漏帶來的問題。javascript
像C語言這樣的編程語言,具備簡單的內存管理功能函數,例如malloc( )和free( )。開發人員可使用這些功能函數來顯式地分配和釋放系統的內存。前端
當建立對象和字符串等時,JavaScript就會分配內存,並在再也不使用時自動釋放內存,這種機制被稱爲垃圾收集。這種釋放資源看似是「自動」的,但本質是混淆的,這也給JavaScript(以及其餘高級語言)的開發人員產生了能夠不關心內存管理的錯誤印象。其實這是一個大錯誤。java
即便使用高級語言,開發人員也應該理解內存管理的知識。有時自動內存管理也會存在問題(例如垃圾收集器中的錯誤或實施限制等),開發人員必須瞭解這些問題才能正確地進行處理。算法
不管你使用的是什麼編程語言,內存生命週期幾乎都是同樣的:express
如下是對內存生命週期中每一個步驟發生的狀況的概述:編程
在硬件層面上,計算機的內存由大量的觸發器組成的。每一個觸發器包含一些晶體管,並可以存儲一位數據。單獨的觸發器能夠經過惟一的標識符來尋址,因此咱們能夠讀取和覆蓋它們。所以,從概念上講,咱們能夠把整個計算機內存看做是咱們能夠讀寫的一大塊空間。數組
不少東西都存儲在內存中:瀏覽器
編譯器和操做系統一塊兒工做,來處理大部分的內存管理,可是咱們須要瞭解從本質上發生了什麼。緩存
編譯代碼時,編譯器會檢查原始數據類型,並提早計算它們須要多少內存,而後將所需的內存分配給調用堆棧空間中的程序。分配這些變量的空間被稱爲堆棧空間,隨着函數的調用,內存會被添加到現有的內存之上。當終止時,空間以LIFO(後進先出)順序被移除。例如以下聲明:session
int n; // 4個字節 int x [4]; // 4個元素的數組,每個佔4個字節 double m; // 8個字節
編譯器插入與操做系統進行交互的代碼,以便在堆棧中請求所需的字節數來存儲變量。
在上面的例子中,編譯器知道每一個變量的確切內存地址。實際上,每當咱們寫入這個變量n,它就會在內部翻譯成「內存地址4127963」。
注意,若是咱們試圖訪問x[4],咱們將訪問與m關聯的數據。這是由於咱們正在訪問數組中不存在的元素 - 它比數組中最後一個數據實際分配的元素多了4個字節x[3],而且可能最終讀取(或覆蓋)了一些m比特。這對其他部分會產生不利的後果。
當函數調用其它函數時,每一個函數被調用時都會獲得本身的堆棧塊。它會保留全部的局部變量和一個程序計數器,還會記錄執行的地方。當功能完成時,其內存塊會被釋放,能夠再次用於其它目的。
如若咱們不知道編譯時,變量須要的內存數量時,事情就會變得複雜。假設咱們想要作以下事項:
int n = readInput(); //讀取用戶的輸入 ... //用「n」個元素建立一個數組
在編譯時,編譯器不知道數組須要多少內存,由於它是由用戶提供的輸入值決定的。
所以,它不能爲堆棧上的變量分配空間。相反,咱們的程序須要在運行時明確地向操做系統請求適當的空間。這個內存是從堆空間分配的。下表總結了靜態和動態內存分配之間的區別:
如今來解釋如何在JavaScript中分配內存。
JavaScript使得開發人員免於處理內存分配的工做。
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
基本上在JavaScript中使用分配的內存,意味着在其中讀寫。
這能夠經過讀取或寫入變量或對象屬性的值,或者甚至將參數傳遞給函數來完成。
大部份內存泄漏問題都是在這個階段產生的,這個階段最難的問題就是肯定什麼時候再也不須要已分配的內存。它一般須要開發人員肯定程序中的哪一個部分再也不須要這些內存,並將其釋放。
高級語言嵌入了一個名爲垃圾收集器的功能,其工做是跟蹤內存分配和使用狀況,以便在再也不須要分配內存的狀況下自動釋放內存。
不幸的是,這個過程沒法作到那麼準確,由於像某些內存再也不須要的問題是不能由算法來解決的。
大多數垃圾收集器經過收集不能被訪問的內存來工做,例如指向它的變量超出範圍的這種狀況。然而,這種方式只能收集內存空間的近似值,由於在內存的某些位置可能仍然有指向它的變量,但它卻不會被再次訪問。
因爲肯定一些內存是否「再也不須要」,是不可斷定的,因此垃圾收集機制就有必定的侷限性。下面將解釋主要垃圾收集算法及其侷限性的概念。
垃圾收集算法所依賴的主要概念之一就是內存引用。
在內存管理狀況下,若是一個對象訪問變量(能夠是隱含的或顯式的),則稱該對象引用另外一個對象。例如,JavaScript對象具備對其原對象(隱式引用)及其屬性值(顯式引用)的引用。
在這種狀況下,「對象」的概念擴展到比普通JavaScript對象更普遍的範圍,而且還包含函數範圍。
這是最簡單的垃圾收集算法。若是有零個引用指向它,則該對象會被認爲是「垃圾收集」 。
看看下面的代碼:
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.
在週期方面有一個限制。例以下面的例子,建立兩個對象並相互引用,這樣會建立一個循環引用。在函數調用以後,它們將超出範圍,因此它們其實是無用的,能夠被釋放。然而,引用計數算法認爲,因爲兩個對象中的每個都被引用至少一次,因此二者都不能被垃圾收集機制收回。
function f() { var o1 = {}; var o2 = {}; o1.p = o2; // o1 references o2 o2.p = o1; // o2 references o1. This creates a cycle. } f( );
爲了決定是否須要對象,標記和掃描算法會肯定對象是不是活動的。
標記和掃描算法通過如下3個步驟:
roots:一般,root是代碼中引用的全局變量。例如,在JavaScript中,能夠充當root的全局變量是「窗口」對象。Node.js中的相同對象稱爲「全局」。全部root的完整列表由垃圾收集器構建。
而後算法會檢查全部root和他們的子對象而且標記它們是活動的(即它們不是垃圾)。任何root不能達到的,將被標記爲垃圾。
最後,垃圾回收器釋放全部未標記爲活動的內存塊,並將該內存返回給操做系統。
這個算法比引用計數垃圾收集算法更好。JavaScript垃圾收集(代碼/增量/併發/並行垃圾收集)領域中所作的全部改進都是對這種標記和掃描算法的實現改進,但不是對垃圾收集算法自己的改進。
在上面的相互引用例子中,在函數調用返回以後,兩個對象再也不被全局對象可訪問的對象引用。所以,它們將被垃圾收集器發現,從而進行收回。
即便在對象之間有引用,它們也不能從root目錄中訪問,從而會被認爲是垃圾而收集。
儘管垃圾收集器使用起來很方便,但它們也有本身的一套標準,其中之一是非決定論。換句話說,垃圾收集是不可預測的。你不能真正知道何時進行收集,這意味着在某些狀況下,程序會使用更多的內存,雖然這是實際須要的。在其它狀況下,在特別敏感的應用程序中,短暫暫停是極可能出現的。儘管非肯定性意味着不能肯定什麼時候進行集合,但大多數垃圾收集實現了共享在分配期間進行收集的通用模式。若是沒有執行分配,大多數垃圾收集會保持空閒狀態。如如下狀況:
在這種狀況下,大多數垃圾收集不會作出任何的收集工做。換句話說,即便有不可用的引用須要收集,可是收集器不會進行收集。雖然這並非嚴格的泄漏,但仍會致使內存使用率高於平時。
內存泄漏是應用程序使用過的內存片斷,在再也不須要時,不能返回到操做系統或可用內存池中的狀況。
編程語言有各自不一樣的內存管理方式。可是是否使用某一段內存,其實是一個不可斷定的問題。換句話說,只有開發人員明確的知道是否須要將一塊內存返回給操做系統。
1:全局變量
JavaScript以一種有趣的方式來處理未聲明的變量:當引用未聲明的變量時,會在全局對象中建立一個新變量。在瀏覽器中,全局對象將是window,這意味着
function foo(arg) { bar = "some text"; }
至關於:
function foo(arg) { window.bar = "some text"; }
bar只是foo函數中引用一個變量。若是你不使用var聲明,將會建立一個多餘的全局變量。在上述狀況下,不會形成很大的問題。可是,如如果下面的這種狀況。
你也可能不當心建立一個全局變量this:
function foo() { this.var1 = "potential accidental global"; } // Foo called on its own, this points to the global object (window) // rather than being undefined. foo( );
你能夠經過在JavaScript文件的開始處添加‘use strict’;來避免這中錯誤,這種方式將開啓嚴格的解析JavaScript模式,從而防止意外建立全局變量。
意外的全局變量固然是一個問題。更多的時候,你的代碼會受到顯式的全局變量的影響,而這些全局變量在垃圾收集器中是沒法收集的。須要特別注意用於臨時存儲和處理大量信息的全局變量。若是必須使用全局變量來存儲數據,那麼確保將其分配爲空值,或者在完成後從新分配。
2:被遺忘的定時器或回調
下面列舉setInterval的例子,這也是常常在JavaScript中使用。
對於提供監視的庫和其它接受回調的工具,一般在確保全部回調的引用在其實例沒法訪問時,會變成沒法訪問的狀態。可是下面的代碼倒是一個例外:
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); //This will be executed every ~5 seconds.
上面的代碼片斷顯示了使用引用節點或再也不須要的數據的定時器的結果。
該renderer對象可能會在某些時候被替換或刪除,這會使interval處理程序封裝的塊變得冗餘。若是發生這種狀況,那麼處理程序及其依賴項都不會被收集,由於interval須要先中止。這一切都歸結爲存儲和處理負載數據的serverData不會被收集的緣由。
當使用監視器時,你須要確保作了一個明確的調用來刪除它們。
幸運的是,大多數現代瀏覽器都會爲你作這件事:即便你忘記刪除監聽器,當被監測對象變得沒法訪問,它們就會自動收集監測處理器。這是過去的一些瀏覽器沒法處理的狀況(例如舊的IE6)。
看下面的例子:
var element = document.getElementById('launch-button'); var counter = 0; function onClick(event) { counter++; element.innerHtml = 'text ' + counter; } element.addEventListener('click', onClick); // Do stuff element.removeEventListener('click', onClick); element.parentNode.removeChild(element); // Now when element goes out of scope, // both element and onClick will be collected even in old browsers // that don't handle cycles well.
因爲現代瀏覽器支持垃圾回收機制,因此當某個節點變的不能訪問時,你再也不須要調用removeEventListener,由於垃圾回收機制會恰當的處理這些節點。
若是你正在使用jQueryAPI(其餘庫和框架也支持這一點),那麼也能夠在節點不用以前刪除監聽器。即便應用程序在較舊的瀏覽器版本下運行,庫也會確保沒有內存泄漏。
3:閉包
JavaScript開發的一個關鍵方面是閉包。閉包是一個內部函數,能夠訪問外部(封閉)函數的變量。因爲JavaScript運行時的實現細節,可能存在如下形式泄漏內存:
var theThing = null; var replaceThing = function(){ var originalThing = theThing; var unused = function(){ if(originalThing)//對'originalThing'的引用 console.log(「hi」); }; theThing = { longStr:new Array(1000000).join('*'), someMethod:function(){ console.log(「message」); } }; }; setInterval(replaceThing,1000);
一旦replaceThing被調用,theThing會獲取由一個大數組和一個新的閉包(someMethod)組成的新對象。然而,originalThing會被unused變量所持有的閉包所引用(這是theThing從之前的調用變量replaceThing)。須要記住的是,一旦在同一父做用域中爲閉包建立了閉包的做用域,做用域就被共享了。
在這種狀況下,閉包建立的範圍會將someMethod共享給unused。然而,unused有一個originalThing引用。即便unused從未使用過,someMethod 也能夠經過theThing在整個範圍以外使用replaceThing。並且someMethod經過unused共享了閉包範圍,unused必須引用originalThing以便使其它保持活躍(兩封閉之間的整個共享範圍)。這就阻止了它被收集。
全部這些均可能致使至關大的內存泄漏。當上面的代碼片斷一遍又一遍地運行時,你會看到內存使用率的不斷上升。當垃圾收集器運行時,其內存大小不會縮小。這種狀況會建立一個閉包的鏈表,而且每一個閉包範圍都帶有對大數組的間接引用。
4:超出DOM引用
在某些狀況下,開發人員會在數據結構中存儲DOM節點,例如你想快速更新表格中的幾行內容的狀況。若是在字典或數組中存儲對每一個DOM行的引用,則會有兩個對同一個DOM元素的引用:一個在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. }
在涉及DOM樹內的內部節點或葉節點時,還有一個額外的因素須要考慮。若是你在代碼中保留對錶格單元格(標籤)的引用,並決定從DOM中刪除該表格,還須要保留對該特定單元格的引用,則可能會出現嚴重的內存泄漏。你可能會認爲垃圾收集器會釋放除了那個單元以外的全部東西,但狀況並不是如此。因爲單元格是表格的一個子節點,而且子節點保留着對父節點的引用,因此對錶格單元格的這種引用,會將整個表格保存在內存中。
SpreadJS 純前端表格控件是基於 HTML5 的 JavaScript 電子表格和網格功能控件,提供了完備的公式引擎、排序、過濾、輸入控件、數據可視化、Excel 導入/導出等功能,適用於 .NET、Java 和移動端等各平臺在線編輯類 Excel 功能的表格程序開發。
以上內容是對JavaScript內存管理機制的講解,以及常見的四種內存泄漏的分析。但願對JavaScript的編程人員有所幫助。
原文連接:https://blog.sessionstack.com...
轉載請註明出自:葡萄城控件
葡萄城成立於1980年,是全球最大的控件提供商,世界領先的企業應用定製工具、企業報表和商業智能解決方案提供商,爲超過75%的全球財富500強企業提供服務。葡萄城於1988年在中國設立研發中心,在全球化產品的研發過程當中,不斷適應中國市場的本地需求,併爲軟件企業和各行業的信息化提供優秀的軟件工具和諮詢服務。