Chrome 瀏覽器垃圾回收機制與內存泄漏分析

垃圾回收機制

本部分的內容引用自《瀏覽器工做原理與實踐》html

一般狀況下,垃圾數據回收分爲手動回收自動回收兩種策略。前端

手動回收策略,什麼時候分配內存、什麼時候銷燬內存都是由代碼控制的。node

自動回收策略,產生的垃圾數據是由垃圾回收器來釋放的,並不須要手動經過代碼來釋放。git

JavaScript 中調用棧中的數據回收

JavaScript 引擎會經過向下移動 ESP(記錄當前執行狀態的指針) 來銷燬該函數保存在棧中的執行上下文。github

JavaScript 堆中的數據回收

在 V8 中會把堆分爲新生代老生代兩個區域,新生代中存放的是生存時間短的對象,老生代中存放的生存時間久的對象。web

新生區一般只支持 1~8M 的容量,而老生區支持的容量就大不少了。對於這兩塊區域,V8 分別使用兩個不一樣的垃圾回收器,以便更高效地實施垃圾回收。算法

  • 副垃圾回收器,主要負責新生代的垃圾回收。
  • 主垃圾回收器,主要負責老生代的垃圾回收。

不論什麼類型的垃圾回收器,它們都有一套共同的執行流程。chrome

  1. 第一步是標記空間中活動對象和非活動對象。所謂活動對象就是還在使用的對象,非活動對象就是能夠進行垃圾回收的對象。
  2. 第二步是回收非活動對象所佔據的內存。其實就是在全部的標記完成以後,統一清理內存中全部被標記爲可回收的對象。
  3. 第三步是作內存整理。通常來講,頻繁回收對象後,內存中就會存在大量不連續空間,咱們把這些不連續的內存空間稱爲內存碎片,。當內存中出現了大量的內存碎片以後,若是須要分配較大連續內存的時候,就有可能出現內存不足的狀況。因此最後一步須要整理這些內存碎片。(這步實際上是可選的,由於有的垃圾回收器不會產生內存碎片).
新生代中垃圾回收

新生代中用Scavenge 算法來處理,把新生代空間對半劃分爲兩個區域,一半是對象區域,一半是空閒區域。新加入的對象都會存放到對象區域,當對象區域快被寫滿時,就須要執行一次垃圾清理操做。數組

在垃圾回收過程當中,首先要對對象區域中的垃圾作標記;標記完成以後,就進入垃圾清理階段,副垃圾回收器會把這些存活的對象複製到空閒區域中,同時它還會把這些對象有序地排列起來,因此這個複製過程,也就至關於完成了內存整理操做,複製後空閒區域就沒有內存碎片了。瀏覽器

完成複製後,對象區域與空閒區域進行角色翻轉,也就是原來的對象區域變成空閒區域,原來的空閒區域變成了對象區域。這樣就完成了垃圾對象的回收操做,同時這種角色翻轉的操做還能讓新生代中的這兩塊區域無限重複使用下去.

爲了執行效率,通常新生區的空間會被設置得比較小,也正是由於新生區的空間不大,因此很容易被存活的對象裝滿整個區域。爲了解決這個問題,JavaScript 引擎採用了對象晉升策略,也就是通過兩次垃圾回收依然還存活的對象,會被移動到老生區中。

老生代中的垃圾回收

老生代中用標記 - 清除(Mark-Sweep)的算法來處理。首先是標記過程階段,標記階段就是從一組根元素開始,遞歸遍歷這組根元素(遍歷調用棧),在這個遍歷過程當中,能到達的元素稱爲活動對象,沒有到達的元素就能夠判斷爲垃圾數據.而後在遍歷過程當中標記,標記完成後就進行清除過程。它和副垃圾回收器的垃圾清除過程徹底不一樣,這個的清除過程是刪除標記數據。

清除算法後,會產生大量不連續的內存碎片。而碎片過多會致使大對象沒法分配到足夠的連續內存,因而又產生了標記 - 整理(Mark-Compact)算法,這個標記過程仍然與標記 - 清除算法裏的是同樣的,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存,從而讓存活對象佔用連續的內存塊。

全停頓

因爲 JavaScript 是運行在主線程之上的,一旦執行垃圾回收算法,都須要將正在執行的 JavaScript 腳本暫停下來,待垃圾回收完畢後再恢復腳本執行。咱們把這種行爲叫作全停頓

