Javascript 內存空間管理

原文地址:github.com/ruizhengyun…javascript

咱們都知道 Javascript 具備自動垃圾回收機制。一聽到自動這個詞,多好啊,能幫咱們作點,咱們就能夠少作點事了。或許也部分是由於這個自動回收垃圾的機制,不少的前端小夥伴就減小了和內存空間打交道的場合或機會,就容易忽視這些。還有就是,前端開發人員許多都不是計算機專業畢業的,對內存空間的認知就比較模糊了,有的乾脆就是一無所知。剛入行(前端)幾年的我(一個文科生),就是一無所知的菜鳥,如今好多了,用我對象的話說搶了她(計算機專業畢業的)的飯碗。其實嘛,不知道不要緊,能夠學,那些計算機專業的小夥伴不也是從上大學開始學的嘛,咱只不過晚了點,如此而已。前端

基礎知識很重要,別嫌我囉嗦,再強調一次。固然了,我確定不是第一個,也不是最後一個這麼囉嗦的。接下來的場景面試,你確定深有體悟:日常在公司開發應用玩的很轉,一面試,彷彿就在裸奔,許多知識講的不完全,本身都以爲虛。這個現象的本質仍是在於基礎知識的不紮實,因此在面試的時候,你想講可是又不是很懂,或許你耍個聰明的去規避這些,可面試官也不傻(要是碰到傻得告訴我)。因此,請不要忽略基礎知識java

好了,接下來好好說說基礎知識之內存空間究竟是怎麼個玩法。這一次,必定要整個明白(給本身立個 Flag)。node

先理解三種數據結構

它們分別是堆(heap)、棧(stack)、隊列(queue),一圖知全部。git

Javascript 沒有像 C/C++ 嚴格意義上區分堆內存和棧內存。咱們能夠簡單粗暴一刀切(就這個意思了)的理解 Javascript 數據存在堆內存中。可是,重點來了,有些場景仍是須要棧數據結構的思惟來理解的,好比執行上下文,存放變量。github

從上圖一眼可看出其特色 後進先出(LIFO)(走後門啊這是),這就是棧的存儲原理,一個有後臺的數據結構面試

這裏有個的概念,即存放常量,因此也叫常量池。算法

一種樹狀結構。比如 JSON 格式中的數據,你有 key,我有對應的 value, 就立馬返給你。一個絕對公平的數據結構編程

只要你要,只要我有。數組

隊列

Javascript 中,隊列數據結構的應用主要體如今事件循環機制(eventLoop) 上。其特色很直白就是先進先出(FIFO),和棧結構正好相反,一個相對公平的數據結構

先到先得

數據類型與數據結構

Javascript 執行上下文後,會建立一個叫變量對象的特殊對象,其實這個對象也存在堆內存中,但因爲特殊,要與堆內存區分。

基礎數據類型

基礎數據類型都是一些簡單的數據段,包括 Undefined、Null、Boolean、Number、String 和 Symbol。保存在棧內存中,由於這些類型在內存中分別佔有固定大小的空間,經過按值訪問,因此可直接操做變量中的值,即尋值

引用數據類型

引用數據類型的值保存在堆內存的對象中。由於這類值的大小不固定,所以不能把它們保存到棧內存中,但內存地址大小的固定的,所以保存在堆內存中,在棧內存中存放的只是該對象的訪問地址。當查詢引用類型的變量時, 先從棧中讀取內存地址, 而後再經過地址找到堆中的值,即尋址

閉包中的變量並不保存中棧內存中,而是保存在堆內存中,這也就解釋了外層函數彈出調用棧後爲何閉包還能引用到函數內的變量。如今的 JS 引擎能夠經過逃逸分析辨別出哪些變量須要存儲在堆上,哪些須要存儲在棧上。

理解方式

var name = 'pr'; // 變量對象
var age = 30; // 變量對象

var info = { name: 'pr' }; // 變量 info 存在變量對象中,{ name: 'pr' } 做爲對象存在堆內存中
var relation = [1, 2, 3]; // 變量 relation 存在變量對象中,[1, 2, 3] 做爲對象存在堆內存中
複製代碼

所以當要訪問 inforelation 這些對內存的引用數據類型時,其實是從變量對象中獲取其地址引用,而後才從堆內存得到數據。

舉例子

問題1

var name = 'pr';
var nickName = name;
nickName = '一如既往如我';
console.log(name); // pr
複製代碼

問題2

var info = { name: 'pr' };
var nicInfo = info;
nicInfo.name = '一如既往如我';
console.log(info.name); // 一如既往如我
複製代碼

問題3

var info = { name: 'pr' };
var nicInfo = info;
nicInfo = null
console.log(info.name); // pr
複製代碼

上面3個例子,相信此次你應該明白了。在變量數據中發生了數據複製。基本數據類型和引用數據類型的複製過程圖可看下面

