V8 引擎是如何給 JS"打掃房間"的 ?
當前位置:點晴教程→知識管理交流
→『 技術文檔交流 』
JS 語言不像 C/C++, 讓程序員自己去開辟或者釋放內存,而是類似Java,采用自己的一套垃圾回收算法進行自動的內存管理。今天就從內存結構說起,一步步聊聊 V8 的垃圾回收機制。 先搞懂JS 的內存都存在哪里?JS 的內存存儲分兩塊:棧(Stack) 和堆(Heap) ,就像家里的 "鞋柜" 和 "儲物間"—— 常用的小東西放鞋柜,大件雜物放儲物間。 棧棧是一塊連續的內存空間,就像排隊的抽屜,每個抽屜大小固定。它主要存兩種東西:
棧的回收特別簡單:函數執行時會創建 " 堆堆是一塊不連續的內存空間,大小不固定,就像開放式儲物間,專門存引用類型(對象、數組、函數等)。比如創建一個 堆的麻煩在于:對象不會像棧里的變量那樣 "用完就走"。比如全局對象 V8 的內存結構V8 引擎把堆內存又細分了兩塊:
這就是 V8 的 "分代回收" 思路:不同生命周期的對象,用不同的方式回收,效率更高。 棧內存的回收棧的回收幾乎不用我們操心,全靠 JS 引擎的 "執行上下文管理"。舉個例子:
這種回收方式效率極高,幾乎不消耗額外性能,所以棧內存很少出問題。 新生代內存的回收新生代存的都是 "短命對象",比如循環里創建的臨時對象:
這些對象的回收,V8 用的是Scavenge 算法,核心是 "復制存活對象,清空剩余空間"。具體步驟可以腦補成這樣:
為什么要這么折騰?主要是為了避免 " 而復制到 To 空間時按順序排列,存活對象會擠在一起,剩下的空間是一整塊連續區域,下次分配新對象就很方便。 當然,這種方式也有代價:新生代內存實際只能用一半(總有一個空間閑置)。但好在新生代對象存活時間短,復制成本低,總體算下來比標記清除快得多。 老生代內存的回收當新生代的對象 "活過" 多次回收(比如被全局變量引用,或者在閉包里被長期持有),就會被 "
老生代的對象要么體積大,要么存活久,用 Scavenge 算法復制太費時間,所以 V8 換了套思路:標記 - 清除+標記 - 整理。 第一步:標記 - 清除
這種方式解決了引用計數法的 "循環引用" 問題。比如兩個對象互相引用,但都不再被全局訪問,標記階段它們不會被標記,清除階段會被回收 —— 而引用計數法會因為它們互相引用,計數不為 0,永遠不回收,導致內存泄漏。 第二步:標記 - 整理標記 - 清除后,堆內存會像被挖過的地一樣坑坑洼洼:存活對象零散分布,中間夾雜著被回收的空白區域(內存碎片)。下次想分配一個大對象,可能找不到連續的空間,明明總內存夠,卻分配失敗。 所以 V8 會緊接著做 "標記 - 整理":把所有存活對象往內存的一端 "擠",讓空白區域集中到另一端,形成一整塊連續的空閑內存。就像把衣柜里的衣服都推到左邊,右邊留出一大塊空地放新衣服。 老生代的"增量標記"JS 是單線程的,一旦開始垃圾回收,JS 代碼就會暫停(稱為 "Stop-The-World")。如果老生代內存很大,一次完整的標記 - 清除可能要卡 1 秒以上 —— 用戶點按鈕沒反應,頁面像死機了一樣。 為了解決這個問題,V8 用了增量標記:把原本一口氣完成的標記階段,拆成一小塊一小塊,穿插在 JS 代碼執行間隙。比如標記 10ms,就讓 JS 執行 20ms,再標記 10ms... ,如果循環,直到標記階段完成才進入內存碎片的整理上面來,不耽誤JS代碼的正常執行。 這樣一來,單次垃圾回收的阻塞時間從幾百毫秒降到幾十毫秒,用戶幾乎感覺不到卡頓。數據顯示,增量標記能把垃圾回收的阻塞時間減少到原來的 1/6,對大型應用來說太重要了。 最后搞懂 V8 的回收機制后,我總結了幾個對開發有用的點:
?轉自https://juejin.cn/post/7524812060761489418 該文章在 2025/7/10 9:53:21 編輯過 |
相關文章
正在查詢... |