自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一文搞懂V8引擎的垃圾回收機(jī)制

開(kāi)發(fā) 前端
我們平時(shí)在寫(xiě)代碼的過(guò)程中,好像很少需要自己手動(dòng)進(jìn)行垃圾回收,那么V8是如何來(lái)減少內(nèi)存占用,從而避免內(nèi)存溢出而導(dǎo)致程序崩潰的情況的。為了更高效地回收垃圾,V8引入了兩個(gè)垃圾回收器,它們分別針對(duì)不同場(chǎng)景進(jìn)行工作。

前言

我們平時(shí)在寫(xiě)代碼的過(guò)程中,好像很少需要自己手動(dòng)進(jìn)行垃圾回收,那么V8是如何來(lái)減少內(nèi)存占用,從而避免內(nèi)存溢出而導(dǎo)致程序崩潰的情況的。為了更高效地回收垃圾,V8引入了兩個(gè)垃圾回收器,它們分別針對(duì)不同場(chǎng)景進(jìn)行工作。

垃圾從何而來(lái)

我們先來(lái)搞清楚這些‘垃圾’是怎么產(chǎn)生的

不管使用哪一種語(yǔ)言,我們勢(shì)必都會(huì)頻繁的操作數(shù)據(jù),這些數(shù)據(jù)一般是存放在棧內(nèi)存與堆內(nèi)存中,通常是會(huì)在內(nèi)存中創(chuàng)建一塊空間,使用這塊空間,再不需要的時(shí)候回收這塊空間。

比如:

var test = {}
test.a = new Array(100)

當(dāng)執(zhí)行這段代碼時(shí),先會(huì)為全局對(duì)象(window)添加一個(gè)test屬性,并在堆內(nèi)存中創(chuàng)建一個(gè)空對(duì)象,并將該對(duì)象的地址指向test屬性,隨后又創(chuàng)建了一個(gè)長(zhǎng)度為100的數(shù)組,并將該數(shù)組地址指向了test.a的屬性值。

從上圖我們可以看出,棧中保存了指向window對(duì)象的指針,通過(guò)棧中window的地址可以找到window對(duì)象,通過(guò)window對(duì)象可以找到test對(duì)象,通過(guò)test對(duì)象可以找到a數(shù)組。

如果此時(shí),我們將a屬性指向了另一個(gè)對(duì)象:

test.a = {}

那么此時(shí)的內(nèi)存會(huì)變成這樣:

那么這個(gè)時(shí)候堆內(nèi)存中的數(shù)組其實(shí)就變成了‘垃圾數(shù)據(jù)’,因?yàn)槲覀冊(cè)僖苍L問(wèn)不到它了,不過(guò)我們不必?fù)?dān)心它會(huì)一直占用內(nèi)存,因?yàn)閂8中的垃圾回收器會(huì)幫我們自動(dòng)清理。

對(duì)于 JavaScript 而言,也正是這個(gè)“自動(dòng)”釋放資源的特性帶來(lái)了很多困惑,也讓一些 JavaScript 開(kāi)發(fā)者誤以為可以不關(guān)心內(nèi)存管理,這是一個(gè)很大的誤解。

代際假說(shuō)與分代收集

代際假說(shuō)是垃圾回收領(lǐng)域中的一個(gè)重要術(shù)語(yǔ),后續(xù)垃圾回收策略都是建立在該假說(shuō)之上的。

特點(diǎn)

  • 第一個(gè)是大部分對(duì)象在內(nèi)存中存在的時(shí)間很短,簡(jiǎn)單來(lái)說(shuō),就是很多對(duì)象一經(jīng)分配內(nèi)存,很快就變得不可訪問(wèn)
  • 第二個(gè)是不死的對(duì)象,會(huì)活得更久

為了達(dá)到最好的回收效果,V8會(huì)根據(jù)對(duì)象的生存周期的不同來(lái)應(yīng)用不同的回收算法,所以在 V8 中會(huì)把堆分為新生代和老生代兩個(gè)區(qū)域,新生代中存放的是生存時(shí)間短的對(duì)象,老生代中存放的生存時(shí)間久的對(duì)象。