在 V8 新生代的垃圾回收中,因其空間較小,且存活對象較少,因此全停頓的影響不大,但老生代就不同了。若是執行垃圾回收的過程當中,佔用主線程時間太久,主線程是不能作其餘事情的。好比頁面正在執行一個 JavaScript 動畫,由於垃圾回收器在工做,就會致使這個動畫在垃圾回收過程當中沒法執行,這將會形成頁面的卡頓現象。

爲了下降老生代的垃圾回收而形成的卡頓,V8 將標記過程分爲一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成,咱們把這個算法稱爲增量標記(Incremental Marking)算法.

使用增量標記算法,能夠把一個完整的垃圾回收任務拆分爲不少小的任務,這些小的任務執行時間比較短,能夠穿插在其餘的 JavaScript 任務中間執行,這樣當執行上述動畫效果時,就不會讓用戶由於垃圾回收任務而感覺到頁面的卡頓了。

內存泄漏

再也不用到的內存,沒有及時釋放,就叫作內存泄漏(memory leak)。

內存泄漏發生的緣由

  1. 緩存

有時候爲了方便數據的快捷複用,咱們會使用緩存,可是緩存必須有一個大小上限纔有用。高內存消耗將會致使緩存突破上限,由於緩存內容沒法被回收。

  1. 隊列消費不及時

當瀏覽器隊列消費不及時時,會致使一些做用域變量得不到及時的釋放,於是致使內存泄漏。

  1. 全局變量

除了常規設置了比較大的對象在全局變量中,還多是意外致使的全局變量,如:

function foo(arg) {
    bar = "this is a hidden global variable";
}
複製代碼

在函數中,沒有使用 var/let/const 定義變量,這樣其實是定義在window上面,變成了window.bar。 再好比因爲this致使的全局變量:

function foo() {
    this.bar = "this is a hidden global variable";
}
foo()
複製代碼

這種函數,在window做用域下被調用時,函數裏面的this指向了window,執行時實際上爲window.bar=xxx,這樣也產生了全局變量。

  1. 計時器中引用沒有清除

先看以下代碼:

var someData = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someData));
    }
}, 1000);
複製代碼

這裏定義了一個計時器,每隔1s把一些數據寫到Node節點裏面。可是當這個Node節點被刪除後,這裏的邏輯其實都不須要了,但是這樣寫,卻致使了計時器裏面的回調函數沒法被回收,同時,someData裏的數據也是沒法被回收的。

  1. 閉包

看如下這個閉包:

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);
複製代碼

每次調用 replaceThingtheThing 會建立一個大數組和一個新閉包(someMethod)的新對象。同時,變量 unused 是一個引用 originalThing(theThing) 的閉包,閉包的做用域一旦建立,它們有一樣的父級做用域,做用域是共享的。

someMethod 能夠經過 theThing 使用,someMethodunused 分享閉包做用域,儘管 unused 從未使用,它引用的 originalThing 迫使它保留在內存中(防止被回收)。

所以,當這段代碼反覆運行,就會看到內存佔用不斷上升,垃圾回收器(GC)並沒有法下降內存佔用。

本質上,閉包的鏈表已經建立,每個閉包做用域攜帶一個指向大數組的間接的引用,形成嚴重的內存泄漏。

  1. 事件監聽

例如,Node.js 中 Agent 的 keepAlive 爲 true 時,可能形成的內存泄漏。當 Agent keepAlive 爲 true 的時候,將會複用以前使用過的 socket,若是在 socket 上添加事件監聽,忘記清除的話,由於 socket 的複用,將致使事件重複監遵從而產生內存泄漏。

內存泄漏的識別方法

  1. 使用 Chrome 任務管理器實時監視內存使用 打開 chrome 瀏覽器,點擊右上角主菜單,選擇更多工具->任務管理器,這樣就開啓了任務管理器面板,而後再右鍵點擊任務管理器的表格標題並啓用 JavaScript使用的內存,能看到這樣的面板:

下面兩列能夠告訴您與頁面的內存使用有關的不一樣信息:

GitHub

  1. 內存佔用空間(Memory) 列表示原生內存。DOM 節點存儲在原生內存中。 若是此值正在增大,則說明正在建立 DOM 節點。
  2. JavaScript使用的內存(JavaScript Memory) 列表示 JS 堆。此列包含兩個值。 您感興趣的值是實時數字(括號中的數字)。實時數字表示您的頁面上的可到達對象正在使用的內存量。 若是此數字在增大,要麼是正在建立新對象,要麼是現有對象正在增加。

