天天看點

浏覽器垃圾回收機制浏覽器垃圾回收機制

浏覽器垃圾回收機制

簡介

由于字元串、對象和數組沒有固定大小,所有當他們的大小已知時,才能對他們進行動态的存儲配置設定。JavaScript 程式每次建立字元串、數組或對象時,解釋器都必須配置設定記憶體來存儲那個實體。隻要像這樣動态地配置設定了記憶體,最終都要釋放這些記憶體以便他們能夠被再用,否則,JavaScript 的解釋器将會消耗完系統中所有可用的記憶體,造成系統崩潰。

JavaScript 使用垃圾回收機制來自動管理記憶體。

現在各大浏覽器通常用采用的垃圾回收有兩種方法:标記清除、引用計數。

标記清除

現代浏覽器大多數采用這種方式:當變量進入環境時,将變量标記"進入環境",當變量離開環境時,标記為:“離開環境”。某一個時刻,垃圾回收器會過濾掉環境中的變量,以及被環境變量引用的變量,剩下的就是被視為準備回收的變量。

引用計數

引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明了一個變量并将一個引用類型指派給該變量時,則這個值的引用次數就是 1。相反,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數就減 1。當這個引用次數變成 0 時,則說明沒有辦法再通路這個值了,因而就可以将其所占的記憶體空間給收回來。

因為存在循環引用的情況會導緻記憶體無法釋放,需要手動值為 null,是以大多數的浏覽器已經放棄這種回收方式。

什麼時候觸發垃圾回收

一般浏覽器會自動觸發 GC,我們不用太過關注。但是和其他語言一樣,當觸發 GC 的時候,浏覽器就會停止工作。如果頻繁觸發 GC 頁面就會發生抖動現象。

一般的 GC 耗時在 100ms 左右,對于一般的程式來說夠了。但是對于一些流暢度要求高的程式來說就很麻煩,這就需要新引擎需要優化的地方。

chrome 的 GC 優化

V8 引擎的垃圾回收政策主要基于分代垃圾回收機制:

  • 将整個堆記憶體分為新生代記憶體和老齡代記憶體,所有的記憶體配置設定操作發生在新生代
  • 新生代記憶體又分成兩部分,From(使用) 空間和 To(閑置) 空間,所有的記憶體配置設定操作發生在 From 空間
  • 新生代空間發生 GC(複制算法)
    • From 空間中存活的對象複制到 To 空間,釋放未存活的對象
    • 轉換兩者的角色 From 空間變為 To 空間,To 空間變為 From 空間
    • 如果某個對象已經經曆過一次複制算法,就将該對象複制到老齡代空間
    • 如果 To 空間的使用率超過了 25%,将整個空間的對象複制到老齡代空間。主要是為了角色轉換之後留足配置設定記憶體的空間
  • 老齡代空間發生 GC (标記清除與标記合并)
    • 主要采用标記清除算法,通過标記清除算法清理未存活的對象
    • 清除算法完成之後會使記憶體空間出現不連續的狀态,這種記憶體碎片會對後續的記憶體配置設定造成問題。是以在記憶體空間不足的時候采用标記合并算法,将活着的對象移動到記憶體的一端,完成之後清除另外一端的對象
  • 新生代的 GC 觸發要比老齡代的頻繁
  • 一般浏覽器要求最高 60fps,算下來每幀 16.6ms。Chrome 為了縮短 GC 時間,它嘗試将工作分攤到每個空閑時間。它将檢查每個幀時間(16.6 ms)的剩餘時間,并嘗試為 GC 做一些工作。原文
    • 如果垃圾收集事件可能很快發生,V8 GC 将檢查每 n 個配置設定或 m 個時間機關。V8 GC 在任務排程程式中為事件注冊空閑任務。
    • 任務排程程式将排程空閑任務并使用可用空閑時間調用給定的回調。V8 GC 将檢查任務是否仍處于待處理狀态,以及是否有足夠的空閑時間來處理任務。
浏覽器垃圾回收機制浏覽器垃圾回收機制
浏覽器垃圾回收機制浏覽器垃圾回收機制

什麼操作會引起記憶體洩漏

1、意外的全局變量

leak 成為一個全局變量,不會被回收。

function leaks() {
  leak = 'xxxxxx'
}
           

2、閉包引起

閉包維持了 onclick 方法的内部變量,并且這個綁定在了 DOM 上。

function bindEvent() {
  var obj = document.createElement('XXX')
  obj.onclick = function() {}
}

// 解決方法 1
function bindEvent() {
  var obj = document.createElement('XXX')
  obj.onclick = onclickHandler
}
function onclickHandler() {}

// 解決方法 2
function bindEvent() {
  var obj = document.createElement('XXX')
  obj.onclick = function() {}
  obj = null
}
           

3、沒有清理的 DOM 元素

雖然我們用 removeChild 移除了 button 但是還在 elements 對象裡儲存着 button 的引用換言之, DOM 元素還在記憶體裡面。

var elements = {
  button: document.getElementById('button')
}
document.body.removeChild(document.getElementById('button'))
           

4、被遺忘的定時器或者回調

這樣的代碼很常見,如果 id 為 Node 的元素從 DOM 中移除。該定時器仍會存在,又因為回調函數中包含對 someResource 的引用,定時器外面的 someResource 也不會被釋放。

var someResource = getData()
setInterval(function() {
  var node = document.getElementById('Node')
  if (node) {
  node.innerHTML = JSON.stringify(someResource))
  }
}, 1000)
           

5、子元素存在引用引起的記憶體洩露

執行完畢後,兩個對象都已經離開環境,在标記清除方式下是沒有問題的,但是在引用計數政策下,因為 a 和 b 的引用次數不為 0,是以不會被垃圾回收器回收記憶體,如果 fn 函數被大量調用,就會造成記憶體洩露。

function fn() {
  var a = {}
  var b = {}
  a.pro = b
  b.pro = a
}
fn()