天天看點

javascript中的記憶體管理簡介記憶體生命周期JS中的垃圾回收器調試記憶體問題閉包Closures中的記憶體洩露

簡介

在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的部落格

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