JavaScript 內(nèi)存機制

每種編程語言都有它的內(nèi)存管理機制,比如C語言這樣的底層語言,有原生內(nèi)存管理接口,像malloc()動態(tài)的分配內(nèi)存空間free()釋放動態(tài)分配的內(nèi)存空間。開發(fā)人員使用這些接口可以顯式分配和釋放操作系統(tǒng)的內(nèi)存。

JS作為一門高級語言,JS并不像底層語言C那樣擁有對內(nèi)存操作的完全掌控。相對地,JavaScript會在創(chuàng)建變量(對象、字符串)時自動分配內(nèi)存,并在這些變量不被使用時自動釋放內(nèi)存,這個過程被稱為垃圾回收。

內(nèi)存生命周期

不管什么程序語言,內(nèi)存生命周期基本是一致的:

  • 分配你所需要的內(nèi)存
  • 使用分配到的內(nèi)存(進行讀、寫)
  • 不需要時將內(nèi)存進行釋放

JS 內(nèi)存模型

JavaScript中的內(nèi)存分配是由js引擎完成的,內(nèi)存空間分為兩種:棧內(nèi)存(stack) 與 堆內(nèi)存(heap), 而JavaScript的數(shù)據(jù)類型也分為兩大類, 分別是基本數(shù)據(jù)類型和引用數(shù)據(jù)類型,與兩種內(nèi)存空間相對應。

基礎(chǔ)數(shù)據(jù)類型與棧內(nèi)存

JS中的基礎(chǔ)數(shù)據(jù)類型都有固定的大小,往往都保存在棧內(nèi)存中(閉包除外),由系統(tǒng)自動分配存儲空間。我們可以直接操作保存在棧內(nèi)存空間的值,因此基礎(chǔ)數(shù)據(jù)類型都是按值訪問數(shù)據(jù),在棧內(nèi)存中的存儲與使用方式類似于數(shù)據(jù)結(jié)構(gòu)中的堆棧數(shù)據(jù)結(jié)構(gòu),遵循后進先出的原則。

基礎(chǔ)數(shù)據(jù)類型:

Number、String、Null、Boolean、Undefiend、Symbol(ES6新增)

簡單理解棧的存取方式,我們可以通過類比乒乓球盒子來分析。


這種乒乓球的存放方式與棧中存取數(shù)據(jù)的方式如出一轍。處于盒子中最頂層的乒乓球5,它一定是最后被放進去,但可以最先被拿出來。而我們想要拿出底層的乒乓球1,就必須將上面的4個乒乓球取出來,讓乒乓球1處于盒子頂層。這就是??臻g先進后出,后進先出的特點。

引用數(shù)據(jù)類型與堆內(nèi)存

JS的引用數(shù)據(jù)類型,比如數(shù)組Array,它們值的大小是不固定的。引用數(shù)據(jù)類型的值是保存在堆內(nèi)存中的對象。JavaScript不允許直接訪問堆內(nèi)存中的位置,因此我們不能直接操作對象的堆內(nèi)存空間。在操作對象時,實際上是在操作對象的引用而不是實際的對象。因此,引用類型的值都是按引用訪問的。這里的引用,我們可以粗淺地理解為保存在變量對象中的一個地址,該地址與堆內(nèi)存的實際值相關(guān)聯(lián)。

特點:不連續(xù)的內(nèi)存區(qū)域,容量較大,讀取速度慢(因為引用地址在堆中,多了一次中轉(zhuǎn),所以讀取速度自然會比棧要慢)。隨意讀取,類似于圖書館書架上的書,喜歡哪本拿哪本。

熟知的引用數(shù)據(jù)類型:

Object、Array、Date、RegExp、Function 等。

var a1 = 0;   // 變量對象
var a2 = 'this is string'; // 變量對象
var a3 = null; // 變量對象

var b = { m: 20 }; // 變量b存在于變量對象中,{m: 20} 作為對象存在于堆內(nèi)存中
var c = [1, 2, 3]; // 變量c存在于變量對象中,[1, 2, 3] 作為對象存在于堆內(nèi)存中
image.png

當我們要訪問堆內(nèi)存中的引用數(shù)據(jù)類型時,實際上我們首先是從變量對象中獲取了該對象的地址引用(或者地址指針),然后再從堆內(nèi)存中取得我們需要的數(shù)據(jù)。

接下來,我們通過下面的例子來加深對JS內(nèi)存的理解

var a = 20;
var b = a;
b = 30;

var m = { a: 10, b: 20 };
var n = m;
n.a = 15; 
image.png

在變量對象中的數(shù)據(jù)發(fā)生復制行為時,系統(tǒng)會自動為新的變量分配一個新值。var b = a執(zhí)行之后,a與b雖然值都等于20,但是他們其實已經(jīng)是相互獨立互不影響的值了。具體如圖。所以我們修改了b的值以后,a的值并不會發(fā)生變化。


image.png

