Garbage Collector (GC) 是 JS 中非常重要的概念,但是它的行為並不是很容易讓我們 debug,這篇文章使用了 FinalizationRegistry
來觀察 GC 的行為。如果你還不熟悉 GC 的概念,可以先閱讀由 Lin Clark 寫的 A Crash Course in Memory Management 。
FinalizationRegistry
是一個支援主流 browser 和 Node.js 的 API,它可以讓我們在物件被回收時執行一些程式碼。它的使用方式如下:在底下的程式碼中,由於 x
在 example()
執行完後不再被引用,因此它將被回收。當 x
被回收時,FinalizationRegistry
會執行 console.log(message)
。
const registry = new FinalizationRegistry ( ( message ) => console . log ( message ) ) ;
function example ( ) {
console . log ( "example() is running, waiting for GC..." ) ;
const x = { } ;
registry . register ( x , "x has been collected" ) ;
}
render ( < button onClick = { example } > Open console and click me </ button > ) ;
The output in the console will be:
example ( ) is running, waiting for GC .. . x has been collected
通常情況下,x
並不會馬上被回收,因為瀏覽器的 engine 可能會有其他更重要的事情需要先完成。但我們可以透過 DevTools > Memory > collect garbage icon
來強制觸發 GC。
DevTools > Memory > collect garbage icon 在第一個範例,我們嘗試將 x
設為 z
的屬性,並且將 x
設為 globalThis.temp
的值。在 example()
執行完一段時間後,z
和 y
會不再被任何東西引用,因此它們將會被回收。但 x
仍然被 globalThis.temp
引用,因此它不會被回收。如果我們將 globalThis.temp
設為 undefined
,則 x
也將被回收。
const registry = new FinalizationRegistry ( ( message ) => console . log ( message ) ) ;
function example ( ) {
console . log ( "example() is running, waiting for GC..." ) ;
const x = { } ;
const y = { } ;
const z = { x , y } ;
registry . register ( x , "x has been collected" ) ;
registry . register ( y , "y has been collected" ) ;
registry . register ( z , "z has been collected" ) ;
globalThis . temp = x ;
}
render ( < button onClick = { example } > Open console and click me </ button > ) ;
The output in the console will be:
example ( ) is running, waiting for GC .. . z has been collected y has been collected
在第二個範例,我們將 z.x
設為 globalThis.temp
的值。於是在 example()
執行完後,我們將沒辦法再取得 z
和 y
。
照理來說,z
和 y
應該會被回收,但實際上並沒有。可能是因為 engine 無法判斷 z.x
是否只是一個單純的值 (有可能是 z
的一個 getter function
),因此 engine 沒有將 z
回收,也就導致 y
也沒有被回收。
const registry = new FinalizationRegistry ( ( message ) => console . log ( message ) ) ;
function example ( ) {
console . log ( "example() is running, waiting for GC..." ) ;
const x = { } ;
const y = { } ;
const z = { x , y } ;
registry . register ( x , "x has been collected" ) ;
registry . register ( y , "y has been collected" ) ;
registry . register ( z , "z has been collected" ) ;
globalThis . temp = ( ) => z . x ;
}
render ( < button onClick = { example } > Open console and click me </ button > ) ;
The output in the console will be:
example ( ) is running, waiting for GC .. .
在第三個範例,我們將 eval function
設為 globalThis.temp
。由於 eval
可以執行任何程式碼,因此 engine 並不知道 eval
會執行什麼程式碼,因此它無法判斷同樣位在 example()
中的 x
是否還會被使用。因此 x
不會被回收。
事實上,就算我們將 globalThis.temp
設為 () => eval(1)
,x
也不會被回收。因為 engine 可能只要看到 eval
就會將所有在 example()
中的變數都保留下來。
const registry = new FinalizationRegistry ( ( message ) => console . log ( message ) ) ;
function example ( ) {
console . log ( "example() is running, waiting for GC..." ) ;
const x = { } ;
registry . register ( x , "x has been collected" ) ;
globalThis . temp = ( string ) => eval ( string ) ;
}
render ( < button onClick = { example } > Open console and click me </ button > ) ;
The output in the console will be:
example ( ) is running, waiting for GC .. .
這個範例和第一個很像,只是改用 DOM element 來代替 plain object。不同於 plain object,DOM element 會有連結到它的 parent 和 sibling。所以我們可以透過 globalThis.temp.parentElement
來取得 z
,透過 globalThis.temp.nextSibling
來取得 y
。因此 z
和 y
都不會被回收。
如果我們執行 temp.remove()
,y
和 z
將會被回收,因為 x
已經從它的 parent 中分離。但 x
不會被回收,因為它仍然被 temp
引用。
const registry = new FinalizationRegistry ( ( message ) => console . log ( message ) ) ;
function example ( ) {
console . log ( "example() is running, waiting for GC..." ) ;
const x = document . createElement ( "div" ) ;
const y = document . createElement ( "div" ) ;
const z = document . createElement ( "div" ) ;
z . append ( x ) ;
z . append ( y ) ;
registry . register ( x , "x has been collected" ) ;
registry . register ( y , "y has been collected" ) ;
registry . register ( z , "z has been collected" ) ;
globalThis . temp = x ;
}
render ( < button onClick = { example } > Open console and click me </ button > ) ;
The output in the console will be:
example ( ) is running, waiting for GC .. .