javascript中的內存管理

簡介

在c語言中,我們需要手動分配和釋放對象的內存,但是在java中,所有的內存管理都交給了java虛擬機,程序員不需要在手動進程內存的分配和釋放,大大的減少了程序編寫的難度。

同樣的,在javascript中,內存管理也是自動進行的,雖然有自動的內存管理措施,但是這並不意味着程序員就不需要關心內存管理了。

本文將會進行詳細的介紹javascript中的內存管理策略。

內存生命週期

對於任何程序來說,內存的生命週期通常都是一樣的。

可以分爲三步:

  1. 在可用空間分配內存
  2. 使用該內存空間
  3. 在使用完畢之後,釋放該內存空間

所有的程序都需要手動執行第二步,對於javascript來說,第1,3兩步是隱式實現的。

我們看下javascript中分配內存空間的例子。

通過初始化分配內存空間:

var n = 123; // 爲數字分配內存
var s = 'azerty'; // 爲String分配內存

var o = {
  a: 1,
  b: null
}; // 爲對象分配內存

// 爲數組分配內存
var a = [1, null, 'abra']; 

function f(a) {
  return a + 2;
} // 爲函數分配內存
 

通過函數調用分配內存空間:

var d = new Date(); // 通過new分配date對象

var e = document.createElement('div'); // 分配一個DOM對象

var s = 'azerty';
var s2 = s.substr(0, 3); // 因爲js中字符串是不可變的,所以substr的操作將會創建新的字符串

var a = ['ouais ouais', 'nan nan'];
var a2 = ['generation', 'nan nan'];
var a3 = a.concat(a2); 
// 同樣的,concat操作也會創建新的字符串
 

釋放空間最難的部分就是需要判斷空間什麼時候不再被使用。在javascript中這個操作是由GC垃圾回收器來執行的。

垃圾回收器的作用就是在對象不再被使用的時候進行回收。

JS中的垃圾回收器

判斷一個對象是否可以被回收的一個非常重要的標準就是引用。

如果一個對象被另外一個對象所引用,那麼這個對象肯定是不能夠被回收的。

引用計數垃圾回收算法

引用計數垃圾回收算法是一種比較簡單和簡潔的垃圾回收算法。他把對象是否能夠被回收轉換成了對象是否仍然被其他對象所引用。

如果對象沒有被引用,那麼這個對象就是可以被垃圾回收的。

我們舉一個引用計數的例子:

var x = { 
  a: {
    b: 2
  }
}; 
//我們創建了兩個對象,a對象和a外面用大括號創建的對象。
// 我們將大括號創建的對象引用賦值給了x變量,所以x擁有大括號創建對象的引用,該對象不能夠被回收。
// 同時,因爲a對象是創建在大括號對象內部的,所以大括號對象默認擁有a對象的引用
// 因爲兩個對象都有引用,所以都不能夠被垃圾回收

var y = x;  //我們將x賦值給y,大括號對象現在擁有兩個引用

x = 1;   // 我們將1賦值給x,這樣只有y引用了大括號的對象

var z = y.a;  // 將y中的a對象引用賦值給z,a對象擁有兩個引用

y = 'flydean';  // 重新賦值給y,大括號對象的引用數爲0,大括號對象可以被回收了,但是因爲其內部的a對象還有一個z在被引用
                // 所以暫時不能被回收

z = null;       // z引用也被重新賦值,a對象的引用數爲0,兩個對象都可以被回收了
 

引用計數的一個缺點就是可能會出現循環引用的情況。

考慮下面的一個例子:

function f() {
  var x = {};
  var y = {};
  x.a = y;        // x references y
  y.a = x;        // y references x

  return 'flydean';
}

f();
 

在上面的例子中,x中的a屬性引用了y。而y中的a屬性又引用了x。

從而導致循環引用的情況,最終導致內存泄露。

在實際的應用中,IE6 和IE7 對DOM對象使用的就是引用計數的垃圾回收算法,所以可能會出現內存泄露的情況。

var div;
window.onload = function() {
  div = document.getElementById('myDivElement');
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join('*');
};
 

上面的例子中,DOM中的myDivElement元素使用circularReference引用了他本身,如果在引用計數的情況下,myDivElement是不會被回收的。

當myDivElement中包含了大量的數據的時候,即使myDivElement從DOM tree中刪除了,myDivElement也不會被垃圾回收,從而導致內存泄露。