對於問題1,基礎類型複製過程

對於問題2,引用類型複製過程

對於問題3,null 是基本類型,因此同問題1,並不會影響堆內存中的對象,因此 info 不受影響,你學會了麼?

總結

在計算機的數據結構中,棧比堆的運算速度快,Object 是一個複雜的結構且能夠擴展:數組可擴充,對象可添加屬性,均可以增刪改查。將他們放在堆中是爲了避免影響棧的效率。而是經過引用的方式查找到堆中的實際對象再進行操做。因此查找引用類型值的時候先去棧查找再去堆查找,這點要熟記於心。

內存空間的管理

本文一開始就提到 Javascript 具備自動垃圾回收機制,因此前端開發就沒關注過內存的使用問題。可是,瞭解內存機制有助於知道編寫的代碼在執行過程當中發生了什麼,從而提升編程質量。

生命週期

  • 分配內存
  • 使用內存(讀寫)
  • 釋放內存

內存回收

1.局部變量和全局變量的銷燬

  • 局部變量:局部做用域中,當函數執行完畢,局部變量也就沒有存在的必要了,所以垃圾收集器很容易作出判斷並回收;
  • 全局變量:全局變量何時須要自動釋放內存空間則很難判斷,因此在開發中儘可能避免使用全局變量;

2.以 Google 的 V8 引擎爲例,V8 引擎中全部的 JS 對象都是經過堆來進行內存分配的

  • 初始分配:當聲明變量並賦值時,V8引擎就會在堆內存中分配給這個變量;
  • 繼續申請:當已申請的內存不足以存儲這個變量時,V8引擎就會繼續申請內存,直到堆的大小達到了V8引擎的內存上限爲止;

3.V8引擎對堆內存中的 JS 對象進行分代管理

  • 新生代:存活週期較短的 JS 對象,如臨時變量、字符串等;
  • 老生代:通過屢次垃圾回收仍然存活,存活週期較長的對象,如主控制器、服務器對象等;

垃圾回收算法

對垃圾回收算法來講,核心思想就是如何判斷內存已經再也不使用,經常使用垃圾回收算法有下面兩種。

  • 引用計數(現代瀏覽器再也不使用)
  • 標記清除(經常使用)

引用計數

引用計數算法定義「內存再也不使用」的標準很簡單,就是看一個對象是否有指向它的引用。若是沒有其餘對象指向它了,說明該對象已經再也不須要了。

let info = {
    name: 'pr',
    age: 30
};
let nickInfo = info;
info = '一如既往如你';
nickInfo = null;
複製代碼

引用計數有一個致命的問題,那就是循環引用

function cycleFn() {
    let o1 = {};
    let o2 = {};
    o1.obj = o2;
    o2.obj = o1;
}
cycleFn();
複製代碼

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

let createElementDiv = document.createElement("div");
createElementDiv.onclick = function() {
    console.log("點擊建立的元素 div", createElementDiv);
};
複製代碼

上面的例子就是一個循環引用。變量 createElementDiv 有事件處理函數的引用,同時事件處理函數也有 createElementDiv 的引用,由於 createElementDiv 變量可在函數內被訪問,因此循環引用就出現了。

標記清除

標記清除算法將「再也不使用的對象」定義爲「沒法到達的對象」。即從根部(在JS中就是全局對象)出發定時掃描內存中的對象,凡是能從根部到達的對象,保留。那些從根部出發沒法觸及到的對象被標記爲再也不使用,稍後進行回收。因此上面的例子就能夠正確被垃圾回收處理了。

var name = 'pr';
console.log(name);
name = null;
複製代碼

上面 name = null 作了釋放引用,脫離執行環境,這個值在下次垃圾收集器執行時釋放。因此,適當時候解除引用,是頁面性能提高的一個好的方式。

內存泄漏

對於持續運行的服務進程(daemon),必須及時釋放再也不用到的內存。不然,內存佔用愈來愈高,輕則影響系統性能,重則致使進程崩潰。 對於再也不用到的內存,沒有及時釋放,就叫作內存泄漏(memory leak)

一、瀏覽器方法

  • 打開開發者工具,選擇 Memory
  • 在右側的Select profiling type字段裏面勾選 timeline
  • 點擊左上角的錄製按鈕。
  • 在頁面上進行各類操做,模擬用戶的使用狀況。
  • 一段時間後,點擊左上角的 stop 按鈕,面板上就會顯示這段時間的內存佔用狀況。

二、命令行方法 使用 Node 提供的 process.memoryUsage 方法。

node
> process.memoryUsage()

// 輸出
{ rss: 23904256,
  heapTotal: 7331840,
  heapUsed: 5042520, // 用到的堆的部分,判斷內存泄漏,以這個字段爲準。
  external: 20601 
}
複製代碼
相關文章
相關標籤/搜索