1. 程式人生 > 其它 >什麼是記憶體洩漏?Chrome瀏覽器中怎麼檢視記憶體佔用情況?

什麼是記憶體洩漏?Chrome瀏覽器中怎麼檢視記憶體佔用情況?

JavaScript,會在建立變數(物件,字串等)時分配記憶體,並且在不再使用它們時“自動”釋放記憶體,這個自動釋放記憶體的過程稱為垃圾回收。

因為自動垃圾回收機制的存在,讓大多Javascript開發者感覺他們可以不關心記憶體管理,所以會在一些情況下導致記憶體洩漏。

記憶體生命週期

JS 環境中分配的記憶體有如下宣告週期:

  1. 記憶體分配:當我們申明變數、函式、物件的時候,系統會自動為他們分配記憶體
  2. 記憶體使用:即讀寫記憶體,也就是使用變數、函式等
  3. 記憶體回收:使用完畢,由垃圾回收機制自動回收不再使用的記憶體

JS 的記憶體分配

為了不讓程式設計師費心分配記憶體,JavaScript 在定義變數時就完成了記憶體分配。

var n = 123; // 給數值變數分配記憶體
var s = "azerty"; // 給字串分配記憶體
var o = {
 a: 1,
 b: null
}; // 給物件及其包含的值分配記憶體
// 給陣列及其包含的值分配記憶體(就像物件一樣)
var a = [1, null, "abra"]; 
function f(a){
 return a + 2;
} // 給函式(可呼叫的物件)分配記憶體
// 函式表示式也能分配一個物件
someElement.addEventListener('click', function(){
 someElement.style.backgroundColor = 'blue';
}, false);

有些函式呼叫結果是分配物件記憶體:

var d = new Date(); // 分配一個 Date 物件
var e = document.createElement('div'); // 分配一個 DOM 元素

有些方法分配新變數或者新物件:

var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一個新的字串
// 因為字串是不變數,
// JavaScript 可能決定不分配記憶體,
// 只是儲存了 [0-3] 的範圍。
var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); 
// 新陣列有四個元素,是 a 連線 a2 的結果

JS 的記憶體使用

使用值的過程實際上是對分配記憶體進行讀取與寫入的操作。

讀取與寫入可能是寫入一個變數或者一個物件的屬性值,甚至傳遞函式的引數。

var a = 10; // 分配記憶體
console.log(a); // 對記憶體的使用

JS 的記憶體回收

JS 有自動垃圾回收機制,那麼這個自動垃圾回收機制的原理是什麼呢?

其實很簡單,就是找出那些不再繼續使用的值,然後釋放其佔用的記憶體。

大多數記憶體管理的問題都在這個階段。

在這裡最艱難的任務是找到不再需要使用的變數。

不再需要使用的變數也就是生命週期結束的變數,是區域性變數,區域性變數只在函式的執行過程中存在,

當函式執行結束,沒有其他引用(閉包),那麼該變數會被標記回收。

全域性變數的生命週期直至瀏覽器解除安裝頁面才會結束,也就是說全域性變數不會被當成垃圾回收。

因為自動垃圾回收機制的存在,開發人員可以不關心也不注意記憶體釋放的有關問題,但對無用記憶體的釋放這件事是客觀存在的。

不幸的是,即使不考慮垃圾回收對效能的影響,目前最新的垃圾回收演算法,也無法智慧回收所有的極端情況。


一、什麼是記憶體洩漏

程式的執行需要記憶體。只要程式提出要求,作業系統或執行時(runtime)就必須提供記憶體。對於持續執行的服務程序(daemon),必須及時釋放不再用到的記憶體。否則,記憶體佔用越來越高,輕則影響系統性能,重則導致程序崩潰。

本質上講,記憶體洩漏就是由於疏忽或錯誤造成程式未能釋放那些不再使用的記憶體,照成記憶體的浪費。

簡單地說就是申請了一塊記憶體空間,使用完畢後沒有釋放掉。它的一般表現方式是程式執行時間越長,佔用記憶體越多,最終用盡全部記憶體,整個系統崩潰。由程式申請的一塊記憶體,且沒有任何一個指標指向它,那麼這塊記憶體就洩露了。

二、記憶體洩漏的識別辦法

經驗法則是,如果連續5次垃圾回收之後,記憶體佔用一次比一次大,就有記憶體洩漏。

這就要求實時檢視記憶體的佔用情況。

