javascript
搞不定 NodeJS 内存泄漏?先从了解垃圾回收开始
通常來(lái)說(shuō),內(nèi)存管理有兩種方式,一種是手動(dòng)管理,一種是自動(dòng)管理。
手動(dòng)管理需要開(kāi)發(fā)者自己管理內(nèi)存,什么時(shí)候申請(qǐng)內(nèi)存空間,什么時(shí)候釋放都需要小心處理,否則容易形成內(nèi)存泄漏和指針亂飛的局面。C 語(yǔ)言開(kāi)發(fā)是典型的需要手動(dòng)管理內(nèi)存的例子。
自動(dòng)管理通常通過(guò)垃圾回收的機(jī)制來(lái)實(shí)現(xiàn)內(nèi)存管理。NodeJS 中的內(nèi)存管理是自動(dòng)管理的。
垃圾回收
垃圾回收器(garbage collector,GC)通過(guò)判斷對(duì)象是否還在被其他對(duì)象引用來(lái)決定是否回收該對(duì)象的內(nèi)存空間。
垃圾回收之前的內(nèi)存
在下面的圖中,有一些對(duì)象還在被其他對(duì)象使用,而有一些對(duì)象已經(jīng)是完全孤立狀態(tài),沒(méi)有其他對(duì)象使用它了。這些已經(jīng)完全孤立狀態(tài)的對(duì)象是可以被垃圾回收器回收的。
垃圾回收之后的內(nèi)存
垃圾回收一旦開(kāi)始運(yùn)行,內(nèi)存中的那些完全孤立(不可到達(dá))的對(duì)象會(huì)被刪除,內(nèi)存空間會(huì)被釋放。
垃圾回收是如何工作的
要搞清楚垃圾回收是如何工作的,需要先了解一些基本概念。
基本概念
- 常駐集大小(resident set size):NodeJS 進(jìn)程運(yùn)行時(shí)占據(jù)的內(nèi)存大小,通常包含:代碼、棧和堆。
- 棧(stack):包含原始類(lèi)型數(shù)據(jù)和指向?qū)ο蟮囊脭?shù)據(jù)。棧中保存著局部變量和指向堆上對(duì)象的指針或定義應(yīng)用程序控制流的指針(比如函數(shù)調(diào)用等)。下面代碼中,a 和 b 都保存在棧中。function?add?(a, b) {?return?a + b } add(4, 5)
- 堆(heap):存放引用類(lèi)型數(shù)據(jù),比如對(duì)象、字符串、閉包等。下面代碼中,創(chuàng)建的 Car 對(duì)象會(huì)被保存在堆中。function?Car?(opts) {?this.name = opts.name }?const?LightningMcQueen =?new?Car({name:?'Lightning McQueen'})?對(duì)象創(chuàng)建后,堆內(nèi)存狀態(tài)如下:現(xiàn)在我們添加更多的對(duì)象:const?SallyCarrera =?new?Car({name:?'Sally Carrera'})?const?Mater =?new?Car({name:?'Mater'})?堆內(nèi)存狀態(tài)如下:如果現(xiàn)在執(zhí)行垃圾回收,沒(méi)有任何內(nèi)存會(huì)被釋放,因?yàn)槊總€(gè)對(duì)象都在被使用(可到達(dá))。現(xiàn)在我們修改代碼,如下:function?Engine?(power) {?this.power = power }?function?Car?(opts) {?this.name = opts.name?this.engine =?new?Engine(opts.power) }?let?LightningMcQueen =?new?Car({name:?'Lightning McQueen',?power: 900})?let?SallyCarrera =?new?Car({name:?'Sally Carrera',?power: 500})?let?Mater =?new?Car({name:?'Mater',?power: 100})?堆內(nèi)存狀態(tài)變成:如果我們不在使用 Mater 的話,通過(guò) Mater = undefined 刪除了對(duì)內(nèi)存中對(duì)象的引用,則內(nèi)存狀態(tài)變化為:此時(shí)內(nèi)存中的 Mater 不再被其他對(duì)象使用了(不可達(dá)),當(dāng)垃圾回收運(yùn)行的時(shí)候,Mater 對(duì)象會(huì)被回收,其占據(jù)的內(nèi)存會(huì)被釋放。
- 對(duì)象的淺層大小(shallow size of an object):對(duì)象本身占據(jù)的內(nèi)存大小。
- 對(duì)象的保留大小(retained size of an object):刪除對(duì)象及其依賴(lài)對(duì)象后釋放的內(nèi)存大小
垃圾回收器是如何工作的
NodeJS 的垃圾回收通過(guò) V8 實(shí)現(xiàn)。大多數(shù)對(duì)象的生命周期都很短,而少數(shù)對(duì)象的壽命往往更長(zhǎng)。為了利用這種行為,V8 將堆分成兩個(gè)部分,年輕代(Young Generation)和老年代(Old Generation)。
年輕代
新的內(nèi)存需求都在年輕代中分配。年輕代的大小很小,在 1 到 8 MB 之間。在年輕代中內(nèi)存分配非常便宜,V8 在內(nèi)存中會(huì)逐個(gè)為對(duì)象分配空間,當(dāng)?shù)竭_(dá)年輕代的邊界時(shí),會(huì)觸發(fā)一次垃圾回收。
V8 在年輕代會(huì)采用 Scavenge 回收策略。Scavenge 采用復(fù)制的方式進(jìn)行垃圾回收。它將內(nèi)存一分為二,每一部分空間稱(chēng)為 semispace。這兩個(gè)空間,只有一個(gè)空間處于使用中,另一個(gè)則處于閑置。使用中的 semispace 稱(chēng)為 「From 空間」,閑置的 semispace 稱(chēng)為 「To 空間」。
年輕代的內(nèi)存分配過(guò)程如下:
在年輕代中幸存的對(duì)象會(huì)被提升到老年代。
老年代
老年代中的對(duì)象有兩個(gè)特點(diǎn),第一是存活對(duì)象多,第二個(gè)存活時(shí)間長(zhǎng)。若在老年代中使用 Scavenge 算法進(jìn)行垃圾回收,將會(huì)導(dǎo)致復(fù)制存活對(duì)象的效率不高,且還會(huì)浪費(fèi)一半的空間。因此在老年代中,V8 通常采用 Mark-Sweep 和 Mark-Compact 策略回收。
Mark-Sweep 就是標(biāo)記清除,它主要分為標(biāo)記和清除兩個(gè)階段。
- 標(biāo)記階段,將遍歷堆中所有對(duì)象,并對(duì)存活的對(duì)象進(jìn)行標(biāo)記;
- 清除階段,對(duì)未標(biāo)記對(duì)象的空間進(jìn)行回收。
與 Scavenge 策略不同,Mark-Sweep 不會(huì)對(duì)內(nèi)存一分為二,因此不會(huì)浪費(fèi)空間。但是,經(jīng)歷過(guò)一次 Mark-Sweep 之后,內(nèi)存的空間將會(huì)變得不連續(xù),這樣會(huì)對(duì)后續(xù)內(nèi)存分配造成問(wèn)題。比如,當(dāng)需要分配一個(gè)比較大的對(duì)象時(shí),沒(méi)有任何一個(gè)碎片內(nèi)支持分配,這將提前觸發(fā)一次垃圾回收,盡管這次垃圾回收是沒(méi)有必要的。
為了解決內(nèi)存碎片的問(wèn)題,提高對(duì)內(nèi)存的利用,引入了 Mark-Compact (標(biāo)記整理)策略。Mark-Compact 是在 Mark-Sweep 算法上進(jìn)行了改進(jìn),標(biāo)記階段與 Mark-Sweep 相同,但是對(duì)未標(biāo)記的對(duì)象處理方式不同。與Mark-Sweep是對(duì)未標(biāo)記的對(duì)象立即進(jìn)行回收,Mark-Compact則是將存活的對(duì)象移動(dòng)到一邊,然后再清理端邊界外的內(nèi)存。
由于 Mark-Compact 需要移動(dòng)對(duì)象,所以執(zhí)行速度上,比 Mark-Sweep 要慢。所以,V8 主要使用 Mark-Sweep 算法,然后在當(dāng)空間內(nèi)存分配不足時(shí),采用 Mark-Compact 算法。
總結(jié)
以上是生活随笔為你收集整理的搞不定 NodeJS 内存泄漏?先从了解垃圾回收开始的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 牛皮啊,全网独家SpringCloud
- 下一篇: Spring选择哪种注入方式