JS前端的內(nèi)存處理的方法全面詳解
一、內(nèi)存的儲(chǔ)存和代碼執(zhí)行的場(chǎng)所關(guān)系
對(duì)于任何語言來說,內(nèi)存管理、垃圾回收等知識(shí)都是進(jìn)階路上繞不開的坎。出來面試估計(jì)也沒少被問到“前端的內(nèi)存處理你了解么? 你知道js中的垃圾處理機(jī)制嗎? 什么情況會(huì)導(dǎo)致內(nèi)存泄漏呢?
1. 儲(chǔ)存空間
兩種結(jié)構(gòu):
- ??臻g:
- 存儲(chǔ)原始類型
- 執(zhí)行上下文 (代碼空間:主要存儲(chǔ)可執(zhí)行代碼)
- 堆空間:存儲(chǔ)引用類型
為什么不都用棧存呢?
因?yàn)樾枰脳砭S護(hù)程序執(zhí)行期間上下文的狀態(tài),如果??臻g大了話,所有的數(shù)據(jù)都存放在??臻g里面,那么會(huì)影響到上下文切換的效率,進(jìn)而又影響到整個(gè)程序的執(zhí)行效率。
2. 內(nèi)存的生命周期
內(nèi)存分配:聲明變量、函數(shù)、對(duì)象的時(shí)候 內(nèi)存的使用:讀寫內(nèi)存,使用變量 函數(shù)等 內(nèi)存回收:使用完畢,由垃圾回收機(jī)制自動(dòng)回收不再使用的內(nèi)存
3. js 中的內(nèi)存分配和使用
// 分配 const num = 123; // 分配到棧 const str = 'sss';// 分配到棧 const obj = {}; // 分配到堆 // 使用 const a = 10; console.log(a); // 使用
4. 調(diào)用棧下移ESP(記錄當(dāng)前執(zhí)行狀態(tài)的指針)
當(dāng)一個(gè)函數(shù)執(zhí)行結(jié)束之后,JavaScript 引擎會(huì)通過向下移動(dòng) ESP 來銷毀該函數(shù)保存在棧中的執(zhí)行上下文。 比方下面這個(gè)例子通過ESP 狀態(tài)來展示就如圖所示。
var foo1 = () => { console.log('foo1') foo2() } var foo2 = () => { console.log('foo2') } foo1()
被ESP指針移開后的函數(shù)作用域foo1 明顯屬于不在被引用,后續(xù)將會(huì)直接被GC回收
二、 js中的垃圾回收機(jī)制
有C語言經(jīng)驗(yàn)的開發(fā)者,一定明白內(nèi)存聲明分配好之后,需要手動(dòng) free
的操作,這就是手動(dòng)回收。而 Js 本身是自動(dòng)回收機(jī)制,所以開發(fā)者不需要過多關(guān)注內(nèi)存分配和釋放的問題。這些工作都讓 V8 引擎中的垃圾回收器(GC)給承包了。
最早接觸 js 的時(shí)候,市面上對(duì)于 js 內(nèi)存管理、垃圾回收主要講的是下面兩種概念:
1. 引用計(jì)數(shù)法
引用計(jì)數(shù)法的算法主要依賴于引用的概念,這個(gè)回收機(jī)制最早是在 IE 在使用的。目前主流瀏覽器都使用標(biāo)記清除法了??匆粋€(gè)對(duì)象是否有指向他的引用,如果沒有其他對(duì)象指向他了,說明當(dāng)前這個(gè)對(duì)象不再被需要了。
他的缺陷在于:循環(huán)引用
如果兩個(gè)對(duì)象相互引用,盡管他們已不再被使用,但是引用計(jì)數(shù)無法識(shí)別,導(dǎo)致內(nèi)存泄漏。
2. 標(biāo)記清除法(Mark-Sweep)
將“不再使用的的對(duì)象”定義為“無法到達(dá)的對(duì)象”
從根部js的全局對(duì)象觸發(fā),定時(shí)遞歸掃描內(nèi)存中的對(duì)象,凡是無法從根部到達(dá)的對(duì)象,就會(huì)被標(biāo)記為不再使用,稍后進(jìn)行回收。
執(zhí)行過程如下:
- GC在運(yùn)行的時(shí)候會(huì)給內(nèi)存中的所有變量都加上標(biāo)記
- 將從根部觸發(fā)能夠觸及到的對(duì)象標(biāo)記清除
- 剩下的還有標(biāo)記的變量被視為準(zhǔn)備刪除的變量
- GC銷毀帶有標(biāo)記的值 回收內(nèi)存空間
三、代際假說和分代收集
代際假說(The Generational Hypothesis)是現(xiàn)代瀏覽器垃圾回收策略的基礎(chǔ)。整個(gè)模型可以看看下圖
新生代(副垃圾回收器)
存放的是生存時(shí)間短、占用空間較小的的對(duì)象,通過 Scavenge
算法,是把新生代空間對(duì)半劃分為兩個(gè)區(qū)域,一半是對(duì)象區(qū)域,一半是空閑區(qū)域。新的對(duì)象都要放到對(duì)象區(qū),當(dāng)快滿的時(shí)候,將還存活的對(duì)象復(fù)制到空閑區(qū)后進(jìn)行角色互換。并且執(zhí)行對(duì)象晉升策略,對(duì)象區(qū)域和空閑區(qū)域翻轉(zhuǎn)兩次還存在的對(duì)象,升級(jí)到老生代。這個(gè)復(fù)制翻轉(zhuǎn)的過程也避免內(nèi)存碎片的產(chǎn)生。
老生代(主垃圾回收器)
存放的生存時(shí)間久的對(duì)象或者大的對(duì)象,使用標(biāo)記清除的算法進(jìn)行垃圾回收。一旦執(zhí)行垃圾回收算法,都需要將正在執(zhí)行的 JavaScript 腳本暫停下來,待垃圾回收完畢后再恢復(fù)腳本執(zhí)行。這種行為稱為全停頓(Stop-The-World)。實(shí)際上瀏覽器為了避免垃圾回收卡頓通過增量標(biāo)記方式將回收任務(wù)拆解成多個(gè)小任務(wù)穿插在js主線程中執(zhí)行。
四、常見的內(nèi)存泄漏
1. 全局變量
function foo() { bar1 = 'aaa'; // 相當(dāng)于聲明在window.bar1 this.bar2 = 'aaaa' } foo(); // 執(zhí)行函數(shù)事this指向window ,相當(dāng)于一個(gè)函數(shù)給全局變量增加了兩個(gè)變量
2. 未被清理的定時(shí)器和回調(diào)函數(shù)
//setInterval //setTimeout setInterval(() => { console.log('test') }, 500) //沒用用 clearInterval clearTimeout 做清除
3. 閉包
個(gè)人最喜歡《你不知道的js》里對(duì)閉包的描述
一個(gè)內(nèi)部函數(shù),有權(quán)訪問包含其的外部函數(shù)的變量 —— 《你不知道的js》 或者也可以用“內(nèi)存逃逸”這種高逼格的屬于形容。
// 閉包 gc 案例 var one = null; var replace = function() { var originalOne = one; var unused =function() { if(originalOne) { console.log(111); } } one = { longString: '111', method: function() { console.log() } } } setInterval(replace, 500)
每次調(diào)用 replace
, one
得到一個(gè)包含字符串和一個(gè)對(duì)于新閉包 method
的對(duì)象 unused
引用了 originOne
5. DOM 引用
var elements= { image: document.getElementById('111'); } elements.image = null;
6. 怎么避免呢?
- 盡量減少全局變量
//盡可能少寫 window.object = {} // 這類代碼
- 使用完引用數(shù)據(jù)后,及時(shí)解除引用.null
let obj = {} ... obj = null;
- 避免死循環(huán)等持續(xù)執(zhí)行的操作(例如 邊界判斷不清晰的 for 或 while 循環(huán))
- 多使用 WeakSet / WeakMap 特性
// Vue3 中就大量用了WeakMap 優(yōu)化實(shí)例引用 const bucket = new WeakMap(); ... const obj = new Proxy(data, { get(target, key) { track(target, key); return target[key]; }, set(target, key, newVal) { target[key] = newVal; trigger(target, key); }, }); function track(target, key) { if (!activeEffect) return; let depsMap = bucket.get(target); if (!depsMap) { bucket.set(target, (depsMap = new Map())); } // 再根據(jù)key 從 depsMap 中取得 deps, 它是一個(gè) Set 類型,里面存儲(chǔ)著左右與當(dāng)前 key 相關(guān)聯(lián)的副作用函數(shù): effects let deps = depsMap.get(key); // 如果 deps 不存在,同樣新建一個(gè) Set 并與 key 關(guān)聯(lián) if (!deps) { depsMap.set(key, (deps = new Set())); } deps.add(activeEffect); activeEffect.deps.push(deps); } ...
參考
【1】 《瀏覽器工作原理與實(shí)踐》
【2】 一文搞懂垃圾回收
以上就是JS前端的內(nèi)存處理的方法全面詳解的詳細(xì)內(nèi)容,更多關(guān)于JS前端內(nèi)存處理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用typescript類型實(shí)現(xiàn)ThreeSum
這篇文章主要介紹了使用typescript類型實(shí)現(xiàn)ThreeSum,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以一下,希望對(duì)你學(xué)習(xí)又是幫助2022-08-08對(duì)象無length屬性時(shí)IE6/IE7中無法將其轉(zhuǎn)換成偽數(shù)組(ArrayLike)
對(duì)象無length屬性時(shí)IE6/7中無法將其轉(zhuǎn)換成偽數(shù)組(ArrayLike) 的解決方法,需要的朋友可以參考下。2011-07-07Bootstrap基本布局實(shí)現(xiàn)方法詳解
這篇文章主要為大家詳細(xì)介紹了Bootstrap基本布局實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11JavaScript實(shí)現(xiàn)拖拽排序的方法詳解
可拖拽排序的菜單效果大家想必都很熟悉,本次我們通過一個(gè)可拖拽排序的九宮格案例來演示其實(shí)現(xiàn)原理,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-05-05原生javascript實(shí)現(xiàn)獲取指定元素下所有后代元素的方法
這篇文章主要介紹了原生javascript實(shí)現(xiàn)獲取指定元素下所有后代元素的方法,在進(jìn)行web程序設(shè)計(jì)時(shí)是非常實(shí)用的技巧,需要的朋友可以參考下2014-10-10js實(shí)現(xiàn)iframe跨頁面調(diào)用函數(shù)的方法
這篇文章主要介紹了js實(shí)現(xiàn)iframe跨頁面調(diào)用函數(shù)的方法,實(shí)例展示了iframe中父頁面調(diào)用子頁面和子頁面調(diào)用父頁面的實(shí)現(xiàn)技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2014-12-12點(diǎn)擊按鈕彈出模態(tài)框的一系列操作代碼實(shí)例
這篇文章主要介紹了js彈出模態(tài)框方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03javascript實(shí)現(xiàn)類似于新浪微博搜索框彈出效果的方法
這篇文章主要介紹了javascript實(shí)現(xiàn)類似于新浪微博搜索框彈出效果的方法,涉及javascript彈出搜索框的相關(guān)實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07JavaScript DSL 流暢接口(使用鏈?zhǔn)秸{(diào)用)實(shí)例
這篇文章主要介紹了JavaScript DSL 流暢接口(使用鏈?zhǔn)秸{(diào)用)實(shí)例,本文講解了DSL 流暢接口、DSL 表達(dá)式生成器等內(nèi)容,需要的朋友可以參考下2015-03-03