[TOC]
# 垃圾回收機制
對垃圾回收算法而言,其核心思想就是如何判斷內存不再使用了
比較古老的說法是 **引用計數** 和 **標記清除**
## 引用計數
引用計數算法定義“內存不再使用”的標準很簡單,就是看一個對象是否有指向它的引用。如果沒有其他對象指向它了,說明該對象已經不再需了。
``` js
// 創建一個對象 person,他有兩個指向屬性 age 和 name 的引用
var person = {
age: 12,
name: 'aaaa'
};
person.name = null // 雖然設置為null,但因為 person 對象還有指向 name 的引用,因此name 不會回收
var p = person
person = 1 // 原來的 person 對象被賦值為 1,但因為有新引用 p 指向原 person 對象,因此它不會被回收
p = null // 原 person 對象已經沒有引用,很快會被回收
```
由上面可以看出,引用計數算法是個簡單有效的算法。但它卻存在一個致命的問題:循環引用。如果兩個對象相互引用,盡管他們已不再使用,垃圾回收器不會進行回收,導致內存泄露。比如下面這樣
``` js
function cycle () {
var o1 = {}
var o2 = {}
o1.a = o2
o2.a = o1
return "Cycle reference!"
}
cycle()
```
## 標記清除
標記清除算法將“不再使用的對象”定義為“無法達到的對象”。簡單來說,就是從根部(在 JS 中就是全局對象)出發定時掃描內存中的對象。凡是能從根部到達的對象,都是還需要使用的。那些無法由根部出發觸及到的對象被標記為不再使用,稍后進行回收。
從這個概念可以看出,無法觸及的對象包含了沒有引用的對象這個概念(沒有任何引用的對象也是無法觸及的對象)。但反之未必成立。
# V8引擎垃圾回收機制
可以閱讀[這篇文章](https://v8.dev/blog/concurrent-marking),最近看 《深入淺出 Node.js》淘到些 V8 垃圾回收機制的介紹。
## V8 的垃圾回收機制與內存限制
在一般的后端開發語言中,基本的內存使用上沒有什么限制,然而在 Node 中通過 JavaScript 使用內存時會發現只能使用部分內存(64 位系統下約為 1.4 GB,32 位系統下約為 0.7 GB)。在這樣的限制下,將會導致 Node 無法直接操作大內存對象,比如無法將一個 2GB 的文件讀入內存中進行字符串分析處理。(stream 模塊解決了這個問題)
造成這個問題的主要原因在于 Node 基于 V8 構建,V8 的內存管理機制在瀏覽器的應用場景下綽綽有余,但在 Node 中卻限制了開發者。所以我們有必要知曉 V8 的內存管理策略。
## V8 的對象分配
在 V8 中,所有的 JavaScript 對象(object)都是通過堆來進行分配的,Node 提供了 V8 中內存使用量的查看方式,如下:
```js
process.memoryUsage()
{ rss: 21434368,
heapTotal: 7159808,
heapUsed: 4455120,
external: 8224 }
```
其中,heapTotal 和 heapUsed 是 V8 的堆內存使用情況,前者是已申請到的堆內存,后者是當前使用的量。如果已申請的堆空閑內存不夠分配新的對象,將繼續申請堆內存,直到堆的大小超過 V8 的限制為止。
至于 V8 為何要限制堆的大小,主要是內存過大會導致垃圾回收引起 JavaScript 線程暫停執行的時間增長,應用的性能和響應會直線下降,這樣的情況不僅僅是后端服務無法接受,前端瀏覽器也無法接受。因此,在當時的考慮下直接限制堆內存是一個好的選擇。
不過 V8 也提供了選項讓我們打開這個限制,Node 在啟動時可以傳遞如下的選項:
```shell
node --max-old-space-size=1700 test.js // 單位為 MB 設置老生代的內存空間
node --max-new-space-size=1024 test.js // 單位為 KB 設置新生代的內存空間
```
上述參數在 V8 初始化時生效,一旦生效就不能再改變。
## V8 的垃圾回收機制
V8 的垃圾回收策略主要基于分代式垃圾回收機制,在實際應用中,人們發現沒有一種垃圾回收算法能夠勝任所有的場景,因為對象的生存周期長短不一,不同的算法只能針對特定情況具有最好的效果。因此,現代的垃圾回收算法按對象的存活時間將內存的垃圾回收進行不同的分代,然后分別對不同分代的內存施以更高效的算法。
在 V8 中,主要將內存分為新生代和老生代。新生代的對象為存活時間較短的對象,老生代的對象為存活時間較長或常駐內存的對象。

*Scavenge 算法*
在分代的基礎上,新生代的對象主要通過 Scavenge 算法進行垃圾回收,在 Scavenge 的具體實現中,主要采用了 Cheney 算法。
Cheney 算法是一種采用復制的方式實現的垃圾回收算法,它將堆內存一分為二,每一部分空間稱為 semispace。在這兩個 semispace 空間中,只有一個處于使用中,另一個處于閑置狀態。處于使用狀態的 semispace 空間稱為 From 空間,處于閑置狀態的空間稱為 To 空間。

當我們分配對象時,先是在 From 空間中進行分配。當開始進行垃圾回收時,會檢查 From 空間的存活對象,這些存活對象將被復制到 To 空間中,而非存活對象占用的空間將被釋放。
完成復制后,From 空間和 To 空間的角色發生對換。
- Scavenge 的缺點是只能使用堆內存中的一半
- Scavenge 是典型的犧牲空間換取時間的算法,適合應用于新生代中,因為新生代中對象的生命周期較短
- 當一個對象經過多次復制仍然存活時,它將會被認為是生命周期較長的對象,其隨后會被移動到老生代中,這一過程稱為**晉升**
*Mark-Sweep & Mark-Compact*
老生代中的對象生命周期較長,存活對象占較大比重,V8 在老生代主要采用 Mark-Sweep 和 Mark-Compact 相結合的方式進行垃圾回收
Mark-Sweep:標記清除,其分為標記和清除兩個階段。在標記階段遍歷堆中的所有對象,并標記活著的對象,在清除階段只清除沒有被標記的對象。Mark-Sweep 最大的問題在于進行一次標記清除回收后,內存空間會出現不連續的狀態,內存碎片會對后續的內存分配造成問題,比如碎片空間不足以分配一個大對象導致提前觸發垃圾回收。
于是就有了 Mark-Compact:標記整理,簡單來說就是標記完成后加一個整理階段,存活對象往一端移動(合并),整理完成后直接清理掉邊界外的內存。

*Incremental Marking*
為了避免出現 JavaScript 應用邏輯與垃圾回收器看到的不一致的情況,垃圾回收的 3 種基本算法需要將應用邏輯暫停下來,待執行完垃圾回收后再恢復執行應用邏輯,這種行為被稱為全停頓(stop-the-world)。
對于新生代來說,全停頓的影響不大,但是對于老生代就需要改善。
為了降低全堆垃圾回收帶來的停頓時間,V8 采用了增量標記(incremental marking)的技術,大概是將原本一口氣停頓完成的動作拆分為許多小“步進”,每做完一“步進”就讓 JavaScript 應用邏輯執行一小會兒,垃圾回收與應用邏輯交替執行直到標記階段完成。

V8 后續還引入了延遲清理(lazy sweeping)、增量式整理(incremental compaction)、[并發標記](https://www.oschina.net/translate/v8-javascript-engine) 等技術,感興趣的可以自行了解。
## 查看垃圾回收日志
啟動時添加 `--trace_gc` 參數,這樣在進行垃圾回收時,將會從標準輸出中打印垃圾回收的日志信息。
下面是一段示例,執行結束后,將會在 gc.log 文件中得到所有垃圾回收信息:
```shell
node --trace_gc -e "var a = []; for (var i = 0; i < 1000000; i++) a.push(new Array(100));" > gc.log
```
通過在 Node 啟動時使用 --prof 參數,可以得到 V8 執行時的性能分析數據:
```shell
node --prof test.js
```
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs