JavaScript內存模型

1.簡介

小弟也是後端開發的,工做中接觸JavaScript好久了.想借助本文講解一下JavaScript內存.同時也爲後續的設計模式作鋪墊.javascript

每種編程語言都有它的內存管理機制,比java也有本身的內存和GC。一樣咱們在學習JavaScript的時候,頗有必要了解JavaScript的內存管理機制。html

JavaScript的內存管理機制是:內存基元在變量(對象,字符串等等)建立時分配,而後在他們再也不被使用時「自動」釋放。後者被稱爲垃圾回收。這個「自動」是混淆並給JavaScript(和其餘高級語言)開發者一個錯覺:他們能夠不用考慮內存管理。對於前端開發來講,內存空間並非一個常常被說起的概念,很容易被你們忽視。固然也包括我本身。(原本不想長篇大論的W( ̄_ ̄)W);前端

2.內存模型

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

2.1基礎數據類型與棧內存

數據在棧內存中的存儲與使用方式相似於數據結構中的堆棧數據結構,遵循後進先出的原則。
基礎數據類型: Number String Null Undefined Boolean程序員

2.2引用數據類型與堆內存

爲了更好的搞懂棧內存與堆內存,咱們能夠結合如下例子與圖解進行理解。
引用類型的值都是按引用訪問的。這裏的引用,咱們能夠粗淺地理解爲保存在棧內存中的一個地址,該地址與堆內存的實際值相關聯。算法

var a1 = 0; // 棧
var a2 = 'this is string'; // 棧
var a3 = null; // 棧
var b = { m: 20 }; // 變量b存在於棧中,{m: 20} 做爲對象存在於堆內存中
var c = [1, 2, 3]; // 變量c存在於棧中,[1, 2, 3] 做爲對象存在於堆內存中編程

// demo01.js
var a = 20;
var b = a;
b = 30;
// 這時a的值是多少?

// demo02.js
var m = { a: 10, b: 20 };
var n = m;
n.a = 15;
// 這時m.a的值是多少

相信上面的問題可想而知 ,a仍是20,可是m.a是15後端

 

 

3.內存的生命週期

JS環境中分配的內存通常有以下生命週期:設計模式

  1. 內存分配:當咱們申明變量、函數、對象的時候,系統會自動爲他 們分配內存
  2. 內存使用:即讀寫內存,也就是使用變量、函數等
  3. 內存回收:使用完畢,由垃圾回收機制自動回收再也不使用的內存

廢話很少說看代碼瀏覽器

var a = 20;  // 在內存中給數值變量分配空間
alert(a + 100);  // 使用內存
var a = null; // 使用完畢以後,釋放內存空間

//注意這行代碼的意識
console.info(typeof(null))//object
console.info( typeof(undefined) )//undefined

第一步和第二步咱們都很好理解,JavaScript在定義變量時就完成了內存分配。第三步釋放內存空間則是咱們須要重點理解的一個點。

如今想一想,從內存來看 null 和 undefined 本質的區別是什麼?

爲何typeof(null) //object typeof(undefined) //undefined

如今再想一想,構造函數和當即執行函數的聲明週期是什麼?

ES6語法中的 const 聲明一個只讀的常量。一旦聲明,常量的值就不能改變。可是下面的代碼能夠改變 const 的值,這是爲何?

const foo = {}; 
foo.prop = 123;
foo.prop // 123
foo = {}; // TypeError: "foo" is read-only

4.內存回收

JavaScript有自動垃圾收集機制,就是找出那些再也不繼續使用的值,而後釋放其佔用的內存。垃圾收集器會每隔固定的時間段就執行一次釋放操做.

在JavaScript中,最經常使用的是經過標記清除的算法來找到哪些對象是再也不繼續使用的,所以 a = null 其實僅僅只是作了一個釋放引用的操做,讓 a 本來對應的值失去引用,脫離執行環境,這個值會在下一次垃圾收集器執行操做時被找到並釋放。而在適當的時候解除引用,是爲頁面得到更好性能的一個重要方式。

  • 在局部做用域中,當函數執行完畢,局部變量也就沒有存在的必要了,所以垃圾收集器很容易作出判斷並回收。可是全局變量何時須要自動釋放內存空間則很難判斷,所以在咱們的開發中,須要儘可能避免使用全局變量,以確保性能問題
  • 以Google的V8引擎爲例,在V8引擎中全部的JAVASCRIPT對象都是經過堆來進行內存分配的。當咱們在代碼中聲明變量並賦值時,V8引擎就會在堆內存中分配一部分給這個變量。若是已申請的內存不足以存儲這個變量時,V8引擎就會繼續申請內存,直到堆的大小達到了V8引擎的內存上限爲止(默認狀況下,V8引擎的堆內存的大小上限在64位系統中爲1464MB,在32位系統中則爲732MB)。

  • 另外,V8引擎對堆內存中的JAVASCRIPT對象進行分代管理。新生代:新生代即存活週期較短的JAVASCRIPT對象,如臨時變量、字符串等;老生代:老生代則爲通過屢次垃圾回收仍然存活,存活週期較長的對象,如主控制器、服務器對象等。