通過var n = m執(zhí)行一次復制引用類型的操作。引用類型的復制同樣也會為新的變量自動分配一個新的值保存在變量對象中,但不同的是,這個新的值,僅僅只是引用類型的一個地址指針。當?shù)刂分羔樝嗤瑫r,盡管他們相互獨立,但是在變量對象中訪問到的具體對象實際上是同一個。

內(nèi)存回收

垃圾回收是一種內(nèi)存管理機制,就是將不再用到的內(nèi)存及時釋放,以防內(nèi)存占用越來越高,防止卡頓甚至進程崩潰。在JavaScript中有自動化的垃圾回收機制,自動回收過期無效的變量。

在JavaScript中內(nèi)存垃圾回收是由js引擎自動完成的。實現(xiàn)垃圾回收的關(guān)鍵在于如何確定內(nèi)存不再使用,也就是確定對象是否無用。主要有兩種方式:*引用計數(shù)標記清除。

引用計數(shù)算法

引用就是指向某一地址的指針。我們可簡單將引用視為一個對象訪問另一個對象的路徑。(這里的對象是一個寬泛的概念,泛指JS環(huán)境中的實體)。

引用計數(shù)算法定義就是以內(nèi)存不再使用為標準,就是看一個對象是否有指向它的引用。如果沒有其他地址指向它了,說明該對象已經(jīng)不再需要了,可以進行回收。

下面來看個例子:

// 創(chuàng)建一個對象person,他有兩個指向?qū)傩詀ge和name的引用
var person = {
    age: 22,
    name: 'ifcode'
};

person.name = null; // 雖然設(shè)置為null,但因為person對象還有指向name的引用,因此name不會回收

var p = person; 
person = 1;         //原來的person對象被賦值為1,但因為有新引用p指向原person對象,因此它不會被回收

p = null;           //原person對象已經(jīng)沒有引用,很快會被回收

由上面例子可以看出,引用計數(shù)算法是個簡單有效的算法。但它卻存在一個致命的問題:循環(huán)引用。如果兩個對象相互引用,盡管他們已不再使用,垃圾回收器不會進行回收,導致內(nèi)存泄露。

function cycle() {
    var o1 = {};
    var o2 = {};
    o1.a = o2;
    o2.a = o1; 
    
    return "Cycle reference!"
}

cycle();

上面我們申明了一個cycle方程,其中包含兩個相互引用的對象。在調(diào)用函數(shù)結(jié)束后,對象o1和o2實際上已離開函數(shù)范圍,因此不再需要了。但根據(jù)引用計數(shù)的原則,他們之間的相互引用依然存在,因此這部分內(nèi)存不會被回收,內(nèi)存泄露不可避免了。

正是因為有這個嚴重的缺點,這個算法在現(xiàn)代瀏覽器中已經(jīng)被下面要介紹的標記清除算法所取代了。但絕不可認為該問題已經(jīng)不再存在了,因為還占有大量市場的IE6、IE7使用的正是這一算法。在需要照顧兼容性的時候,某些看起來非常普通的寫法也可能造成意想不到的問題:

var div = document.createElement("div");
div.onclick = function() {
    console.log("click");
};

現(xiàn)在雖然有各種框架,很少直接操作dom了 ,但上面這種JS寫法很簡單卻存在問題。創(chuàng)建一個DOM元素并綁定一個點擊事件,這里有什么問題呢?請注意,變量div有事件處理函數(shù)的引用,同時事件處理函數(shù)也有div的引用!(div變量可在函數(shù)內(nèi)被訪問)。一個循序引用出現(xiàn)了,按上面所講的算法,該部分內(nèi)存無可避免地泄露了。

標記清除算法

上面說過,現(xiàn)代的瀏覽器已經(jīng)不再使用引用計數(shù)算法了?,F(xiàn)代瀏覽器通用的大多是基于標記清除算法的某些改進算法,總體思想都是一致的。

標記清除算法將“不再使用的對象”定義為“無法達到的對象”。簡單來說,就是從根部(在JS中就是全局對象)出發(fā)定時掃描內(nèi)存中的對象。凡是能從根部到達的對象,都是還需要使用的。那些無法由根部出發(fā)觸及到的對象被標記為不再使用,稍后進行回收。

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

根據(jù)這個概念,上面的例子可以正確被垃圾回收處理了。當div與其時間處理函數(shù)不能再從全局對象出發(fā)觸及的時候,垃圾回收器就會標記并回收這兩個對象。

image.png

內(nèi)存管理友好的JS代碼

如果還需要兼容老舊瀏覽器,那么就需要注意代碼中的循環(huán)引用問題?;蛘咧苯硬捎帽WC兼容性的庫來幫助優(yōu)化代碼。
對現(xiàn)代瀏覽器來說,唯一要注意的就是明確切斷需要回收的對象與根部的聯(lián)系。有時候這種聯(lián)系并不明顯,且因為標記清除算法的強壯性,這個問題較少出現(xiàn)。最常見的內(nèi)存泄露一般都與DOM元素綁定有關(guān):

