javascript中的記憶體管理

flydean發表於2021-03-01

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

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

相關文章