支持 1~8M 的容量,而老生區(qū)支持的容量就大很多了。對(duì)于這兩塊區(qū)域,V8 分別使用兩個(gè)不同的垃圾回收器,以便更高效地實(shí)施垃圾回收

  • 副垃圾回收器,主要負(fù)責(zé)新生代的垃圾回收
  • 主垃圾回收器,主要負(fù)責(zé)老生代的垃圾回收

垃圾回收器的工作流程

V8的內(nèi)存結(jié)構(gòu)

  • 新生代(new_space):大多數(shù)的對(duì)象開(kāi)始都會(huì)被分配在這里,這個(gè)區(qū)域相對(duì)較小但是垃圾回收特別頻繁,該區(qū)域被分為兩半,一半用來(lái)分配內(nèi)存,另一半用于在垃圾回收時(shí)將需要保留的對(duì)象復(fù)制過(guò)來(lái)。
  • 老生代(old_space):新生代中的對(duì)象在存活一段時(shí)間后就會(huì)被轉(zhuǎn)移到老生代內(nèi)存區(qū),相對(duì)于新生代該內(nèi)存區(qū)域的垃圾回收頻率較低。老生代又分為老生代指針區(qū)和老生代數(shù)據(jù)區(qū),前者包含大多數(shù)可能存在指向其他對(duì)象的指針的對(duì)象,后者只保存原始數(shù)據(jù)對(duì)象,這些對(duì)象沒(méi)有指向其他對(duì)象的指針。
  • 大對(duì)象區(qū)(large_object_space):存放體積超越其他區(qū)域大小的對(duì)象,每個(gè)對(duì)象都會(huì)有自己的內(nèi)存,垃圾回收不會(huì)移動(dòng)大對(duì)象區(qū)。
  • 代碼區(qū)(code_space):代碼對(duì)象,會(huì)被分配在這里,唯一擁有執(zhí)行權(quán)限的內(nèi)存區(qū)域。
  • map區(qū)(map_space):存放Cell和Map,每個(gè)區(qū)域都是存放相同大小的元素,結(jié)構(gòu)簡(jiǎn)單

垃圾回收的過(guò)程一般主要出現(xiàn)在「新生代」「老生代」。

垃圾回收策略

標(biāo)記清除

標(biāo)記清除( Mark-Sweep ),目前在 JavaScript引擎 里這種算法是最常用的,到目前為止的大多數(shù)瀏覽器的 JavaScript引擎 都在采用標(biāo)記清除算法,只是各大瀏覽器廠商還對(duì)此算法進(jìn)行了優(yōu)化加工,且不同瀏覽器的 JavaScript引擎 在運(yùn)行垃圾回收的頻率上有所差異。此算法分為 標(biāo)記 和 清除 兩個(gè)階段,標(biāo)記階段即為所有活動(dòng)對(duì)象做上標(biāo)記,清除階段則把沒(méi)有標(biāo)記(也就是非活動(dòng)對(duì)象)銷(xiāo)毀。

引擎在執(zhí)行 GC(使用標(biāo)記清除算法)時(shí),需要從出發(fā)點(diǎn)去遍歷內(nèi)存中所有的對(duì)象去打標(biāo)記,而這個(gè)出發(fā)點(diǎn)有很多,我們稱之為一組根對(duì)象,而所謂的根對(duì)象,其實(shí)在瀏覽器環(huán)境中包括又不止于 全局Window對(duì)象、文檔DOM樹(shù)等。

整個(gè)標(biāo)記清除算法大致過(guò)程就像下面這樣:

  • 垃圾收集器在運(yùn)行時(shí)會(huì)給內(nèi)存中的所有變量都加上一個(gè)標(biāo)記,假設(shè)內(nèi)存中所有對(duì)象都是垃圾,全標(biāo)記為0;
  • 然后從各個(gè)根對(duì)象開(kāi)始遍歷,把不是垃圾的節(jié)點(diǎn)改成1;
  • 清理所有標(biāo)記為0的垃圾,銷(xiāo)毀并回收它們所占用的內(nèi)存空間;
  • 最后,把所有內(nèi)存中對(duì)象標(biāo)記修改為0,等待下一輪垃圾回收;