當你頁面穩定下來以後,這兩個的值還在上漲,你就能夠查一查是否內存泄漏了。

  1. 利用chrome 時間軸記錄可視化內存泄漏

Performance(時間軸)可以面板直觀實時顯示JS內存使用狀況、節點數量、監聽器數量等。

打開 chrome 瀏覽器,調出調試面板(DevTools),點擊Performance選項(低版本是Timeline),勾選Memory複選框。一種比較好的作法是使用強制垃圾回收開始和結束記錄。在記錄時點擊 Collect garbage 按鈕 (強制垃圾回收按鈕) 能夠強制進行垃圾回收。 因此錄製順序能夠這樣:開始錄製前先點擊垃圾回收-->點擊開始錄製-->點擊垃圾回收-->點擊結束錄製。 面板介紹如圖:

GitHub
錄製結果如圖:
GitHub
首先,從圖中咱們能夠看出不一樣顏色的曲線表明的含義,這裏主要關注JS堆內存、節點數量、監聽器數量。鼠標移到曲線上,能夠在左下角顯示具體數據。在實際使用過程當中,若是您看到這種 JS 堆大小或節點大小不斷增大的模式,則可能存在內存泄漏。

  1. 使用堆快照發現已分離 DOM 樹的內存泄漏

只有頁面的 DOM 樹或 JavaScript 代碼再也不引用 DOM 節點時,DOM 節點纔會被做爲垃圾進行回收。 若是某個節點已從 DOM 樹移除,但某些 JavaScript 仍然引用它,咱們稱此節點爲「已分離」,已分離的 DOM 節點是內存泄漏的常見緣由。

同理,調出調試面板,點擊Memory,而後選擇Heap Snapshot,而後點擊進行錄製。錄製完成後,選中錄製結果,在 Class filter 文本框中鍵入 Detached,搜索已分離的 DOM 樹。 以這段代碼爲例:

<html>
<head>
</head>
<body>
<button id="createBtn">增長節點</button>
<script> 
var detachedNodes;

function create() {
  var ul = document.createElement('ul');
  for (var i = 0; i < 10; i++) {
    var li = document.createElement('li');
    ul.appendChild(li);
  }
  detachedTree = ul;
}

document.getElementById('createBtn').addEventListener('click', create);
</script>
</body>
</html>
複製代碼

點擊幾下,而後記錄。能夠獲得如下信息:

GitHub
舊版的面板,還會有顏色標註,黃色的對象實例表示它被JS代碼引用,紅色的對象實例表示被黃色節點引用的遊離節點。上圖是新版本的,不會有顏色標識。可是仍是能夠一個個來看,如上圖,點開節點,能夠看到下面的引用信息,上面能夠看出,有個HTMLUListElement(ul節點)被window.detachedNodes引用。再結合代碼,原來是沒有加var/let/const聲明,致使其成了全局變量,因此DOM沒法釋放。

  1. 按函數調查內存分配 打開面板,點擊JavaScript Profiler,若是沒看到這個選項,你能夠點調試面板右上角的三個點,選擇more tools,而後選擇。

ps: chrome 舊版的瀏覽器,這個功能在 Profiles 裏面,點Record Allocation Profile便可.

操做步驟:點start->在頁面進行你要檢測的操做->點stop。

GitHub
DevTools 按函數顯示內存分配明細。默認視圖爲 Heavy (Bottom Up),將分配了最多內存的函數顯示在最上方,還有函數的位置,你能夠看看是哪些函數佔用內存較多。

避免內存泄漏的方法

  1. 少用全局變量,避免意外產生全局變量
  2. 使用閉包要及時注意,有Dom元素的引用要及時清理。
  3. 計時器裏的回調沒用的時候要記得銷燬。
  4. 爲了不疏忽致使的遺忘,咱們可使用 WeakSetWeakMap結構,它們對於值的引用都是不計入垃圾回收機制的,表示這是弱引用。 舉個例子:
const wm = new WeakMap();

const element = document.getElementById('example');

wm.set(element, 'some information');
wm.get(element) // "some information"
複製代碼

這種狀況下,一旦消除對該節點的引用,它佔用的內存就會被垃圾回收機制釋放。Weakmap 保存的這個鍵值對,也會自動消失。

基本上,若是你要往對象上添加數據,又不想幹擾垃圾回收機制,就可使用 WeakMap。

參考資料

最後

  • 歡迎加我微信(winty230),拉你進技術羣,長期交流學習...
  • 歡迎關注「前端Q」,認真學前端,作個有專業的技術人...

GitHub
相關文章
相關標籤/搜索