三、在Chrome瀏覽器中,我們怎麼檢視記憶體佔用情況?

  1. 開啟開發者工具,選擇 Performance 面板
  2. 在頂部勾選 Memory
  3. 點選左上角的 record 按鈕
  4. 在頁面上進行各種操作,模擬使用者的使用情況
  5. 一段時間後,點選對話方塊的 stop 按鈕,面板上就會顯示這段時間的記憶體佔用情況(如下圖)

我們有兩種方式來判定當前是否有記憶體洩漏:

  1. 多次快照後,比較每次快照中記憶體的佔用情況,如果呈上升趨勢,那麼可以認為存在記憶體洩漏
  2. 某次快照後,看當前記憶體佔用的趨勢圖,如果走勢不平穩,呈上升趨勢,那麼可以認為存在記憶體洩漏

在伺服器環境中使用 Node 提供的 process.memoryUsage 方法檢視記憶體情況

console.log(process.memoryUsage());
// { 
// rss: 27709440,
// heapTotal: 5685248,
// heapUsed: 3449392,
// external: 8772 
// }

process.memoryUsage返回一個物件,包含了 Node 程序的記憶體佔用資訊。

該物件包含四個欄位,單位是位元組,含義如下:

  • rss(resident set size):所有記憶體佔用,包括指令區和堆疊。
  • heapTotal:"堆"佔用的記憶體,包括用到的和沒用到的。
  • heapUsed:用到的堆的部分。
  • external: V8 引擎內部的 C++ 物件佔用的記憶體。

判斷記憶體洩漏,以heapUsed欄位為準。

常見的記憶體洩露案例:

1.意外的全域性變數

function foo() {
 bar1 = 'some text'; // 沒有宣告變數 實際上是全域性變數 => window.bar1
 this.bar2 = 'some text' // 全域性變數 => window.bar2
}
foo();

在這個例子中,意外的建立了兩個全域性變數 bar1 和 bar2

2.被遺忘的定時器和回撥函式

在很多庫中, 如果使用了觀察者模式, 都會提供回撥方法, 來呼叫一些回撥函式。

要記得回收這些回撥函式。舉一個 setInterval的例子:

var serverData = loadData();
setInterval(function() {
 var renderer = document.getElementById('renderer');
 if(renderer) {
 renderer.innerHTML = JSON.stringify(serverData);
 }
}, 5000); // 每 5 秒呼叫一次

如果後續 renderer 元素被移除,整個定時器實際上沒有任何作用。

但如果你沒有回收定時器,整個定時器依然有效, 不但定時器無法被記憶體回收,

定時器函式中的依賴也無法回收。在這個案例中的 serverData 也無法被回收。

3.閉包

在 JS 開發中,我們會經常用到閉包,一個內部函式,有權訪問包含其的外部函式中的變數。

下面這種情況下,閉包也會造成記憶體洩露:

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 的物件。

同時 unused 是一個引用了 originalThing 的閉包。

這個範例的關鍵在於,閉包之間是共享作用域的,儘管 unused 可能一直沒有被呼叫,但是 someMethod 可能會被呼叫,就會導致無法對其記憶體進行回收。

當這段程式碼被反覆執行時,記憶體會持續增長。
4.DOM 引用

很多時候, 我們對 Dom 的操作, 會把 Dom 的引用儲存在一個數組或者 Map 中。

var elements = {
 image: document.getElementById('image')
};
function doStuff() {
 elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
 document.body.removeChild(document.getElementById('image'));
 // 這個時候我們對於 #image 仍然有一個引用, Image 元素, 仍然無法被記憶體回收.
}

上述案例中,即使我們對於 image 元素進行了移除,但是仍然有對 image 元素的引用,依然無法對齊進行記憶體回收。

另外需要注意的一個點是,對於一個 Dom 樹的葉子節點的引用。

舉個例子: 如果我們引用了一個表格中的td元素,一旦在 Dom 中刪除了整個表格,我們直觀的覺得記憶體回收應該回收除了被引用的 td 外的其他元素。

但是事實上,這個 td 元素是整個表格的一個子元素,並保留對於其父元素的引用。

這就會導致對於整個表格,都無法進行記憶體回收。所以我們要小心處理對於 Dom 元素的引用。

四、如何避免記憶體洩漏

記住一個原則:不用的東西,及時歸還。

  1. 減少不必要的全域性變數,使用嚴格模式避免意外建立全域性變數。
  2. 在你使用完資料後,及時解除引用(閉包中的變數,dom引用,定時器清除)。
  3. 組織好你的邏輯,避免死迴圈等造成瀏覽器卡頓,崩潰的問題。