請各位老鐵see一下如下的代碼,來分析一下垃圾回收。

function fun1() {
    var obj = {name: 'csa', age: 24};
}
 
function fun2() {
    var obj = {name: 'coder', age: 2}
    return obj;
}
 
var f1 = fun1();
var f2 = fun2();
/*在上述代碼中,當執行var f1 = fun1();的時候,執行環境會建立一個{name:'csa', age:24}這個對象,
 *當執行var f2 = fun2();的時候,執行環境會建立一個{name:'coder', age=2}這個對象,
 *而後在下一次垃圾回收來臨的時候,會釋放{name:'csa', age:24}這個對象的內存,
 *但並不會釋放{name:'coder', age:2}這個對象的內存。
 *這就是由於在fun2()函數中將*{name:'coder, age:2'}這個對象返回,
 *而且將其引用賦值給了f2變量,又因爲f2這個對象屬於全局變量,
 *因此在頁面沒有卸載的狀況下,f2所指向的對象{name:'coder', age:2}是不會被回收的。
 *因爲JavaScript語言的特殊性(閉包...),
 *致使如何判斷一個對象是否會被回收的問題上變的異常艱難,各位老鐵看看就行。
 */

4.1垃圾回收算法

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

4.1.1引用計數算法

熟悉或者用C語言搞過事的同窗的都明白,引用無非就是指向某一物體的指針。對不熟悉這個語言的同窗來講,可簡單將引用視爲一個對象訪問另外一個對象的路徑。(這裏的對象是一個寬泛的概念,泛指JS環境中的實體.本人最初作Java開發,java裏面會有環形引用的問題.JavaScript也有這樣的問題)。

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

// 建立一個對象person,他有兩個指向屬性age和name的引用
var person = {
    age: 12,
    name: 'aaaa'
};

person.name = null; // 雖然設置爲null,但由於person對象還有指向name的引用,所以name不會回收

var p = person; 
person = 1;         //原來的person對象被賦值爲1,但由於有新引用p指向原person對象,所以它不會被回收

p = null;

由上面能夠看出,引用計數算法是個簡單有效的算法。但它卻存在一個致命的問題:循環引用。若是兩個對象相互引用,儘管他們已再也不使用,垃圾回收器不會進行回收,致使內存泄露。

function cycle() {
    var o1 = {};
    var o2 = {};
    o1.a = o2;
    o2.a = o1; 

    return "Cycle reference!"
}

cycle();
//出現循環引用
/*上面咱們申明瞭一個cycle方程,其中包含兩個相互引用的對象。在調用函數結束後,
*對象o1和o2實際上已離開函數範圍,所以再也不須要了。但根據引用計數的原則,
*他們之間的相互引用依然存在,所以這部份內存不會被*回收,內存泄露不可避免了。
*正是由於有這個嚴重的缺點,這個算法在現代瀏覽器中已經被下面要介紹的標記清除算法所取代了。
*但毫不可認爲該問題已經再也不存在了,由於還佔有大量市場的IE老祖宗們使用的正是這一算法。
*在須要照顧兼容性的時候,某些看起來很是普通的寫法也可能形成意想不到的問題:
*/

正是由於有這個嚴重的缺點,這個算法在現代瀏覽器中已經被下面要介紹的標記清除算法所取代了。但毫不可認爲該問題已經再也不存在了,由於還佔有大量市場的IE老祖宗們使用的正是這一算法。在須要照顧兼容性的時候,某些看起來很是普通的寫法也可能形成意想不到的問題:

var div = document.createElement("div");
 div.onclick = function() {
     console.log("click");
 };
/*
上面這種JS寫法再普通不過了,建立一個DOM元素並綁定一個點擊事件。
那麼這裏有什麼問題呢?請注意,變量div有事件處理函數的引用,
同時事件處理函數也有div的引用!(div變量可在函數內被訪問)。
一個循序引用出現了,按上面所講的算法,該部份內存無可避免地泄露哦了。
如今你明白爲啥前端程序員都討厭IE了吧?擁有超多BUG並依然佔有大量市場的IE是前端開發一輩子之敵!
親,沒有買賣就沒有殺害。
*/

4.1.2標記清除算法

上面說過,現代的瀏覽器已經再也不使用引用計數算法了。現代瀏覽器通用的大可能是基於標記清除算法的某些改進算法,整體思想都是一致的。

標記清除算法將「再也不使用的對象」定義爲「沒法達到的對象」。簡單來講,就是從根部(在JS中就是全局對象)出發定時掃描內存中的對象。凡是能從根部到達的對象,都是還須要使用的。那些沒法由根部出發觸及到的對象被標記爲再也不使用,稍後進行回收。

