【進階1-4期】JavaScript深刻之帶你走進內存機制

更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端

------ 如下是正文 ------webpack

本期的主題是調用堆棧,本計劃一共28期,每期重點攻克一個面試重難點,若是你還不瞭解本進階計劃,文末點擊查看所有文章。git

若是以爲本系列不錯,歡迎點贊、評論、轉發,您的支持就是我堅持的最大動力。github


JS內存空間分爲棧(stack)堆(heap)池(通常也會歸類爲棧中)。 其中存放變量,存放複雜對象,存放常量,因此也叫常量池。web

昨天文章介紹了堆和棧,小結一下:面試

  • 基本類型:--> 內存(不包含閉包中的變量)
  • 引用類型:--> 內存

今日補充一個知識點,就是閉包中的變量並不保存中棧內存中,而是保存在堆內存中,這也就解釋了函數以後以後爲何閉包還能引用到函數內的變量。算法

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}
複製代碼

閉包的簡單定義是:函數 A 返回了一個函數 B,而且函數 B 中使用了函數 A 的變量,函數 B 就被稱爲閉包。segmentfault

函數 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函數執行完成以後,對象o1o2實際上已經再也不須要了,但根據引用計數的原則,他們之間的相互引用依然存在,所以這部份內存不會被回收。因此現代瀏覽器再也不使用這個算法。

可是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
  2. 在右側的Select profiling type字段裏面勾選 timeline
  3. 點擊左上角的錄製按鈕。
  4. 在頁面上進行各類操做,模擬用戶的使用狀況。
  5. 一段時間後,點擊左上角的 stop 按鈕,面板上就會顯示這段時間的內存佔用狀況。
二、命令行方法

使用 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 新出的兩種數據結構:WeakSetWeakMap,表示這是弱引用,它們對於值的引用都是不計入垃圾回收機制的。

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}
複製代碼

答案已經寫上面了,這道題的關鍵在於

  • 一、優先級。.的優先級高於=,因此先執行a.x,堆內存中的{n: 1}就會變成{n: 1, x: undefined},改變以後相應的b.x也變化了,由於指向的是同一個對象。
  • 二、賦值操做是從右到左,因此先執行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
複製代碼

問題三

哪些狀況下容易產生內存泄漏?

參考

JavaScript 內存機制

MDN之運算符優先級

由ES規範學JavaScript(二):深刻理解「連等賦值」問題

InterviewMap

進階系列目錄

  • 【進階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。

我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!

相關文章
相關標籤/搜索