Mark-and-sweep回收算法

講到這裏,大家是不是覺得JS的垃圾回收算法和java中的很類似,java中也有引用計數和mark-and-sweep清除算法。

這種回收算法的判斷標準是對象不可達。

在javascript中,通過掃描root對象(JS中的root對象那些全局對象),然後找到這些root對象的引用對象,然後再找到這些被引用對象的引用對象,一層一層的往後查找。

最後垃圾回收器會找到所有的可達的對象和不可達的對象。

使用不可達來標記不再被使用的對象可以有效的解決引用計數法中出現的循環引用的問題。

事實上,現在基本上所有的現代瀏覽器都支持Mark-and-sweep回收算法。

調試內存問題

如果發送了內存泄露,我們該怎麼調試和發現這個問題呢?

在nodejs中我們可以添加–inspect,然後藉助Chrome Debugger來完成這個工作:

node --expose-gc --inspect index.js
 

上面的代碼將會開啓nodejs的調試功能。

我們看下輸出結果:

Debugger listening on ws://127.0.0.1:9229/88c23ae3-9081-41cd-98b0-d0f7ebceab5a
For help, see: https://nodejs.org/en/docs/inspector
 

結果告訴了我們兩件事情,第一件事情就是debugger監聽的端口。默認情況下將會開啓127.0.0.1的9229端口。並且分配了一個唯一的UUID以供區分。

第二件事情就是告訴我們nodejs使用的調試器是Inspector。

使用Chrome devTools進行調試的前提是我們已經開啓了 –inspect模式。

在chrome中輸入chrome://inspect:

我們可看到chrome inspect的界面,如果你本地已經有開啓inspect的nodejs程序的話,在Remote Target中就可以直接看到。

選中你要調試的target,點擊inspect,即可開啓Chrome devTools調試工具:

你可以對程序進行profile,也可以進行調試。

閉包Closures中的內存泄露

所謂閉包就是指函數中的函數,內部函數可以訪問外部函數的參數或者變量,從而導致外部函數內部變量的引用。

我們看一個簡單閉包的例子:

 function parentFunction(paramA)
 {
 var a = paramA;
 function childFunction()
 {
 return a + 2;
 }
 return childFunction();
 }
 

上面的例子中,childFunction引用了parentFunction的變量a。只要childFunction還在被使用,a就無法被釋放,從而導致parentFunction無法被垃圾回收。事實上Closure默認就包含了對父function的引用。

我們看下面的例子:

 <html>
 <body>
 <script type="text/javascript">
 document.write("Program to illustrate memory leak via closure");
 window.onload=function outerFunction(){
 var obj = document.getElementById("element");
 obj.onclick=function innerFunction(){
 alert("Hi! I will leak");
 };
 obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
 // This is used to make the leak significant
 };
 </script>
 <button id="element">Click Me</button>
 </body>
 </html>
 

上面的例子中,obj引用了 DOM 對象element,而element的onclick是outerFunction的內部函數,從而導致了對外部函數的引用,從而引用了obj。

這樣最終導致循環引用,造成內存泄露。

怎麼解決這個問題呢?

一個簡單的辦法就是在使用完obj之後,將其賦值爲null,從而中斷循環引用的關係:

 <html>
 <body>
 <script type="text/javascript">
 document.write("Avoiding memory leak via closure by breaking the circular
 reference");
 window.onload=function outerFunction(){
 var obj = document.getElementById("element");
 obj.onclick=function innerFunction()
 {
 alert("Hi! I have avoided the leak");
 // Some logic here
 };
 obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
 obj = null; //This breaks the circular reference
 };
 </script>
 <button id="element">"Click Here"</button>
 </body>
 </html>
 

還有一種很簡潔的辦法就是不要使用閉包,將其分成兩個獨立的函數:

 <html>
 <head>
 <script type="text/javascript">
 document.write("Avoid leaks by avoiding closures!");
 window.onload=function()
 {
 var obj = document.getElementById("element");
 obj.onclick = doesNotLeak;
 }
 function doesNotLeak()
 {
 //Your Logic here
 alert("Hi! I have avoided the leak");
 }
 </script>
 </head>
 <body>
 <button id="element">"Click Here"</button>
 </body>
 </html>
 

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/js-memory-management/

本文來源:flydean的博客

歡迎關注我的公衆號:「程序那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,衆多你不知道的小技巧等你來發現!

 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章