從這個概念能夠看出,沒法觸及的對象包含了沒有引用的對象這個概念(沒有任何引用的對象也是沒法觸及的對象)。但反之未必成立。

根據這個概念,上面的例子能夠正確被垃圾回收處理了(親,想一想爲何?)。

當div與其時間處理函數不能再從全局對象出發觸及的時候,垃圾回收器就會標記並回收這兩個對象。

4.2如何寫出對內存管理友好的JS代碼?

若是還須要兼容老舊瀏覽器,那麼就須要注意代碼中的循環引用問題。或者直接採用保證兼容性的庫來幫助優化代碼。

對現代瀏覽器來講,惟一要注意的就是明確切斷須要回收的對象與根部的聯繫。有時候這種聯繫並不明顯,且由於標記清除算法的強壯性,這個問題較少出現。最多見的內存泄露通常都與DOM元素綁定有關:

email.message = document.createElement(「div」);
displayList.appendChild(email.message);

// 稍後從displayList中清除DOM元素
displayList.removeAllChildren();

div元素已經從DOM樹中清除,也就是說從DOM樹的根部沒法觸及該div元素了。可是請注意,div元素同時也綁定了email對象。因此只要email對象還存在,該div元素將一直保存在內存中.

小結

若是你的引用只包含少許JS交互,那麼內存管理不會對你形成太多困擾。一旦你開始構建中大規模的SPA好比服務器和桌面端的應用,那麼就應當將內存泄露提上日程了。不要知足於寫出能運行的程序,也不要認爲機器的升級就能解決一切

我的筆記:瀏覽器在解析html會把全部的標籤所有解析成dom或則bom結構,全都加載到堆裏面去,這些標籤都是對象,typeof 判斷出來是object的必定是在堆裏面的

<!DOCTYPE HTML>
<html lang="en-US">
<head>
	<meta charset="UTF-8">
	<title></title>
</head>
<body>
	<div id="myDiv"></div>
	<script type="text/javascript">
    //DIV標籤的一個實例
	var myDiv= document.getElementById("myDiv");
	var num =123;
	
	console.info(typeof myDiv); //object
	console.info(typeof num); //number
	</script>
</body>
</html>

5.內存泄漏的識別方法

   5.1瀏覽器方法

  1. 打開開發者工具,選擇 Timeline 面板
  2. 在頂部的Capture字段裏面勾選 Memory
  3. 點擊左上角的錄製按鈕。
  4. 在頁面上進行各類操做,模擬用戶的使用狀況。
  5. 一段時間後,點擊對話框的 stop 按鈕,面板上就會顯示這段時間的內存佔用狀況。

若是內存佔用基本平穩,接近水平,就說明不存在內存泄漏。反之,就是內存泄漏了。

   5.2命令行方法

 

console.log(process.memoryUsage());
// { rss: 27709440,
//  heapTotal: 5685248,
//  heapUsed: 3449392,
//  external: 8772 }
//process.memoryUsage返回一個對象,包含了 Node 進程的內存佔用信息。該對象包含四個字段,單位是字節,含義以下。

//Resident Set(常駐內存)
//Code Segment(代碼區)
//Stack(Local Variables, Pointers)
//Heap(Objects, Closures)
//Used Heap
//rss(resident set size):全部內存佔用,包括指令區和堆棧。
//heapTotal:"堆"佔用的內存,包括用到的和沒用到的。
//heapUsed:用到的堆的部分。
//external: V8 引擎內部的 C++ 對象佔用的內存。
//判斷內存泄漏,以heapUsed字段爲準。

5.3WeakMap

前面說過,及時清除引用很是重要。可是,你不可能記得那麼多,有時候一疏忽就忘了,因此纔有那麼多內存泄漏。

最好能有一種方法,在新建引用的時候就聲明,哪些引用必須手動清除,哪些引用能夠忽略不計,當其餘引用消失之後,垃圾回收機制就能夠釋放內存。這樣就能大大減輕程序員的負擔,你只要清除主要引用就能夠了。

ES6 考慮到了這一點,推出了兩種新的數據結構:WeakSet 和 WeakMap。它們對於值的引用都是不計入垃圾回收機制的,因此名字裏面纔會有一個"Weak",表示這是弱引用。

下面以 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的引用就是弱引用,不會被計入垃圾回收機制。

也就是說,DOM 節點對象的引用計數是1,而不是2。這時,一旦消除對該節點的引用,它佔用的內存就會被垃圾回收機制釋放。Weakmap 保存的這個鍵值對,也會自動消失。

基本上,若是你要往對象上添加數據,又不想幹擾垃圾回收機制,就可使用 WeakMap

本章也是爲了跟好的去理解JavaScript,目前移動開發也能夠用JavaScript,同時跟方便去講解後面的內容

<JavaScript 高級特性 做用域詳解>

特別感謝:

相關文章
相關標籤/搜索