說明: 具體從內(nèi)存空間的使用以及垃圾回收機(jī)制的角度出發(fā)过蹂。
內(nèi)存管理
-
為什么要進(jìn)行內(nèi)存管理
內(nèi)存.png
內(nèi)存管理是可以避免程序出現(xiàn)一些不可察覺的內(nèi)存問題,比如內(nèi)存泄漏糊闽,當(dāng)這些問題反復(fù)出現(xiàn)在代碼中就會有意想不到的bug
- 內(nèi)存管理
- 內(nèi)存:可讀寫的單員組成邢锯,表示一片操作空間
- 管理:操作一片空間的申請,使用辜妓,釋放
申請空間-使用空間-釋放空間
// 申請空間
let obj = { name: 'glh' };
// 使用空間
let ali = obj;
// 釋放空間
obj = null;
JavaScript中的垃圾
- JavaScript 中的內(nèi)存管理是自動的
- 對象不再被引用時(shí)被看作是垃圾
- 對象不能從根上訪問
可達(dá)對象
- 從跟上出發(fā)可以訪問到的對象就是可達(dá)對象(引用叁幢,作用域鏈)
- JavaScript的根可以理解為全局變量
GC 算法
- GC 就是垃圾回收機(jī)制的簡寫
- GC可以找到內(nèi)存中的垃圾诉濒,并釋放和回收空間
所謂的GC算法就是垃圾回收器工作的時(shí)候查找和回收所遵循的規(guī)則
常見的GC算法
- 引用計(jì)數(shù)
- 標(biāo)記清除
- 標(biāo)記整理
- 分代回收
引用計(jì)數(shù)
在內(nèi)部維護(hù)一個引用計(jì)數(shù)器秽澳,給每個對象設(shè)置引用數(shù)贷币,當(dāng)引用關(guān)系發(fā)生變化時(shí)须蜗,判斷當(dāng)前對象引用數(shù)是否為 0积糯,當(dāng)為 0 時(shí)候榜苫,當(dāng)前對象為垃圾對象跌帐,被 GC 回收且釋放內(nèi)存空間
優(yōu)點(diǎn):1首懈,發(fā)現(xiàn)垃圾對象,立即回收谨敛;2究履,最大限度減少程序暫停。
缺點(diǎn):1脸狸,無法回收循環(huán)引用的對象最仑;2,時(shí)間開銷大
function(){
const obj1 = {}
const obj2 = {}
obj1.name = obj2
obj2.name = obj1
return ''
}
f()
以上代碼在采用引用計(jì)數(shù)算法進(jìn)行垃圾回收時(shí)炊甲,并不會對obj1和obj2進(jìn)行回收泥彤,因?yàn)榛ハ啻嬖谝?/p>
如果一個對象的引用數(shù)為 0,則該對象將被回收蜜葱。程序運(yùn)行時(shí)全景,對內(nèi)存的消耗除邏輯代碼外,也包括了 GC 算法的消耗牵囤,引用計(jì)數(shù)固然可以在程序出現(xiàn)垃圾的時(shí)候可以及時(shí)回收釋放內(nèi)存爸黄,也因內(nèi)存可以不斷得到釋放而減少了程序暫停的時(shí)間,但是 GC 同時(shí)也需要維護(hù)‘roots’表來統(tǒng)計(jì)引用計(jì)數(shù)揭鳞,當(dāng)代碼中引用較多時(shí)炕贵,也會帶來損耗。同時(shí)對于 對象之間互有引用的情況野崇,即使對象本身沒有被使用称开,但是引用存在就導(dǎo)致了引用計(jì)數(shù)不為 0,無法被回收的情況乓梨。
標(biāo)記清除算法
- 將標(biāo)記和清除分為兩個階段
- 遍歷所有對象找到所有活動對象標(biāo)記
- 遍歷所有對象清除沒有標(biāo)記的對象
- 回收相應(yīng)的空間
標(biāo)記清除算法會遞歸的尋找對象之間的引用獲取所有可達(dá)對象鳖轰,并為其做上標(biāo)記, 但是標(biāo)記清除算法所回收的內(nèi)存地址在內(nèi)存上不一定是連續(xù)的,這就導(dǎo)致了內(nèi)存空間的碎片化(類似磁盤碎片), 浪費(fèi)空間扶镀,并且標(biāo)記清除法是不會立即回收對象的蕴侣,而且當(dāng)標(biāo)記清除算法運(yùn)行的時(shí)候,程序會被暫停
優(yōu)點(diǎn):循環(huán)引用的對象也會被回收 缺點(diǎn):不會立即回收垃圾對象
標(biāo)記整理算法
標(biāo)記整理算法增強(qiáng)了標(biāo)記清除算法臭觉,遍歷所有內(nèi)存對象昆雀,將所有可達(dá)內(nèi)存對象標(biāo)記辱志,其在回收非活動對象前會將對象的地址進(jìn)行移動,使其在地址上連續(xù)狞膘,然后再回收揩懒。
優(yōu)點(diǎn):不會存在內(nèi)存空間的碎片化 缺點(diǎn):不會立即回收垃圾對象
V8 簡介
- V8引擎是一款主流的JavaScript執(zhí)行引擎
- 采用即使編輯,代碼可以直接轉(zhuǎn)成機(jī)器碼運(yùn)行
- V8 的運(yùn)行內(nèi)存上限是 1.5G(32 位系統(tǒng)為 800M)
由于 V8 的運(yùn)行內(nèi)存是有上限的挽封,因此垃圾回收需要使用分代回收算法已球,然后針對于新/老生代對象采取不同的 GC 算法
V8中常用的GC算法
- 分代回收
- 空間復(fù)制
- 標(biāo)記清除
- 標(biāo)記整理
- 標(biāo)記增量
V8的垃圾回收策略
- V8 內(nèi)存一分為二(新生代,老生代)
-
針對不同代對象采用不同GC算法
回收策略.png
新生代對象的回收操作
小空間用于存儲新聲代對象 32M(32 位為 16M)
新生代對象指存活時(shí)間較短對象(局部作用域的對象辅愿,經(jīng)過一輪 GC 就會被回收的對象)
新生代對象采用采用復(fù)制算法 + 標(biāo)記整理進(jìn)行回收
新生代內(nèi)存區(qū)同樣會被一分為二(等大小
From & To
)活動對象存儲在
From
空間內(nèi)和悦,當(dāng) From 空間應(yīng)用到一定大小的時(shí)候就會觸發(fā)GC操作使用標(biāo)記整理并整理活動對象的地址,使其連續(xù)然后將活動對象拷貝至 To渠缕,然后 From 空間進(jìn)行內(nèi)存釋放鸽素。當(dāng)完成一次 GC 操作之后,F(xiàn)rom 和 To 需要進(jìn)行置換亦鳞。
細(xì)節(jié)說明:
-
在從
From
到To
拷貝的過程中有可能出現(xiàn)變量晉升的情況馍忽,變量晉升就是新生代的對象移動到老生代。晉升條件:
一輪 GC 執(zhí)行完畢之后還存活的新生代則需要晉升燕差。
當(dāng) To 空間的使用率超過 25%的時(shí)候遭笋,同樣需要將此次的活動對象均移動到老生代中。
老生代對象的回收操作
老生代區(qū)域大小約 1.4G(32 位大小為 700M)徒探,老生代存放的對象為存活時(shí)間較長的對象瓦呼,一般為 window 下的變量或被閉包保存的變量。
老生代區(qū)域采用標(biāo)記清除测暗,標(biāo)記整理央串,增量標(biāo)記的 GC 算法。首先使用標(biāo)記清除完成對垃圾空間的回收碗啄,當(dāng)新生代區(qū)域出現(xiàn)晉升現(xiàn)象時(shí)质和,如果老生代空間不足,則會使用標(biāo)記整理進(jìn)行空間優(yōu)化稚字。同時(shí)在老生代變量進(jìn)行標(biāo)記的時(shí)候也會采用增量標(biāo)記算法進(jìn)行效率優(yōu)化饲宿。
增量標(biāo)記是對標(biāo)記清除算法的優(yōu)化,讓其不會一口氣的去尋找到所有活動對象胆描。而是會穿插在程序的運(yùn)行中執(zhí)行瘫想,降低了程序的卡頓,當(dāng)標(biāo)記徹底采集完畢之后昌讲,才會把程序停下來国夜,進(jìn)行垃圾回收。
新老生代垃圾回收細(xì)節(jié)對比
新生代區(qū)域剧蚣,采用復(fù)制算法支竹, 因此其每時(shí)每刻內(nèi)部都有空閑空間的存在(為了完成 From 到 To 的對象復(fù)制),但是新生代區(qū)域空間較小(32M)且被一分為二鸠按,所以這種空間上的浪費(fèi)也是比較微不足道的礼搁。
老生代因其空間較大(1.4G),如果同樣采用一分為二的做法則對空間大小是比較浪費(fèi),且老生代空間較大目尖,存放對對象也較多馒吴,如果進(jìn)行復(fù)制算法,則其消耗對時(shí)間也會更大瑟曲。也就是是否使用復(fù)制算法來進(jìn)行垃圾回收饮戳,是一個時(shí)間 T 關(guān)于內(nèi)存大小的關(guān)系,當(dāng)內(nèi)存較小時(shí)洞拨,使用復(fù)制算法消耗的時(shí)間是比較短的扯罐,而當(dāng)內(nèi)存較大時(shí),采用復(fù)制算法對時(shí)間對消耗也就更大烦衣。
內(nèi)存問題的外在表現(xiàn)
- 頁面出現(xiàn)延遲加載或經(jīng)常性暫停: 可能存在頻繁當(dāng)GC操作,存在一些代碼瞬間吃滿了內(nèi)存歹河。
- 頁面出現(xiàn)持續(xù)性的糟糕性能: 程序?yàn)榱诉_(dá)到最優(yōu)的運(yùn)行速度,向內(nèi)存申請了一片較大的內(nèi)存空間花吟,但空間大小超過了設(shè)備所能提供的大小秸歧。
- 頁面使用隨著時(shí)間延長越來越卡: 可能存在內(nèi)存泄漏。
使用任務(wù)管理器查看內(nèi)存
喚起瀏覽器自帶的任務(wù)管理器衅澈,觀察 js 內(nèi)存键菱,如果 js 內(nèi)存在持續(xù)增大,則存在內(nèi)存問題今布。
Timeline 記錄內(nèi)存
打開 Timeline 開始錄制经备,進(jìn)行頁面操作,結(jié)束錄制之后部默,開啟內(nèi)存勾選弄喘,拖動截圖到指定時(shí)間段查看發(fā)生內(nèi)存問題時(shí)候到頁面展示,并定位問題甩牺。同時(shí)可以查看對應(yīng)出現(xiàn)紅點(diǎn)到執(zhí)行腳本蘑志,定位問題代碼。
利用瀏覽器控制臺內(nèi)存模塊贬派,查找分離 dom
在頁面上進(jìn)行相關(guān)操作后急但,進(jìn)行“拍照”,在快照中查找Detached HTMLElement,回到代碼中查找對應(yīng)的分離 dom 存在的代碼搞乏,在相關(guān)操作代碼之后波桩,對分離 dom 進(jìn)行釋放,防止內(nèi)存泄漏请敦。
如何確定頻繁的垃圾回收操作
- GC 工作時(shí)镐躲,程序是暫停的储玫,頻繁/過長的 GC 會導(dǎo)致程序假死,用戶會感知到卡頓萤皂。
- 查看 Timeline 中是否存在內(nèi)存走向在短時(shí)間內(nèi)頻繁上升下降的區(qū)域撒穷。瀏覽器任務(wù)管理器是否頻繁的增加減少。
代碼優(yōu)化
慎用全局變量
全局變量定義在全局執(zhí)行的上下文,是所有作用域鏈的頂端
全局執(zhí)行上下文一直存在于上下文執(zhí)行棧裆熙,直到程序退出
如果某個局部作用域出現(xiàn)了同名變量則會屏蔽或者污染全局作用域
全局變量的執(zhí)行速度端礼,訪問速度要低于局部變量,因此對于一些需要經(jīng)常訪問的全局變量可以在局部作用域中進(jìn)行緩存
一些性能上的小問題
通過原型對象添加方法與直接在對象上添加成員方法相比入录,原型對象上的屬性訪問速度較快蛤奥。
當(dāng)回調(diào)函數(shù)可以單獨(dú)抽離的時(shí)候,其執(zhí)行速度要較快僚稿。
直接訪問屬性凡桥,會比通過方法訪問屬性速度來的快。
loop 遍歷速度 forEach > 優(yōu)化 for > for in
節(jié)點(diǎn)克隆(cloneNode)生成節(jié)點(diǎn)速度要快于創(chuàng)建節(jié)點(diǎn)蚀同。
字面量聲明的數(shù)據(jù)生成速度要快于單獨(dú)屬性賦值行為生成的數(shù)據(jù)唬血。
......