一篇看懂JS垃圾回收機制
前言
垃圾回收(Garbage Collection)是一種內存管理機制,用于檢測和清理不再被程序使用的內存。垃圾回收器會在 JS 引擎(瀏覽器或者 nodejs)內部周期性地運行,開發者無需手動操作。
但是,了解垃圾回收機制的工作原理有助于我們寫出更加高效的 JS 代碼,使 JS 引擎更好的幫助我們完成垃圾回收,避免我們開發的應用出現內存泄漏問題。
垃圾是怎樣產生的?
JS 中的數據類型有原始類型和引用類型,原始類型占用的內存極小,一般是字符串、數字、布爾值這些,他們被存放在棧(stack)中。引用類型可以是數組、普通對象或者函數,他們一般會包含較多的數據,所以引用類型的實際數據存放在內存的堆(heap)中,然后在棧中會存放一個指向該實際數據的地址。
const str = "abc" // 原始類型
const obj = { foo: "bar" } // 引用類型
在 JS 中每聲明一個變量,該應用所占的內存就會相應的增加,但機器的內存是有限的,內存占用不能無限制的增加。那些不再被程序使用的數據,就被稱為垃圾,他們所占用的內存就會被 JS 引擎的垃圾回收機制回收。
垃圾回收機制會回收哪些垃圾?
當一個對象不在任何地方被引用時(如何判斷是否被引用由算法決定,后面會介紹算法),垃圾回收器就需要將這些對象進行標記,并在適當的時機回收它們的內存。
以下面這段代碼為例:
function myFunction() {
const obj = { foo: "bar" }
// do something
}
myFunction()
這段代碼中的 obj 對象在 myFunction 函數執行之后,就已經沒有被引用了,就需要被回收。
但當某個對象從開發角度上來說不再被使用了,卻意外的仍然在某個地方被引用,垃圾回收器就無法回收它的內存,就會造成內存泄漏(內存逐漸累積,程序占用的內存越來越多,當超過系統的可用內存時,就會造成程序崩潰)。
比如下面這段代碼中 obj 對象就有可能不會被回收(取決于算法):
function myFunction() {
const obj = { foo: "bar" }
setTimeout(() => {
console.log(obj.foo)
}, 1000)
}
myFunction()
因為 obj 作為閉包中的引用傳遞給了定時器的回調函數,即使 myFunction 執行完畢,由于定時器沒有被清除,obj 仍然被定時器回調函數持有引用,就可能導致 obj 不會被垃圾回收。
垃圾回收的算法
目前 JavaScript 中的垃圾回收機制主要基于以下兩種算法:
引用計數(Reference-Counting)算法
該算法的原理是,記錄每個對象的引用次數,引用增加時計數器加一,引用減少時減一,當引用計數變為零時,就表示沒有任何引用指向該對象了,該對象就可以被回收。
這種規則導致該算法有一個缺點:無法回收循環引用的對象,因為互相引用的對象在程序結束時都至少有一次引用。比如下面這段代碼:
function myFunction() {
const a = {}
const b = {}
a.b = b // a 引用了 b
b.a = a // b 引用了 a
}
myFunction()
在該算法下,即使在 myFunction 執行之后,對象 a 和 b 也不會被回收。
IE 6 和 IE 7 使用的就是這種算法。
標記-清除(Mark-and-Sweep)算法
這是一種更常見的垃圾回收算法。核心概念是對象是否可達,當一個對象不在任何地方被引用時,它就是不可達,相反就是可達。該算法會從根對象(全局對象、函數執行上下文)開始,通過遍歷對象之間的引用關系,記錄所有可達和不可達的對象。然后,回收器清除不可達的對象,釋放其內存。
該算法解決了引用計數算法的循環引用問題,在剛剛那段代碼中,函數 myFunction 執行之后,對象 a 和 b 從全局對象出發就無法獲取了。因此,他們將會被回收。
從 2012 年起,所有現代瀏覽器都使用了這種算法。
Chrome 和 nodejs 的垃圾回收算法
Chrome 和 nodejs 都采用了谷-歌開源的 V8 引擎。V8 引擎的垃圾回收機制采用了標記-清除算法,但在此基礎做了一些優化。
V8 引擎將內存分為新生代(Young Generation)和老生代(Old Generation)。大多數對象在新生代中創建,經過一定時間后,如果它們仍然存活,就會被晉升到老生代。
Scavenger 垃圾回收(新生代)
新生代使用了 Scavenger 垃圾回收算法,它將內存劃分為一個存活區域和一個空閑區域。對象首先被分配到存活區域,當存活區域滿時,會執行垃圾回收操作,將存活的對象復制到空閑區域,并清空存活區域。
Mark-Sweep-Compact 垃圾回收(老生代)
老生代中使用了 Mark-Sweep-Compact(標記-清除-整理)垃圾回收算法。它首先標記所有的存活對象,然后清除掉未被標記的對象,最后進行內存整理,使存活對象連續排列,減少內存碎片。
增量垃圾回收
V8 引擎還支持增量垃圾回收。他會將垃圾回收操作分成多個小步驟執行,每個步驟之間會插入一些 JavaScript 代碼的執行,從而避免長時間的垃圾回收造成的界面卡頓。
空閑時間垃圾回收
V8 引擎還在空閑時間執行部分垃圾回收操作,以充分利用閑置的計算資源。這些時間段可能是在程序等待用戶輸入、網絡請求返回、或者其他暫時沒有任務需要處理的情況下出現的。
需要手動清除的內存
垃圾回收機制會根據算法智能的回收大部分的內存,但由于業務邏輯的關系,它無法明確知道在我們的寫的(垃圾)代碼中,哪些對象其實是不再使用的,所以我們在開發過程中需要及時的清除不需要的事件監聽、定時器、計時器,避免循環引用,以及避免使用閉包。
清除事件監聽
const myButton = document.getElementById("myButton")
function handleClick() {
console.log("Button clicked!")
}
// 添加事件監聽器
myButton.addEventListener("click", handleClick)
// 在頁面卸載或元素移除時解除事件監聽器
window.addEventListener("beforeunload", () => {
myButton.removeEventListener("click", handleClick)
})
執清除定時器、計時器
const timer = setTimeout(() => {}, 500)
// 在頁面卸載或元素移除時解除事件監聽器
window.addEventListener("beforeunload", () => {
clearTimeout(timer)
})
手動調用垃圾回收
一般情況下我們無需手動調用垃圾回收,但有些瀏覽器支持主動觸發垃圾回收。
IE 瀏覽器
if (typeof window.CollectGarbage === "function") {
window.CollectGarbage()
}
Opera 瀏覽器
if (window.opera && typeof window.opera.collect === "function") {
window.opera.collect()
}
總結
總而言之,垃圾回收是一項必不可少的內存管理機制,希望大家都能理解其原理,并運用在實際開發中,避免造成內存泄漏的問題,提高應用程序的性能和用戶體驗。