優(yōu)點(diǎn):

實(shí)現(xiàn)比較簡(jiǎn)單,打標(biāo)記也無(wú)非打與不打兩種情況,這使得一位二進(jìn)制位(0和1)就可以為其標(biāo)記,非常簡(jiǎn)單

缺點(diǎn):

在清除之后,剩余的對(duì)象內(nèi)存位置是不變的,也會(huì)導(dǎo)致空閑內(nèi)存空間是不連續(xù)的,出現(xiàn)了 內(nèi)存碎片,并且由于剩余空閑內(nèi)存不是一整塊,它是由不同大小內(nèi)存組成的內(nèi)存列表,這就牽扯出了內(nèi)存分配的問(wèn)題

引用計(jì)數(shù)

引用計(jì)數(shù)( Reference Counting ),這其實(shí)是早先的一種垃圾回收算法,它把對(duì)象是否不再需要簡(jiǎn)化定義為對(duì)象有沒(méi)有其他對(duì)象引用到它,如果沒(méi)有引用指向該對(duì)象(零引用),對(duì)象將被垃圾回收機(jī)制回收,但因?yàn)樗膯?wèn)題很多,目前很少使用這種算法了。


它的策略是跟蹤記錄每個(gè)變量值被使用的次數(shù)

  • 當(dāng)聲明了一個(gè)變量并且將一個(gè)引用類(lèi)型賦值給該變量的時(shí)候這個(gè)值的引用次數(shù)就為 1;
  • 如果同一個(gè)值又被賦給另一個(gè)變量,那么引用數(shù)加 1;
  • 如果該變量的值被其他的值覆蓋了,則引用次數(shù)減 1;
  • 當(dāng)這個(gè)值的引用次數(shù)變?yōu)?0 的時(shí)候,說(shuō)明沒(méi)有變量在使用,這個(gè)值沒(méi)法被訪問(wèn)了,回收空間,垃圾回收器會(huì)在運(yùn)行的時(shí)候清理掉引用次數(shù)為 0 的值占用的內(nèi)存;

優(yōu)點(diǎn):

  • 引用計(jì)數(shù)在引用值為 0 時(shí),也就是在變成垃圾的那一刻就會(huì)被回收,所以它可以立即回收垃圾;
  • 標(biāo)記清除算法需要每隔一段時(shí)間進(jìn)行一次,那在應(yīng)用程序(JS腳本)運(yùn)行過(guò)程中線程就必須要暫停去執(zhí)行一段時(shí)間的 GC,另外,標(biāo)記清除算法需要遍歷堆里的活動(dòng)以及非活動(dòng)對(duì)象來(lái)清除,而引用計(jì)數(shù)則只需要在引用時(shí)計(jì)數(shù)就可以了;

缺點(diǎn):

  • 需要一個(gè)計(jì)數(shù)器,而此計(jì)數(shù)器需要占很大的位置,因?yàn)槲覀円膊恢辣灰脭?shù)量的上限;
  • 無(wú)法解決循環(huán)引用無(wú)法回收的問(wèn)題;

工作流程

