[TOC]
# 簡介
從本質上說,內存泄漏可以定義為:不再被應用程序所需要的內存,出于某種原因,它不會返回到操作系統或空閑內存池中。
<br>
# 四種常見的內存泄漏
## 全局變量
JavaScript以一種有趣的方式處理未聲明的變量: 對于未聲明的變量,會在全局范圍中創建一個新的變量來對其進行引用。在瀏覽器中,全局對象是window。例如:
~~~
function foo(arg) {
bar = "some text";
}
~~~
等價于:
~~~
function foo(arg) {
window.bar = "some text";
}
~~~
如果 bar 在 foo 函數的作用域內對一個變量進行引用,卻忘記使用 var 來聲明它,那么將創建一個意想不到的全局變量。在這個例子中,遺漏一個簡單的字符串不會造成太大的危害,但這肯定會很糟。
<br>
創建一個意料之外的全局變量的另一種方法是使用this:
~~~
function foo() {
this.var1 = "potential accidental global";
}
// Foo自己調用,它指向全局對象(window),而不是未定義。
foo();
~~~
<br>
> 可以在JavaScript文件的開頭通過添加“use strict”來避免這一切,它將開啟一個更嚴格的JavaScript解析模式,以防止意外創建全局變量。
<br>
盡管我們討論的是未知的全局變量,但仍然有很多代碼充斥著顯式的全局變量。根據定義,這些是不可收集的(除非被指定為空或重新分配)。用于臨時存儲和處理大量信息的全局變量特別令人擔憂。如果你必須使用一個全局變量來存儲大量數據,那么請確保將其指定為null,或者在完成后將其重新賦值。
<br>
## 被遺忘的定時器和回調
以`setInterval`為例,因為它在JavaScript中經常使用。
~~~
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //每五秒會執行一次
~~~
<br>
上面的代碼片段演示了使用定時器時引用不再需要的節點或數據。
<br>
renderer 表示的對象可能會在未來的某個時間點被刪除,從而導致內部處理程序中的一整塊代碼都變得不再需要。但是,由于定時器仍然是活動的,所以,處理程序不能被收集,并且其依賴項也無法被收集。這意味著,存儲著大量數據的 serverData 也不能被收集。
<br>
在使用觀察者時,您需要確保在使用完它們之后進行顯式調用來刪除它們(要么不再需要觀察者,要么對象將變得不可訪問)。
<br>
作為開發者時,需要確保在完成它們之后進行顯式刪除它們(或者對象將無法訪問)。
<br>
在過去,一些瀏覽器無法處理這些情況(很好的IE6)。幸運的是,現在大多數現代瀏覽器會為幫你完成這項工作:一旦觀察到的對象變得不可訪問,即使忘記刪除偵聽器,它們也會自動收集觀察者處理程序。然而,我們還是應該在對象被處理之前顯式地刪除這些觀察者。例如:

<br>
如今,現在的瀏覽器(包括IE和Edge)使用現代的垃圾回收算法,可以立即發現并處理這些循環引用。換句話說,在一個節點刪除之前也不是必須要調用removeEventListener。
<br>
一些框架或庫,比如JQuery,會在處置節點之前自動刪除監聽器(在使用它們特定的API的時候)。這是由庫內部的機制實現的,能夠確保不發生內存泄漏,即使在有問題的瀏覽器下運行也能這樣,比如IE 6。
<br>
## 閉包
閉包是javascript開發的一個關鍵方面,一個內部函數使用了外部(封閉)函數的變量。由于JavaScript運行的細節,它可能以下面的方式造成內存泄漏:

