內(nèi)存管理
內(nèi)存管理流程
- 申請(qǐng)內(nèi)存空間
- 使用內(nèi)存空間
- 釋放內(nèi)存空間
垃圾回收與常見(jiàn)GC算法
js中的垃圾
- js中的內(nèi)存管理是自動(dòng)的栏饮;
- 對(duì)象不再被引用時(shí)是垃圾;
- 對(duì)象不能從根上訪問(wèn)到時(shí)是垃圾。
js中的可達(dá)對(duì)象
- 可以訪問(wèn)到的對(duì)象就是可達(dá)對(duì)象(引用优炬、作用域鏈)颁井;
- 可達(dá)的標(biāo)準(zhǔn)就是從根出發(fā)是否能夠被找到;
- js中的根可以理解為全局變量對(duì)象蠢护。
當(dāng)變量對(duì)象不可達(dá)時(shí)雅宾,就會(huì)被視作垃圾,js引擎會(huì)自動(dòng)找到它并進(jìn)行垃圾回收糊余。
GC算法介紹
GC是垃圾回收機(jī)制的簡(jiǎn)寫(xiě)秀又,GC可以找到內(nèi)存中的垃圾、并釋放和回收空間贬芥。
算法是工作時(shí)查找和回收時(shí)所遵循的原則吐辙。
- GC中的垃圾是什么
- 程序中不再需要使用的對(duì)象;
- 程序中不能再訪問(wèn)到的對(duì)象蘸劈。
常見(jiàn)的GC算法
引用計(jì)數(shù)
通過(guò)引用計(jì)數(shù)器對(duì)對(duì)象進(jìn)行引用計(jì)數(shù)昏苏,引用關(guān)系改變時(shí)修改引用數(shù)字,當(dāng)引用數(shù)字為0時(shí)立即進(jìn)行回收威沫。
優(yōu)點(diǎn)
1.發(fā)現(xiàn)垃圾時(shí)立即回收贤惯;
2.最大限度減少程序暫停,減少程序卡頓時(shí)間棒掠。
缺點(diǎn)
1.無(wú)法回收循環(huán)引用的對(duì)象孵构;
2.時(shí)間開(kāi)銷(xiāo)大、資源消耗較大烟很。標(biāo)記清除
分為標(biāo)記階段和清除階段:首先遍歷所有對(duì)象標(biāo)記活動(dòng)對(duì)象(可達(dá)對(duì)象)颈墅,然后再次遍歷清除沒(méi)有標(biāo)記對(duì)象,回收相應(yīng)的空間雾袱,結(jié)束后還會(huì)清除所有標(biāo)記方便下次進(jìn)行GC恤筛。
優(yōu)點(diǎn)
相對(duì)于引用計(jì)數(shù)算法,可以回收循環(huán)引用的對(duì)象芹橡。
缺點(diǎn)
1.空間碎片化:當(dāng)前所回收的對(duì)象在地址上不連續(xù)毒坛,不能最大化地使用空間;
2.不會(huì)立即回收垃圾對(duì)象林说。標(biāo)記整理
可以看作是標(biāo)記清除的增強(qiáng)煎殷,標(biāo)記階段與標(biāo)記清除算法一致,清楚階段會(huì)先執(zhí)行整理腿箩,移動(dòng)對(duì)象的位置豪直。
優(yōu)點(diǎn)
減少碎片化空間。
缺點(diǎn)
不會(huì)立即回收垃圾對(duì)象度秘。分代回收
將內(nèi)存分為新生代、老生代,針對(duì)不同對(duì)象采用不同的算法剑梳。詳見(jiàn)V8的垃圾回收策略唆貌。
V8引擎的垃圾回收
V8是一款主流的js執(zhí)行引擎,采用即時(shí)編譯垢乙,內(nèi)存設(shè)限(1.對(duì)于瀏覽器來(lái)說(shuō)足夠使用锨咙;2.如果上限再大那么垃圾回收時(shí)間會(huì)超過(guò)用戶感知)。
V8的垃圾回收策略
采用分代回收的思想追逮。
V8中的常用GC算法
- 分代回收
- 空間復(fù)制
- 標(biāo)記清除
- 標(biāo)記整理
- 增量標(biāo)記
增量標(biāo)記
將原本需要一次性遍歷堆內(nèi)存的操作改為增量標(biāo)記的方式酪刀,先標(biāo)記內(nèi)存中的一部分對(duì)象然后暫停,將執(zhí)行權(quán)重新交給JS主線程钮孵,待主線程任務(wù)執(zhí)行完畢后再?gòu)脑瓉?lái)暫停標(biāo)記的地方繼續(xù)標(biāo)記骂倘,直到標(biāo)記完整個(gè)堆內(nèi)存。
V8如何回收新生代對(duì)象
新生代對(duì)象指的是存活時(shí)間較短的對(duì)象巴席。V8的內(nèi)存空間一分為二历涝,小空間用于存儲(chǔ)新生代對(duì)象(32M|16M)。
回收過(guò)程采用的是復(fù)制算法+標(biāo)記整理:
新生代內(nèi)存區(qū)被等分為兩個(gè)空間漾唉,使用空間為From存儲(chǔ)活動(dòng)對(duì)象荧库,空閑空間為T(mén)o。開(kāi)始進(jìn)行GC赵刑,會(huì)檢查From區(qū)中的活動(dòng)對(duì)象分衫,標(biāo)記整理后將活動(dòng)對(duì)象拷貝至To,清空(釋放)From區(qū)般此,最后將From和To互換蚪战。
細(xì)節(jié)說(shuō)明:
拷貝過(guò)程中可能出現(xiàn)晉升(將新生代對(duì)象移動(dòng)至老生代),情況為:
1.一輪GC后還存活的新生代需要晉升恤煞;
2.To空間的使用率超過(guò)25%屎勘。
V8如何回收老生代對(duì)象
老生代對(duì)象指的是存活時(shí)間較長(zhǎng)的對(duì)象。老生代對(duì)象存儲(chǔ)至右側(cè)的老生代區(qū)域(1.4G|700M)居扒。
回收過(guò)程主要采用標(biāo)記清除概漱、標(biāo)記整理和增量標(biāo)記:
首先使用標(biāo)記清除完成垃圾空間的回收,當(dāng)存在新生代對(duì)象晉升到老生代區(qū)域而由于空間碎片化導(dǎo)致空間不足時(shí)則需要采用標(biāo)記整理進(jìn)行空間優(yōu)化喜喂,采用增量標(biāo)記進(jìn)行效率優(yōu)化瓤摧。
新老生代對(duì)象垃圾回收對(duì)比
- 新生代區(qū)域垃圾回收使用空間換時(shí)間。在新生代內(nèi)存中玉吁,大部分對(duì)象的生命周期較短照弥,因此時(shí)間效率可觀;
- 老生代區(qū)域垃圾回收不適合復(fù)制算法进副。老生代內(nèi)存中可能會(huì)存儲(chǔ)大量對(duì)象这揣,如果再將空間一分為二為造成空間的大量浪費(fèi)悔常。
V8引擎執(zhí)行流程
預(yù)解析的優(yōu)點(diǎn)
- 跳過(guò)未被使用的代碼;
- 不生成AST给赞,創(chuàng)建無(wú)變量引用和聲明的scopes机打;
- 依據(jù)規(guī)范拋出特定錯(cuò)誤;
- 解析速度更快片迅。
全量解析
- 解析被使用的代碼残邀;
- 生成AST;
- 構(gòu)建具體scopes信息柑蛇,變量引用芥挣、聲明等;
- 拋出所有語(yǔ)法錯(cuò)誤耻台。
Performance工具
使用目的
Performance提供多種監(jiān)控方式空免,讓開(kāi)發(fā)者可以時(shí)刻關(guān)注當(dāng)前內(nèi)存的變化以確定當(dāng)前內(nèi)存空間的使用是否合理。
使用步驟
- 打開(kāi)瀏覽器輸入目標(biāo)網(wǎng)址粘我;
- 進(jìn)入開(kāi)發(fā)人員工具面板鼓蜒,選擇性能;
- 開(kāi)啟錄制功能征字,訪問(wèn)具體界面都弹;
- 執(zhí)行用戶行為,一段時(shí)間后停止錄制匙姜;
- 分析界面中記錄的內(nèi)存信息畅厢。
內(nèi)存問(wèn)題的體現(xiàn)
- 頁(yè)面出現(xiàn)延遲加載或經(jīng)常性暫停-存在頻繁的垃圾回收;
- 頁(yè)面持續(xù)性出現(xiàn)糟糕的性能-內(nèi)存膨脹氮昧;
- 頁(yè)面的性能隨時(shí)間延長(zhǎng)越來(lái)越差-內(nèi)存泄漏框杜。
監(jiān)控內(nèi)存的幾種方式
界定內(nèi)存問(wèn)題的標(biāo)準(zhǔn):
- 內(nèi)存泄漏:內(nèi)存使用持續(xù)升高;
- 內(nèi)存膨脹:在多數(shù)設(shè)備上都存在性能問(wèn)題袖肥;
- 頻繁垃圾回收:通過(guò)內(nèi)存變化圖進(jìn)行分析咪辱。
方式:
- 瀏覽器任務(wù)管理器;
- Timeline時(shí)序圖記錄椎组;
- 堆快照查找分離DOM油狂;
- 判斷是否存在頻繁的垃圾回收。
堆快照查找分離DOM
DOM存在的幾種狀態(tài)
- 界面元素存活在DOM樹(shù)上寸癌;
- 垃圾對(duì)象的DOM節(jié)點(diǎn)——DOM從DOM樹(shù)上脫離专筷,js里沒(méi)有引用;
- 分離狀態(tài)的DOM節(jié)點(diǎn)——DOM從DOM樹(shù)上脫離蒸苇,js里有引用磷蛹,界面上看不見(jiàn)但是占用內(nèi)存浪費(fèi)空間。
判斷是否存在頻繁GC
為什么需要確定存在頻繁GC溪烤?
- GC工作時(shí)應(yīng)用程序是停止的味咳;
- 頻繁且過(guò)長(zhǎng)的GC會(huì)導(dǎo)致應(yīng)用假死庇勃;
- 用戶使用中感知應(yīng)用卡頓。
確定方式
- Timeline中頻繁的上升下降槽驶;
- 任務(wù)管理器中數(shù)據(jù)頻繁的增加減小匪凉。
代碼優(yōu)化實(shí)例
堆棧處理
- JS執(zhí)行環(huán)境;
- 執(zhí)行環(huán)境棧(ECStack, execution context stack)捺檬;
- 執(zhí)行上下文;
- VO(G)贸铜,全局變量對(duì)象堡纬。
- 基本數(shù)據(jù)類型是按值進(jìn)行操作;
- 基本數(shù)據(jù)類型的值存放在棧區(qū)蒿秦;
- 無(wú)論是棧內(nèi)存還是后續(xù)引用數(shù)據(jù)類型會(huì)使用的堆內(nèi)存都屬于計(jì)算機(jī)內(nèi)存烤镐;
- GO(全局對(duì)象)。
引用類型堆棧處理
函數(shù)堆棧處理
- 創(chuàng)建函數(shù)和創(chuàng)建變量類似棍鳖,函數(shù)名此時(shí)就可以看作是一個(gè)變量名炮叶,存放在VO中;
- 單獨(dú)開(kāi)辟一個(gè)堆內(nèi)存用于存放函數(shù)體(字符串形式代碼)渡处,當(dāng)前內(nèi)存地址也會(huì)有一個(gè)16進(jìn)制數(shù)值地址镜悉;
- 創(chuàng)建函數(shù)的時(shí)候,它的作用域[[scope]]就已經(jīng)確定了(創(chuàng)建函數(shù)時(shí)所在的執(zhí)行上下文)医瘫;
- 創(chuàng)建函數(shù)之后會(huì)將它的內(nèi)存地址存放在棧區(qū)與對(duì)應(yīng)的函數(shù)名進(jìn)行關(guān)聯(lián)侣肄。
函數(shù)執(zhí)行,目的就是為了將函數(shù)對(duì)應(yīng)的堆內(nèi)存里的字符串形式代碼進(jìn)行執(zhí)行醇份。代碼在執(zhí)行的時(shí)候肯定需要有一個(gè)環(huán)境稼锅,此時(shí)就意味著函數(shù)在執(zhí)行的時(shí)候會(huì)生成一個(gè)新的執(zhí)行上下文來(lái)管理函數(shù)體中的代碼。
函數(shù)執(zhí)行時(shí)做的事情:
- 確定作用域鏈:<當(dāng)前執(zhí)行上下文僚纷、上級(jí)執(zhí)行上下文>矩距;
- 確定this指向;
- 初始化arguments對(duì)象怖竭;
- 形參賦值:它相當(dāng)于是變量聲明锥债,然后將聲明的變量放置于AO;
- 變量提升侵状;
- 執(zhí)行代碼赞弥。
閉包堆棧處理
函數(shù)執(zhí)行時(shí)創(chuàng)建的私有執(zhí)行上下文當(dāng)中引用的一個(gè)堆被外部的執(zhí)行上下文中的變量所引用,因此當(dāng)函數(shù)執(zhí)行完后趣兄,該私有執(zhí)行上下文就不能被釋放绽左。
- 閉包是一種機(jī)制,通過(guò)私有上下文來(lái)保護(hù)當(dāng)中變量的機(jī)制艇潭;
- 也可以認(rèn)為當(dāng)創(chuàng)建的某一個(gè)執(zhí)行上下文不被釋放的時(shí)候形成了閉包拼窥;
- 保護(hù)戏蔑、保存數(shù)據(jù)。
閉包與垃圾回收
- 瀏覽器都自有垃圾回收機(jī)制(內(nèi)存管理鲁纠,V8為例)总棵;
- 棧空間改含、堆空間情龄;
- 堆:當(dāng)前堆內(nèi)存如果被占用,就能被釋放掉捍壤,但是如果確認(rèn)后續(xù)不再使用這個(gè)內(nèi)存中的數(shù)據(jù)骤视,可以自己主動(dòng)置空,然后瀏覽器就會(huì)對(duì)其進(jìn)行回收鹃觉;
- 棧:當(dāng)前上下文中是否有內(nèi)容专酗,被其他上下文的變量所占用,如果有則無(wú)法釋放(閉包)盗扇。
循環(huán)添加事件實(shí)現(xiàn)
- 閉包
- 自定義屬性
- 事件委托
變量局部化
可以提交代碼的執(zhí)行效率(減少了數(shù)據(jù)訪問(wèn)時(shí)需要查找的路徑)祷肯。
緩存數(shù)據(jù)
對(duì)于需要多次使用的數(shù)據(jù)進(jìn)行提前保存,后續(xù)進(jìn)行使用疗隶。
- 減少聲明和語(yǔ)句數(shù)(詞法 語(yǔ)法)佑笋;
- 緩存數(shù)據(jù)(作用域鏈查找變快)。
減少訪問(wèn)層級(jí)
防抖與節(jié)流
在一些高頻率事件觸發(fā)的場(chǎng)景下我們不希望對(duì)應(yīng)的事件處理函數(shù)多次執(zhí)行斑鼻。
常見(jiàn)的應(yīng)用場(chǎng)景
- 滾動(dòng)事件
- 輸入的模糊匹配
- 輪播圖切換
- 點(diǎn)擊操作
- ...
瀏覽器默認(rèn)情況下都會(huì)有自己的監(jiān)聽(tīng)事件間隔(4-6ms)允青,如果檢測(cè)到多次事件的監(jiān)聽(tīng)執(zhí)行,那么就會(huì)造成不必要的資源浪費(fèi)卵沉。
前置場(chǎng)景:界面上有一個(gè)按鈕可以多次點(diǎn)擊颠锉。
防抖:對(duì)于這個(gè)高頻的操作來(lái)說(shuō),我們只希望識(shí)別一次點(diǎn)擊史汗,可以認(rèn)為是第一次或者最后一次琼掠;
節(jié)流:對(duì)于高頻操作,我們可以自己來(lái)設(shè)置頻率停撞,讓本來(lái)會(huì)執(zhí)行很多次的事件觸發(fā)瓷蛙,按照我們定義的頻率減少觸發(fā)的次數(shù)。
防抖函數(shù)實(shí)現(xiàn)
// 防抖函數(shù)實(shí)現(xiàn)
/**
* handle 最終需要執(zhí)行的事件監(jiān)聽(tīng)
* wait 事件觸發(fā)多久后開(kāi)始執(zhí)行
* immidiate 控制執(zhí)行第一次還是最后一次戈毒,false執(zhí)行最后一次
*/
function myDebounce(handle, wait, immidiate) {
// 參數(shù)類型判斷及默認(rèn)值處理
if(typeof handle !== function) throw new Error('handle must be a function')
if(typeof wait === 'undefined') wait = 300
if(typeof wait === 'boolean') {
immidiate = wait
wait = 300
}
if(typeof immidiate !== 'boolean') immidiate = false
// 所謂的防抖效果就是有一個(gè)“人”管理handle的執(zhí)行次數(shù)
// 如果我們想要執(zhí)行最后一次艰猬,那就意味著無(wú)論我們點(diǎn)擊了多少次,前面的N-1次都無(wú)用
let timer = null
return function proxy() {
clearTimeout(timer)
timer = setTimeout(()=>{
handle()
}, wait)
}
}
節(jié)流函數(shù)實(shí)現(xiàn)
在自定義的一段時(shí)間內(nèi)讓事件觸發(fā)
// 節(jié)流函數(shù)實(shí)現(xiàn)
function myThottle(handle, wait) {
// 參數(shù)類型判斷及默認(rèn)值處理
if(typeof handle !== function) throw new Error('handle must be a function')
if(typeof wait === 'undefined') wait = 500
let previous = 0
let timer = null
return function proxy(...args) {
let now = new Date() // 定義變量記錄當(dāng)前執(zhí)行的時(shí)間點(diǎn)
let self = this
let interval = wait - (now - previous)
// 非高頻次操作埋市,執(zhí)行
if(interval <= 0) {
// 處理臨界情況
clearTimeout(timer)
timer = null
handle.call(self, ...args)
previous = new Date()
} else if(!timer) {
// 當(dāng)系統(tǒng)中沒(méi)有定時(shí)器冠桃,自定義一個(gè)定時(shí)器讓handle在interval之后執(zhí)行
timer = setTimeout(() => {
clearTimeout(timer) // 清除系統(tǒng)中的定時(shí)器,但是timer中的值還在
timer = null
handle.call(self, ...args)
previous = new Date()
}, interval);
}
}
}
減少判斷層級(jí)
如果出現(xiàn)if多層嵌套的情況道宅,考慮將其改為使用提前return的操作食听。
減少循環(huán)體活動(dòng)
減少對(duì)象成員及數(shù)組項(xiàng)的查找次數(shù)胸蛛。每次循環(huán)都要查找item.length,這樣做很耗時(shí)樱报,由于該值在循環(huán)運(yùn)行過(guò)程中從未變過(guò)葬项,因此產(chǎn)生了不必要的性能損失,提高整個(gè)循環(huán)的性能很簡(jiǎn)單迹蛤,只查找一次屬性民珍,并把值存儲(chǔ)到一個(gè)局部變量中,然后在控制條件中使用整個(gè)變量盗飒。
字面量與構(gòu)造式
盡量用對(duì)象字面量的方式來(lái)創(chuàng)建對(duì)象穷缤。
字面量的優(yōu)勢(shì):
- 它的代碼量更少,更易讀箩兽;
- 對(duì)象字面量運(yùn)行速度更快,因?yàn)樗鼈兛梢栽诮馕龅臅r(shí)候被優(yōu)化章喉。