不論什么類(lèi)型的垃圾回收器,它們都有一套相同的執(zhí)行流程。

  • 第一步是「標(biāo)記空間中活動(dòng)對(duì)象和非活動(dòng)對(duì)象」。所謂活動(dòng)對(duì)象就是還在使用的對(duì)象,非活動(dòng)對(duì)象就是可以進(jìn)行垃圾回收的對(duì)象。
  • 第二步是「回收非活動(dòng)對(duì)象所占據(jù)的內(nèi)存」。其實(shí)就是在所有的標(biāo)記完成之后,統(tǒng)一清理內(nèi)存中所有被標(biāo)記為可回收的對(duì)象。
  • 第三步是做「內(nèi)存整理」。一般來(lái)說(shuō),頻繁回收對(duì)象后,內(nèi)存中就會(huì)存在大量不連續(xù)空間,我們把這些不連續(xù)的內(nèi)存空間稱為內(nèi)存碎片。當(dāng)內(nèi)存中出現(xiàn)了大量的內(nèi)存碎片之后,如果需要分配較大連續(xù)內(nèi)存的時(shí)候,就有可能出現(xiàn)內(nèi)存不足的情況。所以最后一步需要整理這些內(nèi)存碎片,但這步其實(shí)是可選的,因?yàn)橛械睦厥掌鞑粫?huì)產(chǎn)生內(nèi)存碎片,比如接下來(lái)我們要介紹的副垃圾回收器。

副垃圾回收器

副垃圾回收器主要負(fù)責(zé)新生區(qū)的垃圾回收。而通常情況下,大多數(shù)小的對(duì)象都會(huì)被分配到新生區(qū),所以說(shuō)這個(gè)區(qū)域雖然不大,但是垃圾回收還是比較頻繁的。

新生代中用 Scavenge 算法來(lái)處理。所謂 Scavenge 算法,是把新生代空間對(duì)半劃分為兩個(gè)區(qū)域,一半是對(duì)象區(qū)域,一半是空閑區(qū)域,如下圖所示:

圖片

新加入的對(duì)象都會(huì)存放到對(duì)象區(qū)域,當(dāng)對(duì)象區(qū)域快被寫(xiě)滿時(shí),就需要執(zhí)行一次垃圾清理操作。

在垃圾回收過(guò)程中,首先要對(duì)對(duì)象區(qū)域中的垃圾做標(biāo)記;標(biāo)記完成之后,就進(jìn)入垃圾清理階段,副垃圾回收器會(huì)把這些存活的對(duì)象復(fù)制到空閑區(qū)域中,同時(shí)它還會(huì)把這些對(duì)象有序地排列起來(lái),所以這個(gè)復(fù)制過(guò)程,也就相當(dāng)于完成了內(nèi)存整理操作,復(fù)制后空閑區(qū)域就沒(méi)有內(nèi)存碎片了。完成復(fù)制后,對(duì)象區(qū)域與空閑區(qū)域進(jìn)行角色翻轉(zhuǎn),也就是原來(lái)的對(duì)象區(qū)域變成空閑區(qū)域,原來(lái)的空閑區(qū)域變成了對(duì)象區(qū)域。這樣就完成了垃圾對(duì)象的回收操作,同時(shí)這種角色翻轉(zhuǎn)的操作還能讓新生代中的這兩塊區(qū)域無(wú)限重復(fù)使用下去。

由于新生代中采用的 Scavenge 算法,所以每次執(zhí)行清理操作時(shí),都需要將存活的對(duì)象從對(duì)象區(qū)域復(fù)制到空閑區(qū)域。但復(fù)制操作需要時(shí)間成本,如果新生區(qū)空間設(shè)置得太大了,那么每次清理的時(shí)間就會(huì)過(guò)久,所以為了執(zhí)行效率,一般新生區(qū)的空間會(huì)被設(shè)置得比較小。也正是因?yàn)樾律鷧^(qū)的空間不大,所以很容易被存活的對(duì)象裝滿整個(gè)區(qū)域。為了解決這個(gè)問(wèn)題,JavaScript 引擎采用了「對(duì)象晉升策略」,也就是經(jīng)過(guò)兩次垃圾回收依然還存活的對(duì)象,會(huì)被移動(dòng)到老生區(qū)中。

主垃圾回收器

主垃圾回收器主要負(fù)責(zé)老生區(qū)中的垃圾回收。除了新生區(qū)中晉升的對(duì)象,一些大的對(duì)象會(huì)直接被分配到老生區(qū)。因此老生區(qū)中的對(duì)象有兩個(gè)特點(diǎn),一個(gè)是對(duì)象占用空間大,另一個(gè)是對(duì)象存活時(shí)間長(zhǎng)。