<br>
這段代碼做了一件事:每次調用`replaceThing`的時候,`theThing`都會得到一個包含一個大數組和一個新閉包(someMethod)的新對象。同時,變量`unuse`d指向一個引用了``originalThing`的閉包。
<br>
是不是有點困惑了? 重要的是,一旦具有相同父作用域的多個閉包的作用域被創建,則這個作用域就可以被共享。
<br>
在這種情況下,為閉包`someMethod`而創建的作用域可以被`unused`共享的。`unused`內部存在一個對`originalThing`的引用。即使`unused`從未使用過,`someMethod`也可以在`replaceThing`的作用域之外(例如在全局范圍內)通過`theThing`來被調用。
<br>
由于`someMethod`共享了`unused`閉包的作用域,那么`unused`引用包含的`originalThing`會迫使它保持活動狀態(兩個閉包之間的整個共享作用域)。這阻止了它被收集。
<br>
當這段代碼重復運行時,可以觀察到內存使用在穩定增長,當`GC`運行后,內存使用也不會變小。從本質上說,在運行過程中創建了一個閉包鏈表(它的根是以變量`theThing`的形式存在),并且每個閉包的作用域都間接引用了了一個大數組,這造成了相當大的內存泄漏。
<br>
## 脫離DOM的引用
有時,將DOM節點存儲在數據結構中可能會很有用。假設你希望快速地更新表中的幾行內容,那么你可以在一個字典或數組中保存每個DOM行的引用。這樣,同一個DOM元素就存在兩個引用:一個在DOM樹中,另一個則在字典中。如果在將來的某個時候你決定刪除這些行,那么你需要將這兩個引用都設置為不可訪問。

<br>
在引用 DOM 樹中的內部節點或葉節點時,還需要考慮另外一個問題。如果在代碼中保留對表單元格的引用(標記),并決定從 DOM 中刪除表,同時保留對該特定單元格的引用,那么可能會出現內存泄漏。
<br>
你可能認為垃圾收集器將釋放除該單元格之外的所有內容。然而,事實并非如此,由于單元格是表的一個子節點,而子節點保存對父節點的引用,所以對表單元格的這個引用將使**整個表保持在內存中**,所以在移除有被引用的節點時候要移除其子節點。
<br>
<br>
# 內存泄漏的識別方法
怎樣可以觀察到內存泄漏呢?
[經驗法則](https://www.toptal.com/nodejs/debugging-memory-leaks-node-js-applications)是,如果連續五次垃圾回收之后,內存占用一次比一次大,就有內存泄漏。這就要求實時查看內存占用。
<br>
## 瀏覽器
Chrome 瀏覽器查看內存占用,按照以下步驟操作
1. 打開開發者工具,選擇 Performance 面板
2. 勾選 Memory
3. 點擊左上角的錄制按鈕。
4. 在頁面上進行各種操作,模擬用戶的使用情況。
5. 一段時間后,點擊對話框的 stop 按鈕,面板上就會顯示這段時間的內存占用情況。
<br>
如果內存占用基本平穩,接近水平,就說明不存在內存泄漏。
<br>

<br>
反之,就是內存泄漏了。
<br>

<br>
## 命令行
命令行可以使用 Node 提供的[`process.memoryUsage`](https://nodejs.org/api/process.html#process_process_memoryusage)方法。
~~~javascript
console.log(process.memoryUsage());
// { rss: 27709440,
// heapTotal: 5685248,
// heapUsed: 3449392,
// external: 8772 }
~~~
<br>
`process.memoryUsage`返回一個對象,包含了 Node 進程的內存占用信息。該對象包含四個字段,單位是字節,[含義](http://stackoverflow.com/questions/12023359/what-do-the-return-values-of-node-js-process-memoryusage-stand-for)如下。

<br>
* rss(resident set size):所有內存占用,包括指令區和堆棧。
* heapTotal:"堆"占用的內存,包括用到的和沒用到的。
* heapUsed:用到的堆的部分。
* external: V8 引擎內部的 C++ 對象占用的內存。
<br>
判斷內存泄漏,以heapUsed字段為準。
<br>
<br>
# WeakMap
前面說過,及時清除引用非常重要。但是,你不可能記得那么多,有時候一疏忽就忘了,所以才有那么多內存泄漏。
<br>
最好能有一種方法,在新建引用的時候就聲明,哪些引用必須手動清除,哪些引用可以忽略不計,當其他引用消失以后,垃圾回收機制就可以釋放內存。這樣就能大大減輕程序員的負擔,你只要清除主要引用就可以了。
<br>
ES6 考慮到了這一點,推出了兩種新的數據結構:[WeakSet](http://es6.ruanyifeng.com/#docs/set-map#WeakSet)和[WeakMap](http://es6.ruanyifeng.com/#docs/set-map#WeakMap)。它們對于值的引用都是不計入垃圾回收機制的,所以名字里面才會有一個"Weak",表示這是弱引用。
<br>
下面以 WeakMap 為例,看看它是怎么解決內存泄漏的。
~~~
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
~~~
<br>
上面代碼中,先新建一個 Weakmap 實例。然后,將一個 DOM 節點作為鍵名存入該實例,并將一些附加信息作為鍵值,一起存放在 WeakMap 里面。這時,WeakMap 里面對`element`的引用就是弱引用,不會被計入垃圾回收機制。
<br>
也就是說,DOM 節點對象的引用計數是`1`,而不是`2`。這時,一旦消除對該節點的引用,它占用的內存就會被垃圾回收機制釋放。Weakmap 保存的這個鍵值對,也會自動消失。
<br>
基本上,如果你要往對象上添加數據,又不想干擾垃圾回收機制,就可以使用 WeakMap。
<br>
<br>
## WeakMap 示例
WeakMap 的例子很難演示,因為無法觀察它里面的引用會自動消失。此時,其他引用都解除了,已經沒有引用指向 WeakMap 的鍵名了,導致無法證實那個鍵名是不是存在。
<br>
首先,打開 Node 命令行。
~~~
node --expose-gc
~~~
<br>
上面代碼中,`--expose-gc`參數表示允許手動執行垃圾回收機制。
<br>
然后,執行下面的代碼。
~~~
// 手動執行一次垃圾回收,保證獲取的內存使用狀態準確
> global.gc();
undefined
// 查看內存占用的初始狀態,heapUsed 為 4M 左右
> process.memoryUsage();
{ rss: 21106688,
heapTotal: 7376896,
heapUsed: 4153936,
external: 9059 }
> let wm = new WeakMap();
undefined
> let b = new Object();
undefined
> global.gc();
undefined
// 此時,heapUsed 仍然為 4M 左右
> process.memoryUsage();
{ rss: 20537344,
heapTotal: 9474048,
heapUsed: 3967272,
external: 8993 }
// 在 WeakMap 中添加一個鍵值對,
// 鍵名為對象 b,鍵值為一個 5*1024*1024 的數組
> wm.set(b, new Array(5*1024*1024));
WeakMap {}
// 手動執行一次垃圾回收
> global.gc();
undefined
// 此時,heapUsed 為 45M 左右
> process.memoryUsage();
{ rss: 62652416,
heapTotal: 51437568,
heapUsed: 45911664,
external: 8951 }
// 解除對象 b 的引用
> b = null;
null
// 再次執行垃圾回收
> global.gc();
undefined
// 解除 b 的引用以后,heapUsed 變回 4M 左右
// 說明 WeakMap 中的那個長度為 5*1024*1024 的數組被銷毀了
> process.memoryUsage();
{ rss: 20639744,
heapTotal: 8425472,
heapUsed: 3979792,
external: 8956 }
~~~
<br>
上面代碼中,只要外部的引用消失,WeakMap 內部的引用,就會自動被垃圾回收清除。由此可見,有了它的幫助,解決內存泄漏就會簡單很多。
<br>
<br>
# 參考資料
[JavaScript如何工作:內存管理+如何處理4個常見的內存泄漏](https://segmentfault.com/a/1190000017392370)
[JavaScript 內存泄漏教程](http://www.ruanyifeng.com/blog/2017/04/memory-leak.html)
- 第一部分 HTML
- meta
- meta標簽
- HTML5
- 2.1 語義
- 2.2 通信
- 2.3 離線&存儲
- 2.4 多媒體
- 2.5 3D,圖像&效果
- 2.6 性能&集成
- 2.7 設備訪問
- SEO
- Canvas
- 壓縮圖片
- 制作圓角矩形
- 全局屬性
- 第二部分 CSS
- CSS原理
- 層疊上下文(stacking context)
- 外邊距合并
- 塊狀格式化上下文(BFC)
- 盒模型
- important
- 樣式繼承
- 層疊
- 屬性值處理流程
- 分辨率
- 視口
- CSS API
- grid(未完成)
- flex
- 選擇器
- 3D
- Matrix
- AT規則
- line-height 和 vertical-align
- CSS技術
- 居中
- 響應式布局
- 兼容性
- 移動端適配方案
- CSS應用
- CSS Modules(未完成)
- 分層
- 面向對象CSS(未完成)
- 布局
- 三列布局
- 單列等寬,其他多列自適應均勻
- 多列等高
- 圣杯布局
- 雙飛翼布局
- 瀑布流
- 1px問題
- 適配iPhoneX
- 橫屏適配
- 圖片模糊問題
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 內存空間
- 作用域
- 執行上下文棧
- 變量對象
- 作用域鏈
- this
- 類型轉換
- 閉包(未完成)
- 原型、面向對象
- class和extend
- 繼承
- new
- DOM
- Event Loop
- 垃圾回收機制
- 內存泄漏
- 數值存儲
- 連等賦值
- 基本類型
- 堆棧溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍歷對象屬性
- 寬度、高度
- performance
- 位運算
- tostring( ) 與 valueOf( )方法
- JavaScript技術
- 錯誤
- 異常處理
- 存儲
- Cookie與Session
- ES6(未完成)
- Babel轉碼
- let和const命令
- 變量的解構賦值
- 字符串的擴展
- 正則的擴展
- 數值的擴展
- 數組的擴展
- 函數的擴展
- 對象的擴展
- Symbol
- Set 和 Map 數據結構
- proxy
- Reflect
- module
- AJAX
- ES5
- 嚴格模式
- JSON
- 數組方法
- 對象方法
- 函數方法
- 服務端推送(未完成)
- JavaScript應用
- 復雜判斷
- 3D 全景圖
- 重載
- 上傳(未完成)
- 上傳方式
- 文件格式
- 渲染大量數據
- 圖片裁剪
- 斐波那契數列
- 編碼
- 數組去重
- 淺拷貝、深拷貝
- instanceof
- 模擬 new
- 防抖
- 節流
- 數組扁平化
- sleep函數
- 模擬bind
- 柯里化
- 零碎知識點
- 第四部分 進階
- 計算機原理
- 數據結構(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 選擇排序
- 插入排序
- 快速排序
- 搜索算法
- 動態規劃
- 二叉樹
- 瀏覽器
- 瀏覽器結構
- 瀏覽器工作原理
- HTML解析
- CSS解析
- 渲染樹構建
- 布局(Layout)
- 渲染
- 瀏覽器輸入 URL 后發生了什么
- 跨域
- 緩存機制
- reflow(回流)和repaint(重繪)
- 渲染層合并
- 編譯(未完成)
- Babel
- 設計模式(未完成)
- 函數式編程(未完成)
- 正則表達式(未完成)
- 性能
- 性能分析
- 性能指標
- 首屏加載
- 優化
- 瀏覽器層面
- HTTP層面
- 代碼層面
- 構建層面
- 移動端首屏優化
- 服務器層面
- bigpipe
- 構建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack優化
- Webpack原理
- 實現loader
- 實現plugin
- tapable
- Webpack打包后代碼
- rollup.js
- parcel
- 模塊化
- ESM
- 安全
- XSS
- CSRF
- 點擊劫持
- 中間人攻擊
- 密碼存儲
- 測試(未完成)
- 單元測試
- E2E測試
- 框架測試
- 樣式回歸測試
- 異步測試
- 自動化測試
- PWA
- PWA官網
- web app manifest
- service worker
- app install banners
- 調試PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 餓了么整理
- 樣式
- 技巧
- Vue音樂播放器
- Vue源碼
- Virtual Dom
- computed原理
- 數組綁定原理
- 雙向綁定
- nextTick
- keep-alive
- 導航守衛
- 組件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 動畫(未完成)
- 異常監控、收集(未完成)
- 數據采集
- Sentry
- 貝塞爾曲線
- 視頻
- 服務端渲染
- 服務端渲染的利與弊
- Vue SSR
- React SSR
- 客戶端
- 離線包
- 第五部分 網絡
- 五層協議
- TCP
- UDP
- HTTP
- 方法
- 首部
- 狀態碼
- 持久連接
- TLS
- content-type
- Redirect
- CSP
- 請求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服務端
- Linux
- Linux命令
- 權限
- XAMPP
- Node.js
- 安裝
- Node模塊化
- 設置環境變量
- Node的event loop
- 進程
- 全局對象
- 異步IO與事件驅動
- 文件系統
- Node錯誤處理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服務
- 負載均衡
- 獲取用戶IP
- 解決跨域
- 適配PC與移動環境
- 簡單的訪問限制
- 頁面內容修改
- 圖片處理
- 合并請求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自動化(未完成)
- docker
- 創建CLI
- 持續集成
- 持續交付
- 持續部署
- Jenkins
- 部署與發布
- 遠程登錄服務器
- 增強服務器安全等級
- 搭建 Nodejs 生產環境
- 配置 Nginx 實現反向代理
- 管理域名解析
- 配置 PM2 一鍵部署
- 發布上線
- 部署HTTPS
- Node 應用
- 爬蟲(未完成)
- 例子
- 反爬蟲
- 中間件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源碼
- app.js
- config.js
- 消息隊列
- RPC
- 性能優化
- 第七部分 總結
- Web服務器
- 目錄結構
- 依賴
- 功能
- 代碼片段
- 整理
- 知識清單、博客
- 項目、組件、庫
- Node代碼
- 面試必考
- 91算法
- 第八部分 工作代碼總結
- 樣式代碼
- 框架代碼
- 組件代碼
- 功能代碼
- 通用代碼