深刻JavaScript系列(五):JS與內存

1、內存是什麼

咱們如今經常使用的計算機都屬於 馮·諾依曼體系計算機, 計算機硬件由 控制器、運算器、存儲器、輸入設備、輸出設備 五大部分組成。git

咱們一般所說的內存就是 存儲器github

經常使用的內存都是易失性存儲器(須要經過不斷加電刷新來保持數據,一旦斷電就會致使數據丟失),因此須要一種容量大、低成本的非易失性存儲器來進行數據的存儲,這就是外存,例如磁帶、軟盤、硬盤、光盤、閃存卡、U盤等。能夠將外存理解爲輸入輸出設備,由於外存是須要經過I/O接口進行數據存取的,而內存是由CPU直接尋址的。外存中的程序須要經過I/O接口調入內存中才能夠運行。算法

內存就是程序運行的地方,其實程序本質上就是指令和數據的集合。因此說內存是指令和數據的臨時存儲器,而後CPU對內存中的指令和數據進行處理。編程

2、內存的使用

無論什麼程序語言,其運行都依賴內存,內存生命週期基本是一致的:segmentfault

  1. 分配所須要的內存
  2. 使用分配到的內存(讀、寫)
  3. 不須要時將其釋放\歸還

在JavaScript中,第一步和第三步由js引擎完成的,對於編程人員是隱藏的。可是這並不意味着咱們不須要了解JavaScript中的內存機制,瞭解內存機制有助於咱們寫出更優雅、性能更好的代碼。瀏覽器

3、JavaScript的內存模型

JavaScript數據類型有基本類型和引用類型兩大類,基本類型有Undefined、Null、Boolean、Number、String、Symbol六中,引用類型有Object,全部的JavaScript變量值將會是七種的其中之一。這些數據類型在內存中是怎樣存儲的?咱們來看一下JavaScript的內存模型。閉包

說是JavaScript的內存模型其實不太準確,只是便於理解。因爲JavaScript中的內存分配是由js引擎完成的,因此更準確的描述是js引擎的內存模型ide

一個運行中的程序老是與內存中的一部分空間相對應。這部分空間叫作 Resident Set (駐留集)。V8(一種JS引擎) 組織內存的方式以下圖:函數

各部分做用以下:工具

  • Code Segment : 存放正在被執行的代碼
  • Stack : 棧內存,存放標識符、基本類型值及引用類型變量的堆地址
  • Heap : 堆內存,存放引用類型值

爲何內存要如此分配?

  • 基本類型變量:標識符與值都存放在棧內存中(數據大小固定,由系統自動分配內存空間)。
  • 引用類型變量:棧內存中存放標識符與指向堆內存中值的地址,堆內存中存放具體值(數據大小可變,例如對象可隨意增刪屬性,分配內存的大小取決於代碼)。

4、變量傳遞

看到有些文章中說基本類型變量複製按值傳遞,引用類型變量複製按引用傳遞,又有的說引用類型變量複製按共享傳遞。總之對新手不太友好,這裏咱們站在內存層面來解釋就比較好解釋了。

咱們能夠理解爲JavaScript變量的拷貝都是按棧內存內的值傳遞,這裏棧內存內的值對於基本類型變量來講就是其值,對於引用類型來講就是一個指向堆內存中實際值的地址。

咱們來看一個簡單的例子理解一下:

let p1 = {name: 'logan'}
let p2 = p1
// p1 和 p2 在棧內存中存放的引用地址相同,都指向堆內存中存放對象 {name: 'logan'}
// 可是這兩個引用地址倒是相互獨立的,並不存在引用關係
複製代碼

// 本質上是對堆內存中的對象進行修改,因此會同時影響p1和p2
p2.name = 'jason'
console.log(p1) // 輸出:{name: 'jason'}
console.log(p2) // 輸出:{name: 'jason'}
複製代碼

// 這一步是直接修改了棧內存內標識符p2對應值,並不會影響p1
p2 = 3
console.log(p1) // 輸出:{name: 'jason'}
複製代碼

函數的參數傳遞與變量複製傳遞表現一致,也是按棧內存內的值進行傳遞,由於本質上來講,函數傳參就是把傳入的實參拷貝賦值給形參。

5、垃圾回收

垃圾回收是一種內存管理機制,就是將再也不用到的內存及時釋放,以防內存佔用愈來愈高,致使卡頓甚至進程崩潰。

在JavaScript中內存垃圾回收是由js引擎自動完成的。實現垃圾回收的關鍵在於如何肯定內存再也不使用,也就是肯定對象是否無用。主要有兩種方式:引用計數標記清除

1. 引用計數(reference counting)

這是IE六、7採用的一種比較老的垃圾回收機制。引用計數肯定對象是否無用的方法是對象是否被引用。若是沒有引用指向對象,對象就能夠被回收。咱們結合代碼來理解:

