【進階1-4期】JavaScript深入之帶你走進記憶體機制
本期的主題是 呼叫堆疊 ,本計劃一共28期, 每期重點攻克一個面試重難點 ,如果你還不瞭解本進階計劃,文末點選檢視全部文章。
如果覺得本系列不錯,歡迎點贊、評論、轉發,您的支援就是我堅持的最大動力。
JS記憶體空間分為 棧(stack) 、 堆(heap) 、 池(一般也會歸類為棧中) 。 其中 棧 存放變數, 堆 存放複雜物件, 池 存放常量,所以也叫常量池。
昨天文章介紹了堆和棧,小結一下:
棧 堆
今日補充一個知識點,就是閉包中的變數並不儲存中棧記憶體中,而是儲存在 堆記憶體
中,這也就解釋了函式之後之後為什麼閉包還能引用到函式內的變數。
function A() { let a = 1 function B() { console.log(a) } return B } 複製程式碼
閉包
的簡單定義是:函式 A 返回了一個函式 B,並且函式 B 中使用了函式 A 的變數,函式 B 就被稱為閉包。
函式 A 彈出呼叫棧後,函式 A 中的變數這時候是儲存在堆上的,所以函式B依舊能引用到函式A中的變數。現在的 JS 引擎可以通過逃逸分析辨別出哪些變數需要儲存在堆上,哪些需要儲存在棧上。
閉包的介紹點到為止,會詳細介紹,敬請期待。
今天文章的重點是 記憶體回收 和 記憶體洩漏 。
記憶體回收
JavaScript有自動垃圾收集機制,垃圾收集器會每隔一段時間就執行一次釋放操作,找出那些不再繼續使用的值,然後釋放其佔用的記憶體。
- 區域性變數和全域性變數的銷燬
- 區域性變數 :區域性作用域中,當函式執行完畢,區域性變數也就沒有存在的必要了,因此垃圾收集器很容易做出判斷並回收。
- 全域性變數 :全域性變數什麼時候需要自動釋放記憶體空間則很難判斷,所以在開發中儘量 避免 使用全域性變數。
- 以Google的V8引擎為例,V8引擎中所有的JS物件都是通過 堆 來進行記憶體分配的
- 初始分配 :當宣告變數並賦值時,V8引擎就會在堆記憶體中分配給這個變數。
- 繼續申請 :當已申請的記憶體不足以儲存這個變數時,V8引擎就會繼續申請記憶體,直到堆的大小達到了V8引擎的記憶體上限為止。
- V8引擎對堆記憶體中的JS物件進行 分代管理
- 新生代 :存活週期較短的JS物件,如臨時變數、字串等。
- 老生代 :經過多次垃圾回收仍然存活,存活週期較長的物件,如主控制器、伺服器物件等。
垃圾回收演算法
對垃圾回收演算法來說,核心思想就是如何判斷記憶體已經不再使用,常用垃圾回收演算法有下面兩種。
- 引用計數(現代瀏覽器不再使用)
- 標記清除(常用)
引用計數
引用計數演算法定義“記憶體不再使用”的標準很簡單,就是看一個物件是否有指向它的 引用 。如果沒有其他物件指向它了,說明該物件已經不再需要了。
// 建立一個物件person,他有兩個指向屬性age和name的引用 var person = { age: 12, name: 'aaaa' }; person.name = null; // 雖然name設定為null,但因為person物件還有指向name的引用,因此name不會回收 var p = person; person = 1;//原來的person物件被賦值為1,但因為有新引用p指向原person物件,因此它不會被回收 p = null;//原person物件已經沒有引用,很快會被回收 複製程式碼
引用計數有一個致命的問題,那就是 迴圈引用
如果兩個物件相互引用,儘管他們已不再使用,但是垃圾回收器不會進行回收,最終可能會導致記憶體洩露。
function cycle() { var o1 = {}; var o2 = {}; o1.a = o2; o2.a = o1; return "cycle reference!" } cycle(); 複製程式碼
cycle
函式執行完成之後,物件 o1
和 o2
實際上已經不再需要了,但根據引用計數的原則,他們之間的相互引用依然存在,因此這部分記憶體不會被回收。所以現代瀏覽器 不再使用 這個演算法。
但是IE依舊使用。
var div = document.createElement("div"); div.onclick = function() { console.log("click"); }; 複製程式碼
上面的寫法很常見,但是上面的例子就是一個迴圈引用。
變數div有事件處理函式的引用,同時事件處理函式也有div的引用,因為div變數可在函式內被訪問,所以迴圈引用就出現了。
標記清除(常用)
標記清除演算法將“不再使用的物件”定義為“ 無法到達的物件 ”。即從根部(在JS中就是全域性物件)出發定時掃描記憶體中的物件,凡是能從根部到達的物件, 保留 。那些從根部出發無法觸及到的物件被標記為 不再使用 ,稍後進行回收。
無法觸及的物件包含了沒有引用的物件這個概念,但反之未必成立。
所以上面的例子就可以正確被垃圾回收處理了。
所以現在對於主流瀏覽器來說,只需要切斷需要回收的物件與根部的聯絡。最常見的記憶體洩露一般都與DOM元素繫結有關:
email.message = document.createElement(“div”); displayList.appendChild(email.message); // 稍後從displayList中清除DOM元素 displayList.removeAllChildren(); 複製程式碼
上面程式碼中, div
元素已經從DOM樹中清除,但是該 div
元素還繫結在email物件中,所以如果email物件存在,那麼該 div
元素就會一直儲存在記憶體中。
記憶體洩漏
對於持續執行的服務程序(daemon),必須及時釋放不再用到的記憶體。否則,記憶體佔用越來越高,輕則影響系統性能,重則導致程序崩潰。 對於不再用到的記憶體,沒有及時釋放,就叫做記憶體洩漏(memory leak)
記憶體洩漏識別方法
1、瀏覽器方法
- 開啟開發者工具,選擇 Memory
- 在右側的Select profiling type欄位裡面勾選 timeline
- 點選左上角的錄製按鈕。
- 在頁面上進行各種操作,模擬使用者的使用情況。
- 一段時間後,點選左上角的 stop 按鈕,面板上就會顯示這段時間的記憶體佔用情況。
2、命令列方法
使用 Node
提供的 process.memoryUsage
方法。
console.log(process.memoryUsage()); // 輸出 { rss: 27709440,// resident set size,所有記憶體佔用,包括指令區和堆疊 heapTotal: 5685248,// "堆"佔用的記憶體,包括用到的和沒用到的 heapUsed: 3449392,// 用到的堆的部分 external: 8772 // V8 引擎內部的 C++ 物件佔用的記憶體 } 複製程式碼
判斷記憶體洩漏,以 heapUsed
欄位為準。
詳細的JS記憶體分析將在詳細介紹,敬請期待。
WeakMap
ES6 新出的兩種資料結構: WeakSet
和 WeakMap
,表示這是弱引用,它們對於值的引用都是不計入垃圾回收機制的。
const wm = new WeakMap(); const element = document.getElementById('example'); wm.set(element, 'some information'); wm.get(element) // "some information" 複製程式碼
先新建一個 Weakmap
例項,然後將一個 DOM 節點作為鍵名存入該例項,並將一些附加資訊作為鍵值,一起存放在 WeakMap
裡面。這時, WeakMap
裡面對element的引用就是弱引用,不會被計入垃圾回收機制。
昨日思考題解答
昨天文章留了一道思考題,群裡討論很熱烈,大家應該都知道原理了,現在來簡單解答下。
var a = {n: 1}; var b = a; a.x = a = {n: 2}; a.x // --> undefined b.x // --> {n: 2} 複製程式碼
答案已經寫上面了,這道題的關鍵在於
- 1、優先順序。
.
的優先順序高於=
,所以先執行a.x
,堆記憶體中的{n: 1}
就會變成{n: 1, x: undefined}
,改變之後相應的b.x
也變化了,因為指向的是同一個物件。 - 2、賦值操作是
從右到左
,所以先執行a = {n: 2}
,a
的引用就被改變了,然後這個返回值又賦值給了a.x
, 需要注意 的是這時候a.x
是第一步中的{n: 1, x: undefined}
那個物件,其實就是b.x
,相當於b.x = {n: 2}

