本期的主題是呼叫堆疊,本計劃一共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 引擎可以通過逃逸分析辨別出哪些變數需要儲存在堆上,哪些需要儲存在棧上。
閉包的介紹點到為止,【進階2期】 作用域閉包會詳細介紹,敬請期待。
今天文章的重點是記憶體回收和記憶體洩漏。
記憶體回收
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記憶體分析將在【進階20期】效能優化詳細介紹,敬請期待。
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
複製程式碼
問題三:
哪些情況下容易產生記憶體洩漏?
參考
進階系列目錄
- 【進階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。
我是木易楊,網易高階前端工程師,跟著我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高階前端的世界,在進階的路上,共勉!