email.message = document.createElement(“div”);
displayList.appendChild(email.message);

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

div元素已經(jīng)從DOM樹中清除,也就是說從DOM樹的根部無法觸及該div元素了。但是請注意,div元素同時也綁定了email對象。所以只要email對象還存在,該div元素將一直保存在內(nèi)存中。

內(nèi)存泄露

對于持續(xù)運行的服務進程(daemon),必須及時釋放不再用到的內(nèi)存。否則,內(nèi)存占用越來越高,輕則影響系統(tǒng)性能,重則導致進程崩潰。 不再用到的內(nèi)存,沒有及時釋放,就叫做內(nèi)存泄漏。

常見的內(nèi)存泄露

1.意外的全局變量
function foo() {
      bar = '全局變量'; // 沒有聲明變量 實際上是全局變量=>window.bar
    }
    foo();


function foo() {
     const bar = 'foo變量'; // 沒有聲明變量 實際上是全局變量=>window.bar
    }
    foo();
2.定時器和回調(diào)函數(shù)

當不需要setInterval或者setTimeout時,定時器沒有被清除,定時器的回調(diào)函數(shù)以及內(nèi)部依賴的變量都不能被回收,造成內(nèi)存泄漏。

setInterval(function() {
    // 執(zhí)行什么
}, 1000);

頁面卸載或者執(zhí)行完定時器需要主動清除

3.濫用閉包
function fn2(){
  let test = new Array(1000).fill('test')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2()
fn2Child()
//fn2Child = null 解決方法 函數(shù)調(diào)用后,把外部的引用關(guān)系置空

return 的函數(shù)中存在函數(shù) fn2 中的 test 變量引用,所以 test 并不會被回收,也就造成了內(nèi)存泄漏。fn2Child = null 解決方法 函數(shù)調(diào)用后,把外部的引用關(guān)系置空

4.沒有清理DOM元素引用
var refA = document.getElementById("test");
document.body.removeChild(refA); // dom刪除了
console.log(refA, "refA");  // 但是還存在引用 能console出整個div 沒有被回收
refA = null;
console.log(refA, "refA");  // 解除引用
5.console保存大量數(shù)據(jù)在內(nèi)存中

過多的console,比如定時器的console會導致瀏覽器卡死。
合理利用console,線上項目盡量少的使用console。

內(nèi)存查看

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


    image.png

如果內(nèi)存占用基本平穩(wěn),接近水平,就說明不存在內(nèi)存泄漏。

image

反之,就是內(nèi)存泄漏了。

image
  • 命令行方法
    命令行可以使用 Node 提供的 process.memoryUsage 方法。
console.log(process.memoryUsage());
//{
  //rss: 101568512,
  //heapTotal: 72605696,
  //heapUsed: 51070584,
  //external: 5819790,
  //arrayBuffers: 4286309
//}

process.memoryUsage返回一個對象,包含了 Node 進程的內(nèi)存占用信息。該對象包含四個字段,單位是字節(jié),含義如下:

rss(resident set size):所有內(nèi)存占用,包括指令區(qū)和堆棧。
heapTotal:"堆"占用的內(nèi)存,包括用到的和沒用到的。
heapUsed:用到的堆的部分。
external: V8 引擎內(nèi)部的 C++ 對象占用的內(nèi)存。
arrayBuffers: ArrayBufferSharedArrayBuffer 分配的內(nèi)存,包括所有 Node.js Buffer

判斷內(nèi)存泄漏,以heapUsed字段為準。

參考:
https://juejin.cn/post/6844903801191661575#heading-13
http://www.ruanyifeng.com/blog/2017/04/memory-leak.html
https://juejin.cn/post/6844903801191661575#heading-9

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 簡介每種編程語言都有它的內(nèi)存管理機制,比如簡單的C有低級的內(nèi)存管理基元,像malloc(),free()。同樣我們...
    曲昶光閱讀 350評論 0 1
  • 前言 每種編程語言都有它的內(nèi)存管理機制,比如簡單的C有低級的內(nèi)存管理基元,像malloc(),free()。而對于...
    青城墨闋閱讀 1,121評論 0 0
  • 為什么要關(guān)注內(nèi)存 任何程序的運行都要分配運行空間。 如果不在使用的內(nèi)容得不到釋放,不會返回到操作系統(tǒng)或空閑內(nèi)存池,...
    夏末遠歌閱讀 383評論 0 0
  • 對于前端攻城師來說,JS的內(nèi)存機制不容忽視。如果想成為行業(yè)專家,或者打造高性能前端應用,那就必須要弄清楚JavaS...
    IT沐華閱讀 655評論 0 0
  • JavaScript不同于其他語言,在JavaScript中的內(nèi)存都是自動分配和回收。如同請人打掃衛(wèi)生。其實在大多...
    Pamcore閱讀 207評論 0 0

友情鏈接更多精彩內(nèi)容