每種編程語言都有它的內(nèi)存管理機(jī)制掀泳,比如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)建變量(對象滥玷、字符串)時(shí)自動分配內(nèi)存氏身,并在這些變量不被使用時(shí)自動釋放內(nèi)存,這個(gè)過程被稱為垃圾回收惑畴。
內(nèi)存生命周期
不管什么程序語言蛋欣,內(nèi)存生命周期基本是一致的:
- 分配你所需要的內(nèi)存
- 使用分配到的內(nèi)存(進(jìn)行讀、寫)
- 不需要時(shí)將內(nèi)存進(jìn)行釋放
JS 內(nèi)存模型
JavaScript中的內(nèi)存分配是由js引擎完成的如贷,內(nèi)存空間分為兩種:棧內(nèi)存(stack) 與 堆內(nèi)存(heap), 而JavaScript的數(shù)據(jù)類型也分為兩大類陷虎, 分別是基本數(shù)據(jù)類型和引用數(shù)據(jù)類型,與兩種內(nèi)存空間相對應(yīng)杠袱。
基礎(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)纹蝴,遵循后進(jìn)先出的原則庄萎。
基礎(chǔ)數(shù)據(jù)類型:
Number、String塘安、Null糠涛、Boolean、Undefiend兼犯、Symbol(ES6新增)
簡單理解棧的存取方式脱羡,我們可以通過類比乒乓球盒子來分析。
這種乒乓球的存放方式與棧中存取數(shù)據(jù)的方式如出一轍免都。處于盒子中最頂層的乒乓球5锉罐,它一定是最后被放進(jìn)去,但可以最先被拿出來绕娘。而我們想要拿出底層的乒乓球1脓规,就必須將上面的4個(gè)乒乓球取出來,讓乒乓球1處于盒子頂層险领。這就是椙扔撸空間先進(jìn)后出,后進(jìn)先出的特點(diǎn)绢陌。
引用數(shù)據(jù)類型與堆內(nèi)存
JS的引用數(shù)據(jù)類型挨下,比如數(shù)組Array,它們值的大小是不固定的脐湾。引用數(shù)據(jù)類型的值是保存在堆內(nèi)存中的對象臭笆。JavaScript不允許直接訪問堆內(nèi)存中的位置,因此我們不能直接操作對象的堆內(nèi)存空間。在操作對象時(shí)愁铺,實(shí)際上是在操作對象的引用而不是實(shí)際的對象鹰霍。因此,引用類型的值都是按引用訪問的茵乱。這里的引用茂洒,我們可以粗淺地理解為保存在變量對象中的一個(gè)地址,該地址與堆內(nèi)存的實(shí)際值相關(guān)聯(lián)瓶竭。
特點(diǎn):不連續(xù)的內(nèi)存區(qū)域督勺,容量較大,讀取速度慢(因?yàn)橐玫刂吩诙阎薪锓。嗔艘淮沃修D(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)存中
當(dāng)我們要訪問堆內(nèi)存中的引用數(shù)據(jù)類型時(shí)糟需,實(shí)際上我們首先是從變量對象中獲取了該對象的地址引用(或者地址指針),然后再從堆內(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;
在變量對象中的數(shù)據(jù)發(fā)生復(fù)制行為時(shí),系統(tǒng)會自動為新的變量分配一個(gè)新值圆凰。var b = a執(zhí)行之后杈帐,a與b雖然值都等于20,但是他們其實(shí)已經(jīng)是相互獨(dú)立互不影響的值了专钉。具體如圖挑童。所以我們修改了b的值以后,a的值并不會發(fā)生變化跃须。
通過var n = m執(zhí)行一次復(fù)制引用類型的操作站叼。引用類型的復(fù)制同樣也會為新的變量自動分配一個(gè)新的值保存在變量對象中,但不同的是菇民,這個(gè)新的值尽楔,僅僅只是引用類型的一個(gè)地址指針投储。當(dāng)?shù)刂分羔樝嗤瑫r(shí),盡管他們相互獨(dú)立翔试,但是在變量對象中訪問到的具體對象實(shí)際上是同一個(gè)轻要。
內(nèi)存回收
垃圾回收是一種內(nèi)存管理機(jī)制复旬,就是將不再用到的內(nèi)存及時(shí)釋放垦缅,以防內(nèi)存占用越來越高,防止卡頓甚至進(jìn)程崩潰驹碍。在JavaScript中有自動化的垃圾回收機(jī)制壁涎,自動回收過期無效的變量。
在JavaScript中內(nèi)存垃圾回收是由js引擎自動完成的志秃。實(shí)現(xiàn)垃圾回收的關(guān)鍵在于如何確定內(nèi)存不再使用怔球,也就是確定對象是否無用。主要有兩種方式:*引用計(jì)數(shù) 和 標(biāo)記清除浮还。
引用計(jì)數(shù)算法
引用就是指向某一地址的指針竟坛。我們可簡單將引用視為一個(gè)對象訪問另一個(gè)對象的路徑。(這里的對象是一個(gè)寬泛的概念钧舌,泛指JS環(huán)境中的實(shí)體)担汤。
引用計(jì)數(shù)算法定義就是以內(nèi)存不再使用為標(biāo)準(zhǔn),就是看一個(gè)對象是否有指向它的引用洼冻。如果沒有其他地址指向它了崭歧,說明該對象已經(jīng)不再需要了,可以進(jìn)行回收撞牢。
下面來看個(gè)例子:
// 創(chuàng)建一個(gè)對象person率碾,他有兩個(gè)指向?qū)傩詀ge和name的引用
var person = {
age: 22,
name: 'ifcode'
};
person.name = null; // 雖然設(shè)置為null,但因?yàn)閜erson對象還有指向name的引用屋彪,因此name不會回收
var p = person;
person = 1; //原來的person對象被賦值為1所宰,但因?yàn)橛行乱胮指向原person對象,因此它不會被回收
p = null; //原person對象已經(jīng)沒有引用畜挥,很快會被回收
由上面例子可以看出歧匈,引用計(jì)數(shù)算法是個(gè)簡單有效的算法。但它卻存在一個(gè)致命的問題:循環(huán)引用砰嘁。如果兩個(gè)對象相互引用件炉,盡管他們已不再使用,垃圾回收器不會進(jìn)行回收矮湘,導(dǎo)致內(nèi)存泄露斟冕。
function cycle() {
var o1 = {};
var o2 = {};
o1.a = o2;
o2.a = o1;
return "Cycle reference!"
}
cycle();
上面我們申明了一個(gè)cycle方程,其中包含兩個(gè)相互引用的對象缅阳。在調(diào)用函數(shù)結(jié)束后磕蛇,對象o1和o2實(shí)際上已離開函數(shù)范圍景描,因此不再需要了。但根據(jù)引用計(jì)數(shù)的原則秀撇,他們之間的相互引用依然存在超棺,因此這部分內(nèi)存不會被回收,內(nèi)存泄露不可避免了呵燕。
正是因?yàn)橛羞@個(gè)嚴(yán)重的缺點(diǎn),這個(gè)算法在現(xiàn)代瀏覽器中已經(jīng)被下面要介紹的標(biāo)記清除算法所取代了再扭。但絕不可認(rèn)為該問題已經(jīng)不再存在了,因?yàn)檫€占有大量市場的IE6泛范、IE7使用的正是這一算法。在需要照顧兼容性的時(shí)候罢荡,某些看起來非常普通的寫法也可能造成意想不到的問題:
var div = document.createElement("div");
div.onclick = function() {
console.log("click");
};
現(xiàn)在雖然有各種框架,很少直接操作dom了 ,但上面這種JS寫法很簡單卻存在問題区赵。創(chuàng)建一個(gè)DOM元素并綁定一個(gè)點(diǎn)擊事件惭缰,這里有什么問題呢?請注意惧笛,變量div有事件處理函數(shù)的引用从媚,同時(shí)事件處理函數(shù)也有div的引用!(div變量可在函數(shù)內(nèi)被訪問)患整。一個(gè)循序引用出現(xiàn)了拜效,按上面所講的算法,該部分內(nèi)存無可避免地泄露了各谚。
標(biāo)記清除算法
上面說過紧憾,現(xiàn)代的瀏覽器已經(jīng)不再使用引用計(jì)數(shù)算法了。現(xiàn)代瀏覽器通用的大多是基于標(biāo)記清除算法的某些改進(jìn)算法昌渤,總體思想都是一致的赴穗。
標(biāo)記清除算法將“不再使用的對象”定義為“無法達(dá)到的對象”。簡單來說膀息,就是從根部(在JS中就是全局對象)出發(fā)定時(shí)掃描內(nèi)存中的對象般眉。凡是能從根部到達(dá)的對象,都是還需要使用的潜支。那些無法由根部出發(fā)觸及到的對象被標(biāo)記為不再使用甸赃,稍后進(jìn)行回收。
從這個(gè)概念可以看出冗酿,無法觸及的對象包含了沒有引用的對象這個(gè)概念(沒有任何引用的對象也是無法觸及的對象)埠对。但反之未必成立络断。
根據(jù)這個(gè)概念,上面的例子可以正確被垃圾回收處理了项玛。當(dāng)div與其時(shí)間處理函數(shù)不能再從全局對象出發(fā)觸及的時(shí)候貌笨,垃圾回收器就會標(biāo)記并回收這兩個(gè)對象。
內(nèi)存管理友好的JS代碼
如果還需要兼容老舊瀏覽器襟沮,那么就需要注意代碼中的循環(huán)引用問題锥惋。或者直接采用保證兼容性的庫來幫助優(yōu)化代碼臣嚣。
對現(xiàn)代瀏覽器來說净刮,唯一要注意的就是明確切斷需要回收的對象與根部的聯(lián)系剥哑。有時(shí)候這種聯(lián)系并不明顯硅则,且因?yàn)闃?biāo)記清除算法的強(qiáng)壯性,這個(gè)問題較少出現(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元素同時(shí)也綁定了email對象。所以只要email對象還存在姜骡,該div元素將一直保存在內(nèi)存中屿良。
內(nèi)存泄露
對于持續(xù)運(yùn)行的服務(wù)進(jìn)程(daemon)尘惧,必須及時(shí)釋放不再用到的內(nèi)存。否則啥么,內(nèi)存占用越來越高悬荣,輕則影響系統(tǒng)性能似踱,重則導(dǎo)致進(jìn)程崩潰稽煤。 不再用到的內(nèi)存酵熙,沒有及時(shí)釋放匾二,就叫做內(nèi)存泄漏拳芙。
常見的內(nèi)存泄露
1.意外的全局變量
function foo() {
bar = '全局變量'; // 沒有聲明變量 實(shí)際上是全局變量=>window.bar
}
foo();
function foo() {
const bar = 'foo變量'; // 沒有聲明變量 實(shí)際上是全局變量=>window.bar
}
foo();
2.定時(shí)器和回調(diào)函數(shù)
當(dāng)不需要setInterval或者setTimeout時(shí)分飞,定時(shí)器沒有被清除譬猫,定時(shí)器的回調(diào)函數(shù)以及內(nèi)部依賴的變量都不能被回收,造成內(nèi)存泄漏染服。
setInterval(function() {
// 執(zhí)行什么
}, 1000);
頁面卸載或者執(zhí)行完定時(shí)器需要主動清除
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出整個(gè)div 沒有被回收
refA = null;
console.log(refA, "refA"); // 解除引用
5.console保存大量數(shù)據(jù)在內(nèi)存中
過多的console摸袁,比如定時(shí)器的console會導(dǎo)致瀏覽器卡死靠汁。
合理利用console闽铐,線上項(xiàng)目盡量少的使用console兄墅。
內(nèi)存查看
-
瀏覽器方法
1.打開Chrome瀏覽器開發(fā)者工具的Performance面板
2.選項(xiàng)欄中勾選Memory選項(xiàng)
3.點(diǎn)擊左上角錄制按鈕(實(shí)心圓狀按鈕)
4.在頁面上進(jìn)行正常操作
5.一段時(shí)間后隙咸,點(diǎn)擊Stop五督,觀察面板上的數(shù)據(jù)
image.png
如果內(nèi)存占用基本平穩(wěn)充包,接近水平误证,就說明不存在內(nèi)存泄漏修壕。
反之遏考,就是內(nèi)存泄漏了灌具。
- 命令行方法
命令行可以使用 Node 提供的 process.memoryUsage 方法咖楣。
console.log(process.memoryUsage());
//{
//rss: 101568512,
//heapTotal: 72605696,
//heapUsed: 51070584,
//external: 5819790,
//arrayBuffers: 4286309
//}
process.memoryUsage返回一個(gè)對象诱贿,包含了 Node 進(jìn)程的內(nèi)存占用信息珠十。該對象包含四個(gè)字段,單位是字節(jié)晒杈,含義如下:
rss(resident set size):所有內(nèi)存占用拯钻,包括指令區(qū)和堆棧。
heapTotal:"堆"占用的內(nèi)存然磷,包括用到的和沒用到的姿搜。
heapUsed:用到的堆的部分舅柜。
external: V8 引擎內(nèi)部的 C++ 對象占用的內(nèi)存致份。
arrayBuffers: ArrayBuffer
和 SharedArrayBuffer
分配的內(nèi)存氮块,包括所有 Node.js Buffer
判斷內(nèi)存泄漏滔蝉,以heapUsed字段為準(zhǔn)塔沃。
參考:
https://juejin.cn/post/6844903801191661575#heading-13
http://www.ruanyifeng.com/blog/2017/04/memory-leak.html
https://juejin.cn/post/6844903801191661575#heading-9