前端為什么要關(guān)注內(nèi)存
- 防止占用內(nèi)存過大超燃,造成頁面卡頓芍瑞,甚至無響應(yīng)
- Node.js 使用 V8 引擎,內(nèi)存管理對于服務(wù)端至關(guān)重要皮胡,因為服務(wù)端的持久性痴颊,內(nèi)存更容易積累造成內(nèi)存溢出
js 垃圾回收機制
js 使用垃圾回收機制自動管理內(nèi)存,這種方式的利弊都很明顯屡贺。
- 優(yōu)勢: 可以大幅簡化程序中都內(nèi)存管理代碼蠢棱,減輕開發(fā)者的負擔,同時也減少長時間運轉(zhuǎn)造成的內(nèi)存泄漏問題
- 劣勢: 意味著開發(fā)者無法掌控內(nèi)存管理甩栈,我們無法強迫其進行垃圾回收泻仙,進行管理
下面簡單介紹一下 js 的幾種垃圾回收策略:
引用計數(shù)
主要是IE8 以下的瀏覽器使用,現(xiàn)代瀏覽器都棄用了這種方式量没,這里只做簡單介紹玉转。
基本原理就是,記錄跟蹤每個值被引用的次數(shù)殴蹄,被引用一次被引用次數(shù)就加一究抓,被釋放就減一,為零時饶套,就釋放改值所占內(nèi)存漩蟆。
標記清除
主流瀏覽器使用垃圾回收機制。
當變量進入環(huán)境(例如妓蛮,在函 數(shù)中聲明一個變量)時怠李,就將這個變量標記為“進入環(huán)境”。從邏輯上講,永遠不能釋放進入環(huán)境的變量所占用的內(nèi)存捺癞,因為只要執(zhí)行流進入相應(yīng)的環(huán)境夷蚊,就可能會用到它們。
而當變量離開環(huán)境時髓介,則將其 標記為“離開環(huán)境”惕鼓。 可以使用任何方式來標記變量。比如唐础,可以通過翻轉(zhuǎn)某個特殊的位來記錄一個變量何時進入環(huán)境箱歧, 或者使用一個“進入環(huán)境的”變量列表及一個“離開環(huán)境的”變量列表來跟蹤哪個變量發(fā)生了變化。
function test(){
var a = 10; //被標記"進入環(huán)境"
var b = "hello"; //被標記"進入環(huán)境"
}
test(); // 執(zhí)行完畢后之后一膨,a和b又被標記"離開環(huán)境"呀邢,被回收
說到底,如何標記變量其實并不重要豹绪,關(guān)鍵在于采取什么策略价淌。 垃圾收集器在運行的時候會給存儲在內(nèi)存中的所有變量都加上標記(當然,可以使用任何標記方式)瞒津。
然后蝉衣,它會去掉環(huán)境中的變量以及被環(huán)境中的變量引用的變量的標記。而在此之后再被加上標記 的變量將被視為準備刪除的變量巷蚪,原因是環(huán)境中的變量已經(jīng)無法訪問到這些變量了病毡。
最后,垃圾收集器完成內(nèi)存清除工作屁柏,銷毀那些帶標記的值并回收它們所占用的內(nèi)存空間剪验。
環(huán)境可以理解為我們的作用域,但是全局作用域的變量只會在頁面關(guān)閉才會銷毀前联。
V8 內(nèi)存控制
V8 的內(nèi)存限制
在 Node 中只能使用部分內(nèi)存,64位系統(tǒng)下約為1.4GB娶眷,32位系統(tǒng)下約為0.7GB似嗤,在這種限制下,將會導致 Node 無法操作大內(nèi)存對象届宠,比如將一個 2GB 的文件讀入內(nèi)存烁落,即使物理內(nèi)存有64 GB,也沒有辦法完成豌注,這個時候我們可以使用 Buffer 類伤塌,來完成大內(nèi)存文件的讀取。
造成這個問題的主要原因: Node 基于 V8 構(gòu)建轧铁,而 V8 的這套內(nèi)存管理機制主要是在瀏覽器中使用每聪,完全可以滿足前端頁面中的所有需求,但是在 Node 中卻限制了開發(fā)者隨心所欲使用大內(nèi)存的想法。
V8 的垃圾回收機制
V8 的垃圾回收策略主要基于分代式垃圾回收機制药薯。主要將內(nèi)存分為新生代和老生代兩代绑洛。
新生代空間(Young Generaion)
特點:
- 管理對象存活時間較短
- 占用空間比老生代空間小很多
- 垃圾回特別頻繁
新生代空間的垃圾回收采用Scavenge 算法,其工作原理如下:
- 將新生代空間分為兩個空間童本,稱為semispace真屯,處于使用狀態(tài)的叫做 From 空間,處于閑置的叫 To 空間穷娱,當我們分配對象時绑蔫,先是在 From 空間中進行分配。
- 開始垃圾回收時泵额,會檢查 From 空間中的存活對象配深,這些存活對象將被復(fù)制到 To 空間中,然后釋放 From 空間中的內(nèi)存梯刚。
- From 空間與 To 空間對換
從上面的過程我們可以看到凉馆,Scavenge 算法是典型的犧牲空間換取時間的算法。缺點是只能使用堆內(nèi)存中的一半亡资,優(yōu)點是在時間效率上有優(yōu)異的表現(xiàn)澜共。
老生代空間( OldGeneraion)
在新生代空間中生命周期較長的對象會被復(fù)制到老生代空間中,這個過程叫晉升锥腻。對象晉升的條件主要有兩個:
- 對象是否經(jīng)歷過一次 Scavenge 回收嗦董。 對象從 From 空間復(fù)制到 To 空間時,會檢查它的內(nèi)存地址來判斷這個對象是否已經(jīng)經(jīng)歷過一次 Scavenge 回收瘦黑。如果經(jīng)歷過京革,就直接復(fù)制到老生代空間中,而不是 To 空間幸斥。
- To 空間的內(nèi)存使用占比是否超過 To 空間的 25%匹摇。 對象從 From 空間復(fù)制到 To 空間時,發(fā)現(xiàn) To 空間的內(nèi)存占比已經(jīng)超過限制甲葬。因為To 空間將會變成 From空間廊勃,為了不影響后續(xù)的內(nèi)存分配,會直接晉升到老生代空間中经窖。
對于老生代空間坡垫,由于存活對象占比較大,再采用Scavenge的方式會有兩個問題:
- 存活對象比較多画侣,復(fù)制存活對象的效率會很低
- 要拆分兩個 semispace 空間冰悠,比較浪費
為此,老生代中主要采用標記清除(Mark-Sweep)和標記整理(Mark-Compact)相結(jié)合的方式進行垃圾回收配乱,其工作原理如下:
- 在標記階段遍歷堆中的所有對象溉卓,并標記活著的對象皮迟,在隨后的清除階段中,只清除沒有被標記的對象的诵。標記后的示意圖如下:
黃色部分為標記活躍的對象万栅,深灰色部分為標記死亡的對象。
標記清除(Mark-Sweep)最大的問題在于西疤,清除標記死亡的對象后烦粒,內(nèi)存不連續(xù),這種碎片空間會對后續(xù)的內(nèi)存分配造成問題代赁。
- 為了解決內(nèi)存碎片的問題扰她,標記整理(Mark-Compact)被提出來。它會在標記完成后芭碍,將存活的對象移動到一端徒役,然后釋放存活對象這一端之外的空間。
增量標記(Incremental Marking)
在進行上面 V8垃圾回收操作的時候窖壕,需要將應(yīng)用邏輯暫停忧勿,但是由于老生代空間很大,且存活對象很多瞻讽,為了避免長時間的停頓鸳吸,將原本一次性完成的操作改為增量標記,即拆分為許多小“步進”速勇,沒做完一次“步進”晌砾,讓應(yīng)用邏輯執(zhí)行一會兒,交替執(zhí)行烦磁,直到垃圾回收執(zhí)行完成养匈。
參考文章:
- 《Javascript 高級程序設(shè)計》
- 《深入淺出 Node.js》