浏覽器垃圾回收機制
簡介
由于字元串、對象和數組沒有固定大小,所有當他們的大小已知時,才能對他們進行動态的存儲配置設定。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 将檢查任務是否仍處于待處理狀态,以及是否有足夠的空閑時間來處理任務。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL6llaNl3Zq10dJpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL1gzM2UzM0cTMwIzMwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
什麼操作會引起記憶體洩漏
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()