# JavaScript內存管理
點擊關注本[公眾號](http://www.hmoore.net/book/dsh225/javascript_vue_css/edit#_118)獲取文檔最新更新,并可以領取配套于本指南的《**前端面試手冊**》以及**最標準的簡歷模板**.
[TOC]
## 前言
像C語言這樣的底層語言一般都有底層的內存管理接口,比如 malloc()和free()。另一方面,JavaScript創建變量(對象,字符串等)時分配內存,并且在不再使用它們時“自動”釋放。 后一個過程稱為垃圾回收。這個“自動”是混亂的根源,并讓JavaScript(和其他高級語言)開發者感覺他們可以不關心內存管理,這是錯誤的。
> 本文主要參考了深入淺出nodejs中的內存章節
## 內存模型
平時我們使用的基本類型數據或者復雜類型數據都是如何存放的呢?
基本類型普遍被存放在『棧』中,而復雜類型是被存放在堆內存的。
> 如果你不了解執行棧和內存堆的概念,請先閱讀[JavaScript執行機制](https://www.cxymsg.com/guide/memory.html#mechanism.html)
當你讀完上述文章后,你會問,既然復雜類型被存放在內存堆中,執行棧的函數是如何使用內存堆的復雜類型?
實際上,執行棧的函數上下文會保存一個內存堆對應復雜類型對象的內存地址,通過引用來使用復雜類型對象。
一個例子:
~~~
function add() {
const a = 1
const b = {
num: 2
}
const sum = a + b.num
}
~~~
示意圖如下(我們暫時不考慮函數本身的內存)
還有一個問題是否所有的基本類型都儲存在棧中呢?
并不是,當一個基本類型被閉包引用之后,就可以長期存在于內存中,這個時候即使他是基本類型,也是會被存放在堆中的。
## 生命周期
不管什么程序語言,內存生命周期基本是一致的:
1. 分配你所需要的內存
2. 使用分配到的內存(讀、寫)
3. 不需要時將其釋放\\歸還

所有語言第二部分都是明確的。第一和第三部分在底層語言中是明確的,但在像JavaScript這些高級語言中,大部分都是隱含的。
## 內存回收
V8的垃圾回收策略基于分代回收機制,該機制又基于**世代假說**,該假說有兩個特點:
* 大部分新生對象傾向于早死
* 不死的對象,會活得更久
基于這個理論,現代垃圾回收算法根據對象的存活時間將內存進行了分代,并對不同分代的內存采用不同的高效算法進行垃圾回收
### V8的內存分代
在V8中,將內存分為了新生代(new space)和老生代(old space)。它們特點如下:
* 新生代:對象的存活時間較短。新生對象或只經過一次垃圾回收的對象。
* 老生代:對象存活時間較長。經歷過一次或多次垃圾回收的對象。
### Stop The World (全停頓)
在介紹垃圾回收算法之前,我們先了解一下「全停頓」。
為避免應用邏輯與垃圾回收器看到的情況不一致,垃圾回收算法在執行時,需要停止應用邏輯。垃圾回收算法在執行前,需要將應用邏輯暫停,執行完垃圾回收后再執行應用邏輯,這種行為稱為 「全停頓」(Stop The World)。例如,如果一次GC需要50ms,應用邏輯就會暫停50ms。
### Scavenge 算法
Scavenge 算法的缺點是,它的算法機制決定了只能利用一半的內存空間。但是新生代中的對象生存周期短、存活對象少,進行對象復制的成本不是很高,因而非常適合這種場景。
新生代中的對象主要通過 Scavenge 算法進行垃圾回收。Scavenge 的具體實現,主要采用了Cheney算法。

Cheney算法采用復制的方式進行垃圾回收。它將堆內存一分為二,每一部分空間稱為 semispace。這兩個空間,只有一個空間處于使用中,另一個則處于閑置。使用中的 semispace 稱為 「From 空間」,閑置的 semispace 稱為 「To 空間」。
過程如下:
* 從 From 空間分配對象,若 semispace 被分配滿,則執行 Scavenge 算法進行垃圾回收。
* 檢查 From 空間的存活對象,若對象存活,則檢查對象是否符合晉升條件,若符合條件則晉升到老生代,否則將對象從 From 空間復制到 To 空間。
* 若對象不存活,則釋放不存活對象的空間。
* 完成復制后,將 From 空間與 To 空間進行角色翻轉(flip)。
### 對象晉升
1. 對象是否經歷過Scavenge回收。對象從 From 空間復制 To 空間時,會檢查對象的內存地址來判斷對象是否已經經過一次Scavenge回收。若經歷過,則將對象從 From 空間復制到老生代中;若沒有經歷,則復制到 To 空間。
2. To 空間的內存使用占比是否超過限制。當對象從From 空間復制到 To 空間時,若 To 空間使用超過 25%,則對象直接晉升到老生代中。設置為25%的比例的原因是,當完成 Scavenge 回收后,To 空間將翻轉成From 空間,繼續進行對象內存的分配。若占比過大,將影響后續內存分配。
對象晉升到老生代后,將接受新的垃圾回收算法處理。下圖為Scavenge算法中,對象晉升流程圖。

### Mark-Sweep & Mark-Compact
老生代中的對象有兩個特點,第一是存活對象多,第二個存活時間長。若在老生代中使用 Scavenge 算法進行垃圾回收,將會導致復制存活對象的效率不高,且還會浪費一半的空間。因而,V8在老生代采用Mark-Sweep 和 Mark-Compact 算法進行垃圾回收。
Mark-Sweep,是標記清除的意思。它主要分為標記和清除兩個階段。
* 標記階段,它將遍歷堆中所有對象,并對存活的對象進行標記;
* 清除階段,對未標記對象的空間進行回收。
與 Scavenge 算法不同,Mark-Sweep 不會對內存一分為二,因此不會浪費空間。但是,經歷過一次 Mark-Sweep 之后,內存的空間將會變得不連續,這樣會對后續內存分配造成問題。比如,當需要分配一個比較大的對象時,沒有任何一個碎片內支持分配,這將提前觸發一次垃圾回收,盡管這次垃圾回收是沒有必要的。

為了解決內存碎片的問題,提高對內存的利用,引入了 Mark-Compact (標記整理)算法。Mark-Compact 是在 Mark-Sweep 算法上進行了改進,標記階段與Mark-Sweep相同,但是對未標記的對象處理方式不同。與Mark-Sweep是對未標記的對象立即進行回收,Mark-Compact則是將存活的對象移動到一邊,然后再清理端邊界外的內存。

由于Mark-Compact需要移動對象,所以執行速度上,比Mark-Sweep要慢。所以,V8主要使用Mark-Sweep算法,然后在當空間內存分配不足時,采用Mark-Compact算法。
### Incremental Marking(增量標記)
在新生代中,由于存活對象少,垃圾回收效率高,全停頓時間短,造成的影響小。但是老生代中,存活對象多,垃圾回收時間長,全停頓造成的影響大。為了減少全停頓的時間,V8對標記進行了優化,將一次停頓進行的標記過程,分成了很多小步。每執行完一小步就讓應用邏輯執行一會兒,這樣交替多次后完成標記。如下圖所示:

長時間的GC,會導致應用暫停和無響應,將會導致糟糕的用戶體驗。從2011年起,v8就將「全暫停」標記換成了增量標記。改進后的標記方式,最大停頓時間減少到原來的1/6。
### lazy sweeping(延遲清理)
* 發生在增量標記之后
* 堆確切地知道有多少空間能被釋放
* 延遲清理是被允許的,因此頁面的清理可以根據需要進行清理
* 當延遲清理完成后,增量標記將重新開始
## 內存泄露
### 引起內存泄漏的幾個禁忌
* 濫用全局變量:直接用全局變量賦值,在函數中濫用this指向全局對象
* 不銷毀定時器和回調
* DOM引用不規范,很多時候, 我們對 Dom 的操作, 會把 Dom 的引用保存在一個數組或者 Map 中,往往無法對其進行內存回收,ES6中引入 WeakSet 和 WeakMap 兩個新的概念, 來解決引用造成的內存回收問題. WeakSet 和 WeakMap 對于值的引用可以忽略不計, 他們對于值的引用是弱引用,內存回收機制, 不會考慮這種引用. 當其他引用被消除后, 引用就會從內存中被釋放.
* 濫用閉包:
~~~
// 濫用閉包引起內存泄漏
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // 對于 'originalThing'的引用
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);
~~~
### 查看內存泄漏
* 打開開發者工具,選擇 Timeline 面板
* 在頂部的Capture字段里面勾選 Memory
* 點擊左上角的錄制按鈕。
* 在頁面上進行各種操作,模擬用戶的使用情況。
* 一段時間后,點擊對話框的 stop 按鈕,面板上就會顯示這段時間的內存占用情況。
* * *
參考:
[深入淺出Node.js](https://book.douban.com/subject/25768396/)
[MDN內存管理](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management)
* * *
## 公眾號
想要實時關注筆者最新的文章和最新的文檔更新請關注公眾號**程序員面試官**,后續的文章會優先在公眾號更新.
**簡歷模板**:關注公眾號回復「模板」獲取
**《前端面試手冊》**:配套于本指南的突擊手冊,關注公眾號回復「fed」獲取

- 前言
- 指南使用手冊
- 為什么會有這個項目
- 面試技巧
- 面試官到底想看什么樣的簡歷?
- 面試回答問題的技巧
- 如何通過HR面
- 推薦
- 書籍/課程推薦
- 前端基礎
- HTML基礎
- CSS基礎
- JavaScript基礎
- 瀏覽器與新技術
- DOM
- 前端基礎筆試
- HTTP筆試部分
- JavaScript筆試部分
- 前端原理詳解
- JavaScript的『預解釋』與『變量提升』
- Event Loop詳解
- 實現不可變數據
- JavaScript內存管理
- 實現深克隆
- 如何實現一個Event
- JavaScript的運行機制
- 計算機基礎
- HTTP協議
- TCP面試題
- 進程與線程
- 數據結構與算法
- 算法面試題
- 字符串類面試題
- 前端框架
- 關于前端框架的面試須知
- Vue面試題
- React面試題
- 框架原理詳解
- 虛擬DOM原理
- Proxy比defineproperty優劣對比?
- setState到底是異步的還是同步的?
- 前端路由的實現
- redux原理全解
- React Fiber 架構解析
- React組件復用指南
- React-hooks 抽象組件
- 框架實戰技巧
- 如何搭建一個組件庫的開發環境
- 組件設計原則
- 實現輪播圖組件
- 性能優化
- 前端性能優化-加載篇
- 前端性能優化-執行篇
- 工程化
- webpack面試題
- 前端工程化
- Vite
- 安全
- 前端安全面試題
- npm
- 工程化原理
- 如何寫一個babel
- Webpack HMR 原理解析
- webpack插件編寫
- webpack 插件化設計
- Webpack 模塊機制
- webpack loader實現
- 如何開發Babel插件
- git
- 比較
- 查看遠程倉庫地址
- git flow
- 比較分支的不同并保存壓縮文件
- Tag
- 回退
- 前端項目經驗
- 確定用戶是否在當前頁面
- 前端下載文件
- 只能在微信中訪問
- 打開新頁面-被瀏覽器攔截
- textarea高度隨內容變化 vue版
- 去掉ios原始播放大按鈕
- nginx在MAC上的安裝、啟動、重啟和關閉
- 解析latex格式的數學公式
- 正則-格式化a鏈接
- 封裝的JQ插件庫
- 打包問題總結
- NPM UI插件
- 帶你入門前端工程
- webWorker+indexedDB性能優化
- 多個相鄰元素切換效果出現邊框重疊問題的解決方法
- 監聽前端storage變化