由于老生區(qū)的對(duì)象比較大,若要在老生區(qū)中使用 Scavenge 算法進(jìn)行垃圾回收,復(fù)制這些大的對(duì)象將會(huì)花費(fèi)比較多的時(shí)間,從而導(dǎo)致回收?qǐng)?zhí)行效率不高,同時(shí)還會(huì)浪費(fèi)一半的空間。因而,主垃圾回收器是采用「標(biāo)記 - 清除(Mark-Sweep)」的算法進(jìn)行垃圾回收的。

它的原理就是:

  • 首先是標(biāo)記過(guò)程階段。標(biāo)記階段就是從一組根元素開(kāi)始,遞歸遍歷這組根元素,在這個(gè)遍歷過(guò)程中,能到達(dá)的元素稱為活動(dòng)對(duì)象,沒(méi)有到達(dá)的元素就可以判斷為垃圾數(shù)據(jù)。
  • 接下來(lái)就是垃圾的清除過(guò)程。它和副垃圾回收器的垃圾清除過(guò)程完全不同,對(duì)一塊內(nèi)存多次執(zhí)行「標(biāo)記 - 清除」算法后,可能會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片。

圖片

  • 而碎片過(guò)多會(huì)導(dǎo)致大對(duì)象無(wú)法分配到足夠的連續(xù)內(nèi)存,于是又產(chǎn)生了另外一種算法——「標(biāo)記 - 整理(Mark-Compact)」,這個(gè)標(biāo)記過(guò)程仍然與標(biāo)記 - 清除算法里的是一樣的,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。

圖片

全停頓

由于 JavaScript 是運(yùn)行在主線程之上的,一旦執(zhí)行垃圾回收算法,都需要將正在執(zhí)行的 JavaScript 腳本暫停下來(lái),待垃圾回收完畢后再恢復(fù)腳本執(zhí)行。我們把這種行為叫做「全停頓(Stop-The-World)。

在 V8 新生代的垃圾回收中,因其空間較小,且存活對(duì)象較少,所以全停頓的影響不大,但老生代就不一樣了。如果在執(zhí)行垃圾回收的過(guò)程中,占用主線程時(shí)間過(guò)久,將會(huì)造成頁(yè)面卡頓。

為了降低老生代的垃圾回收而造成的卡頓,V8 將標(biāo)記過(guò)程分為一個(gè)個(gè)的子標(biāo)記過(guò)程,同時(shí)讓垃圾回收標(biāo)記和 JavaScript 應(yīng)用邏輯交替進(jìn)行,直到標(biāo)記階段完成,我們把這個(gè)算法稱為增量標(biāo)記(Incremental Marking)算法。

責(zé)任編輯:華軒 來(lái)源: 前端南玖
相關(guān)推薦

2023-02-28 07:56:07

V8內(nèi)存管理

2020-09-27 07:32:18

V8

2021-02-26 05:24:35

Java垃圾回收

2023-10-10 10:23:50

JavaScriptV8

2020-05-14 13:39:19

Java 垃圾回收機(jī)制

2009-08-21 10:09:02

Google ChroV8引擎linux系統(tǒng)

2023-08-27 21:29:43

JVMFullGC調(diào)優(yōu)

2009-06-23 14:15:00

Java垃圾回收

2010-09-25 15:33:19

JVM垃圾回收

2017-03-03 09:26:48

PHP垃圾回收機(jī)制

2017-08-17 15:40:08

大數(shù)據(jù)Python垃圾回收機(jī)制

2011-07-04 16:48:56

JAVA垃圾回收機(jī)制GC

2017-06-12 17:38:32

Python垃圾回收引用

2022-04-29 08:00:51

V8垃圾回收

2024-03-14 09:40:14

2020-10-12 06:35:34

V8JavaScript

2017-12-17 16:34:18

JavaScript代碼V8

2024-05-23 12:40:06

2021-11-05 15:23:20

JVM回收算法

2021-12-07 08:01:33

Javascript 垃圾回收機(jī)制前端
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)