今日份思考題
問題一:
從記憶體來看 null 和 undefined 本質的區別是什麼?
問題二:
ES6語法中的 const 宣告一個只讀的常量,那為什麼下面可以修改const的值?
const foo = {}; foo = {}; // TypeError: "foo" is read-only foo.prop = 123; foo.prop // 123 複製程式碼
問題三:
哪些情況下容易產生記憶體洩漏?
參考
ofollow,noindex">JavaScript 記憶體機制
Operators%2FOperator_Precedence" rel="nofollow,noindex">MDN之運算子優先順序
由ES規範學JavaScript(二):深入理解“連等賦值”問題
進階系列目錄
- 【進階1期】 呼叫堆疊
- 【進階2期】 作用域閉包
- 【進階3期】 this全面解析
- 【進階4期】 深淺拷貝原理
- 【進階5期】 原型Prototype
- 【進階6期】 高階函式
- 【進階7期】 事件機制
- 【進階8期】 Event Loop原理
- 【進階9期】 Promise原理
- 【進階10期】Async/Await原理
- 【進階11期】防抖/節流原理
- 【進階12期】模組化詳解
- 【進階13期】ES6重難點
- 【進階14期】計算機網路概述
- 【進階15期】瀏覽器渲染原理
- 【進階16期】webpack配置
- 【進階17期】webpack原理
- 【進階18期】前端監控
- 【進階19期】跨域和安全
- 【進階20期】效能優化
- 【進階21期】VirtualDom原理
- 【進階22期】Diff演算法
- 【進階23期】MVVM雙向繫結
- 【進階24期】Vuex原理
- 【進階25期】Redux原理
- 【進階26期】路由原理
- 【進階27期】VueRouter原始碼解析
- 【進階28期】ReactRouter原始碼解析
交流
進階系列文章彙總: github.com/yygmind/blo… ,內有優質前端資料,歡迎領取,覺得不錯點個star。
我是木易楊,網易高階前端工程師,跟著我 每週重點攻克一個前端面試重難點 。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!