// 堆內存建立了一個對象{a: 1},咱們記爲ObjA,變量obj1指向ObjA,ObjA引用次數爲1
let obj1 = {
    a: 1
}
// obj2 拷貝 obj1 的地址,也指向ObjA,ObjA引用次數爲2
let obj2 = obj1
// 解除obj1對ObjA的引用,ObjA引用次數減一,爲1
obj1 = 3
// 解除obj2對ObjA的引用,ObjA引用次數減一,爲0,能夠被回收
obj2 = 'logan'
複製代碼

缺點:沒法處理循環引用

什麼意思呢,咱們結合代碼理解,先看正常狀況下引用計數的工做:

function func() {
    // 堆內存建立對象{a: 1},記爲ObjA,變量foo指向ObjA,ObjA引用次數爲1
    let foo = {a: 1}
    // 堆內存建立空對象,記爲ObjB,變量bar指向ObjB,ObjB引用次數爲1
    let bar = {}
    // 其屬性x指向ObjA,ObjA引用次數爲2
    bar.x = foo
    
    // 當函數執行完畢返回時
    // 變量bar生命週期結束,ObjB引用次數減一,爲0,可被回收,故對其內部進行回收
    // bar.x生命週期結束,ObjA引用次數減一,爲1
    // 變量foo生命週期結束,ObjA引用次數減一,爲0,可被回收
}
複製代碼

可是若是兩個對象之間存在循環引用,引用計數就會沒法處理:

function func() {
    // 堆內存建立對象{a: 1},記爲ObjA,變量foo指向ObjA,ObjA引用次數爲1
    let foo = {a: 1}
    // 堆內存建立空對象,記爲ObjB,變量bar指向ObjB,ObjB引用次數爲1
    let bar = {}
    // 變量foo屬性x指向ObjB,ObjB引用次數爲2
    foo.x = bar
    // 變量bar屬性x指向ObjA,ObjA引用次數爲2
    bar.x = foo
    
    // 當函數執行完畢返回時
    // 變量bar生命週期結束,ObjB引用次數減一,爲1,不可被回收
    // 變量foo生命週期結束,ObjA引用次數減一,爲1,不可被回收
}
複製代碼

優勢:肯定性

引用計數其實也是有優勢的,那就是對象必定會在最後一個引用失效的時候銷燬,也就是說垃圾回收的時機在代碼內是可控的,因此對於對延時比較敏感的場合比較適用。

2. 標記清除(mark and sweep)

從 2012 年起,全部現代瀏覽器都使用了標記清除的垃圾回收方法。

標記清除的工做原理簡化後就是:從垃圾收集根(root)對象(在JavaScript中爲全局環境記錄)開始,標記出全部能夠得到的對象,而後清除掉全部未標記的不可得到的對象。

也就是說,標記清除肯定對象是否無用的方法是對象是否能夠被得到

現代瀏覽器對JavaScript垃圾回收算法的改進都是基於標記清除算法的改進,並無改進標記清除算法自己和它對「對象是否能夠被得到」的簡化定義。

關於垃圾回收的更多內容,可閱讀淺談V8引擎中的垃圾回收機制

6、內存泄漏

內存泄漏(Memory Leak) 是指程序中己動態分配的堆內存因爲某種緣由程序未釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果。

說到內存泄漏,不得不提一些文章內說閉包會形成內存泄漏,要儘可能少用。其實這個觀點是錯誤的,咱們運用閉包說到底就兩點目的:一是變量私有化,二是延長變量生命週期。 因此說 閉包並不會形成內存泄漏,而是正常的內存使用。

如何避免內存泄漏?一句話:及時解除無用引用。 例如再也不須要的閉包、定時器及全局變量等。說到底仍是我的編程習慣的好壞,多說無益,列太多的條條框框反而顯得繁瑣。

識別內存泄漏

  1. 打開Chrome瀏覽器開發者工具的Performance面板
  2. 選項欄中勾選Memory選項
  3. 點擊左上角錄製按鈕(實心圓狀按鈕)
  4. 在頁面上進行正常操做
  5. 一段時間後,點擊Stop,觀察面板上的數據

如圖所示,內存佔用若是總體平穩,說明不存在內存泄漏。

若是內存佔用只升不降,或者總體呈一直升高的趨勢,說明存在內存泄漏。

內存泄漏定位

若是發現頁面存在內存泄漏,咱們能夠在下方內存圖點擊對應的內存異常處,而後點擊下方面板內的Event Log面板,能夠查看代碼內具體發生了什麼,見下圖:

咱們發現原來是調用了grow函數

let x = []
function grow() {
    x.push(new Array(1000000).join('x'))
}
document.getElementsByClassName('title-h2')[0].addEventListener('click', grow)
複製代碼

固然,上面的代碼只是爲了模擬,到底是否爲內存泄漏要看變量x咱們是否須要用到,一旦不須要,咱們應該解除其引用。

系列文章

深刻ECMAScript系列目錄地址(持續更新中...)

歡迎前往閱讀系列文章,若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。

菜鳥一枚,若是有疑問或者發現錯誤,能夠在相應的 issues 進行提問或勘誤,與你們共同進步。

相關文章
相關標籤/搜索