2026年3月13日 星期五

V8引擎運作原理與垃圾回收機制

🚀 深入 V8:從編譯原理到記憶體回收的效能實戰

今天在面試中踢到了鐵板。原本以為只要會寫 JS、會處理資料就好,沒想到面試官直接往底層挖,問我 「V8 引擎怎麼運作」 以及 「記憶體洩漏如何精確定位」

當下沒答好,回頭翻了資料才發現,這不只是理論,這跟我們在高併發情境下寫出的程式碼品質息息相關。這份紀錄將 V8 的運作機制與監控實戰整理出來,當作以後的防守指南。

一、 V8 引擎運作流程:從原始碼到機器碼

V8 引擎(Chrome 與 Node.js 的核心)不只是一個直譯器,它是一個強大的 JIT (Just-In-Time) 編譯器。

  1. 解析 (Parsing):將 JavaScript 原始碼轉化為 AST (抽象語法樹)

  2. 解釋 (Ignition):解釋器將 AST 轉為較低階的 Bytecode (位元組碼) 並立即執行。這保證了程式碼能快速啟動。

  3. 編譯優化 (TurboFan):監控程式碼運行(Profiling),如果某段函式被頻繁呼叫(熱點程式碼),編譯器會將其轉換為優化過的 機器碼 (Machine Code),效能直接噴發。

  4. 去優化 (Deoptimization):這是效能損耗的元兇。如果你原本傳入 Int,後來突然變成 String,V8 會發現之前的優化作廢,被迫拋棄機器碼,退回到位元組碼。

    💡 心法:盡量維持變數型別的穩定(Monormorphic),能幫助 V8 保持優化狀態。


二、 V8 垃圾回收機制 (Garbage Collection)

V8 將記憶體分為 新生代老生代,就像是公司的「實習生」與「資深員工」,管理制度完全不同。

1. 新生代:Scavenge 演算法

  • 特性:存活時間短、回收極其頻繁。

  • 機制:空間平分為 FromTo。新物件先放 From,回收時將還活著的物件複製到 To,然後清空 From 並將兩者角色對調(翻轉)。

  • 晉升 (Promotion):撐過兩次回收還沒死的物件,會被升遷到「老生代」。

2. 老生代:Mark-Sweep & Mark-Compact

  • 特性:存活長、空間大。

  • 機制

    • 標記 (Marking):從根節點(如 window)出發,標記所有還有人用的物件。

    • 清除 (Sweeping):把沒標記到的「垃圾」清掉。

    • 整理 (Compacting):把分散的物件推擠到一起,清出連續空間,避免「記憶體碎片」導致大物件放不進來。


三、 記憶體洩漏 (Memory Leak) 診斷實戰

當物件不再需要,卻仍被「根節點」間接引用,就會導致 GC 無法回收。在高併發系統中,這會演變成災難。

1. 常見的洩漏殺手

  • 意外的全域變數window.data = [...]

  • 被遺忘的計時器setInterval 沒清掉,裡面的閉包會一直抓著外部變數不放。

  • 脫離文件的 DOM:JS 變數存了某個按鈕,但該按鈕已經從網頁刪除了,這顆按鈕就成了「殭屍節點」。

2. 實戰定位:三點定位法

當你感覺頁面越來越卡時,請打開 Chrome DevTools:

  1. 快照 A:初始狀態。

  2. 快照 B:執行操作(例如重複開啟彈窗 10 次)。

  3. 快照 C:再次執行操作。

重點:使用 Comparison 模式比較 B 與 C。如果某個物件在 B 到 C 之間持續增加且沒消失,那 99% 就是洩漏點。


四、 程式碼優化建議:寫出對 GC 友善的 Code

  • 優先使用弱引用:如果只是想關聯資料,請用 WeakMapWeakSet,這不會阻止 GC 回收該物件。

  • 手動斷開引用:處理大型數據(如 5MB 的 JSON)後,用完記得 data = null

  • 生命週期清理:在 React 的 useEffect return 或 Vue 的 onUnmounted 中,養成習慣清除 Event Listener 和 Timer。


結語

面試官問這些,其實是在考你:「當系統在高載壓下效能下降,你有沒有科學的方法去排查?」

雖然平常寫 Code 不一定會天天看 AST,但了解 「去優化 (Deoptimization)」「記憶體碎片化」 的概念,能讓我們從寫出「會動的程式碼」,進化成寫出「高效的程式碼」。

沒有留言:

張貼留言