日韩成人免费在线_国产成人一二_精品国产免费人成电影在线观..._日本一区二区三区久久久久久久久不

當(dāng)前位置:首頁 > 科技  > 軟件

DartVM GC 深度剖析

來源: 責(zé)編: 時間:2024-02-06 10:13:51 382觀看
導(dǎo)讀一、前言GC 全稱 Garbage Collection,垃圾收集,是一種自動管理堆內(nèi)存的機制,負(fù)責(zé)管理堆內(nèi)存上對象的釋放。在沒有 GC 時,需要開發(fā)者手動管理內(nèi)存,想要保證完全正確的管理內(nèi)存需要開發(fā)者花費相當(dāng)大的精力。所以為了讓程序員

一、前言

GC 全稱 Garbage Collection,垃圾收集,是一種自動管理堆內(nèi)存的機制,負(fù)責(zé)管理堆內(nèi)存上對象的釋放。在沒有 GC 時,需要開發(fā)者手動管理內(nèi)存,想要保證完全正確的管理內(nèi)存需要開發(fā)者花費相當(dāng)大的精力。所以為了讓程序員把更多的精力集中在實際問題上,GC 誕生了。Dart 作為 Flutter 的主要編程語言,在內(nèi)存管理上也使用了 GC。

而在 Pink(倉儲作業(yè)系統(tǒng))的線上穩(wěn)定性問題中,有一個和 GC 相關(guān)的疑難雜癥,問題堆棧發(fā)生在 GC 標(biāo)記過程,但是導(dǎo)致問題的根源并不在這里,因為 GC 流程相當(dāng)復(fù)雜,無法確定問題到底出在哪個環(huán)節(jié)。于是,就對 DartVM 的 GC 流程進行了一次完整的梳理,從 GC 整個流程逐步排查。yCj28資訊網(wǎng)——每日最新資訊28at.com

二、Dart 對象

要想完整的了解 GC,就要先了解 Dart 對象在 DartVM 的內(nèi)存管理是怎樣呈現(xiàn)的。這里,我們先從 Dart 對象的內(nèi)存分配來展開介紹。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

對象內(nèi)存分配

yCj28資訊網(wǎng)——每日最新資訊28at.com

在 Flutter 中,Dart 代碼會先編譯成 Kernel dill 文件,再通過 gen_snapshot 將 dill 文件生成 Snapshot。而 dill 文件在生成 Snapshot 的中間過程,會將 dill 文件中 AST 翻譯成 FlowGraph,然后再 FlowGraph 中的 il 指令編譯成 AOT 機器指令。那創(chuàng)建對象的代碼最終會編譯成什么指令呢?接下來,我們先看一下 AST 中對象構(gòu)造方法調(diào)用的表達式最終翻譯成 FlowGraph 是什么樣的。

編譯前:yCj28資訊網(wǎng)——每日最新資訊28at.com

void _syncAll() {  final obj = TestB();  obj.hello("arg");}

編譯后的 FlowGraph:yCj28資訊網(wǎng)——每日最新資訊28at.com

@"==== package:flutter_demo/main.dart_::__syncAll@1288309603 (RegularFunction)/r/n"@"B0[graph]:0/r/n"@"B1[function entry]:2/r/n"@"    CheckStackOverflow:8(stack=0, loop=0)/r/n"@"    t0 <- AllocateObject:10(cls=TestB)/r/n"@"    t1 <- LoadLocal(:t0 @-2)/r/n"@"    StaticCall:12( TestB.<0> t1)/r/n"@"    StoreLocal(obj @-1, t0)/r/n"@"    t0 <- LoadLocal(obj @-1)/r/n"@"    t1 <- Constant(#arg)/r/n"@"    StaticCall:14( hello<0> t0, t1, using unchecked entrypoint, result_type = T{??})/r/n"@"    t0 <- Constant(#null)/r/n"@"    Return:16(t0)/r/n"@"*** END CFG/r/n"

可以看到,一個構(gòu)造方法調(diào)用,最終會轉(zhuǎn)換為 AllocateObject 和 StaticCall 兩條指令,其中 AllocateObject 指令用來分配對象內(nèi)存,而 StaticCall 則是真正調(diào)用構(gòu)造方法。yCj28資訊網(wǎng)——每日最新資訊28at.com

那 AllocateObject IL 最終轉(zhuǎn)化成的機器指令又是怎樣的呢?yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

在將 AllocateObject 指令轉(zhuǎn)換為 AOT 指令前,會先通過 GenerateNecessaryAllocationStubs() 方法為 FlowGraph 中的每一個 AllocateObject 指令所對應(yīng) Class 生成一個 StubCode,StubCode::GetAllocationStubForClass() 會先檢查 對應(yīng) Class 是否已經(jīng)存在 allocation StubCode,如果已經(jīng)存在,則直接返回;如果不存在,則會執(zhí)行下面代碼,為 Class 生成 allocation StubCode。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看出,生成的 allocation StubCode 其實主要是使用了 object_store->allocate_object_stub(),而 object_store->allocate_object_stub() 最終指向的則是 DartVM 中的 Object::Allocate()。yCj28資訊網(wǎng)——每日最新資訊28at.com

生成 allocation StubCode 之后,我們來看一下 AllocateObject 指令轉(zhuǎn)成 AOT 機器指令是怎樣的。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看出,最終生成的機器指令主要就是對 StubCode 的調(diào)用,而調(diào)用的 StubCode 就是上文中通過 GenerateNecessaryAllocationStubs() 生成的 allocation StubCode。所以,Dart 對象的內(nèi)存分配最終是通過 DartVM 的 Object::Allocate() 來實現(xiàn)的。接下來,我們簡單看一下 Object::Allocate() 的實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,Object::Allocate() 主要是通過 DartVM 中的 heap 進行內(nèi)存分配的,而 heap->Allocate() 的返回值就是內(nèi)存分配的地址。接下來,通過判斷 address == 0 來判斷,內(nèi)存是否分配成功,如果分配失敗,說明 heap 上已經(jīng)不能再分配更多內(nèi)存了,就會拋出 OOM。反之,則通過 NoSafepointScope 來建立非安全點作用域,然后,通過 InitializeObject() 為對象中屬性賦初始值,完成對象初始化。到這里,Dart 對象在內(nèi)存中的分配流程就結(jié)束了,接下來就是調(diào)用構(gòu)造函數(shù),完成對象的真正構(gòu)造。那么,Dart 對象在內(nèi)存中的存儲形式是怎樣的呢?接下來,我們就來介紹一下 Dart 對象的內(nèi)存布局。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

對象內(nèi)存布局

yCj28資訊網(wǎng)——每日最新資訊28at.com

在 Dart 中,每個對象都是一個類的實例,其內(nèi)部是由一系列數(shù)據(jù)成員(類的字段)和一些額外信息組成的。而 Dart 對象在內(nèi)存中是怎么存儲的呢?這里,就不得不先介紹一下 DartVM 中 raw_object。

Dart 中的大部分對象都是 UntaggedObject 的形式存儲在內(nèi)存中,而對象之間的依賴則是通過 ObjectPtr 來維系,ObjectPtr 是指向 UntaggedObject 的指針,所以對象之間的訪問都是通過 ObjectPtr。yCj28資訊網(wǎng)——每日最新資訊28at.com

先看一下 UntaggedObject 的實現(xiàn):yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

代碼比較長,這里直接看一下偽代碼:yCj28資訊網(wǎng)——每日最新資訊28at.com

class UntaggedObject {  // 表示對象類型的 tag  var tag;}

UntaggedObject 是 Dart VM 中一種比較基礎(chǔ)的對象結(jié)構(gòu),所以 Dart 中的大部分對象都是由 UntaggedObject 來承載的。由于 UntaggedObject 可以存儲不同類型的數(shù)據(jù),因此需要使用 tag 字段來標(biāo)識當(dāng)前對象的類型。具體的實現(xiàn)方式是,使用 tag 字段的低位來記錄對象的類型,另外的高位用來存儲一些額外的信息,例如對象是否已經(jīng)被垃圾回收等。所以,UntaggedObject 可以看做是 Dart 對象的 header。yCj28資訊網(wǎng)——每日最新資訊28at.com

一個 Dart 對象其實是由兩部分組成,一個 header,一個是 fields,而 header 就是上文中的 UntaggedObject。yCj28資訊網(wǎng)——每日最新資訊28at.com

+-------------------+         |    header word    |         +-------------------+         | instance variables|         |     (fields)      |         +-------------------+

Header word:包含了對象的類型信息、標(biāo)記位、長度等一些重要元信息。具體信息將根據(jù)對象的類型與具體實現(xiàn)而不同。yCj28資訊網(wǎng)——每日最新資訊28at.com

Instance variables(fields):是一個數(shù)組,用于存儲類的實例變量。每個字段可以存儲不同的數(shù)據(jù)類型,如布爾值、數(shù)字、字符串、列表等。yCj28資訊網(wǎng)——每日最新資訊28at.com

接下來,我們看一下,一個 Dart 對象是如何遍歷它的所有屬性:yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看出,先通過 HeapSize() 獲取對象在 heap 中的實際大小,然后根據(jù)對象起始地址 + UntaggedObject 的大小計算得出 fileds 中保存第一個 ObjectPtr 的地址,然后根據(jù)對象起始地址 + 對象時機大小 - ObjectPtr 的大小計算得出 fileds 中保存的最后一個 ObjectPrt 的地址,這樣就可以通過第一個 ObjectPtr 遍歷到最后一個 ObjectPrt,訪問到 Dart 對象中的所有屬性。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

對象指針

yCj28資訊網(wǎng)——每日最新資訊28at.com

對象指針就是上文中所提到的 ObjectPtr,它指向的是直接對象或者 heap 上的對象,可以通過指針的低位來進行判斷。在 DartVM 上只有一種直接對象,那就是 Smi(小整形),他的指針標(biāo)記為 0,而 heap 上的對象的指針標(biāo)記則為 1。Smi 指針的高位就是小整形對應(yīng)的數(shù)值,而對于 heap 對象指針,指針本身就是只是指向 UntaggedObject 的地址,但是需要地址稍作轉(zhuǎn)換,將最低位的標(biāo)記位設(shè)置為 0,因為每個 heap 對象都是大于 2 字節(jié)的方式對齊的,所以它的低位永遠(yuǎn)都為 0,所以可以使用低位來存儲一些其他信息,區(qū)分是否為直接對象還是 heap 對象。

標(biāo)記為 0 可以使 Smi 可以直接執(zhí)行很多操作,而無需取消標(biāo)記和重新標(biāo)記。yCj28資訊網(wǎng)——每日最新資訊28at.com

標(biāo)記為 1 的 heap 對象指針 在訪問對象時需要先取消標(biāo)記,代碼實現(xiàn)如下。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

Heap 中的對象總是以雙字節(jié)增量分配的。所以 老年代中的對象是保持雙字節(jié)對齊的(address % double-word == 0),而新生代中的對象則保持雙字節(jié)對齊偏移(address % double-word == word)。這樣的話,我們僅需要通過對象地址的對齊方式就能判斷出對象是老年代 還是 新生代,也方便了在 GC 過程快速分辨出對象是新生代 還是 老年代,從而在遍歷過程中直接跳過。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

三、DartVM GC

在介紹 DartVM GC 之前,我們先來看一下 DartVM 的內(nèi)存模型。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,DartVM 中可以運行多個 isolate group,而一個 ioslate group 中又運行著多個 isolate,對于 Flutter 應(yīng)用來說,通常只有 一個 isolate group,運行 main() 方法的 Root Isolate 和其他 isolate,其他 isolate 也是通過 Root Isolate 孵化而來,所以都隸屬同一個 isolate group。每個 isolate group 都有一個單獨的 Heap,Heap 又分為新生代和老年代,所以 DartVM 采用的 GC 方式是分代 GC。新生代使用 Scavenge 進行垃圾回收,老年代則是使用 Mark-Sweep 和 Mark-Compact 進行垃圾回收。Scavenge 采用的 GC 算法是 Copying GC,而 Copying GC 算法的思路是把內(nèi)存分為兩個空間,這里可以稱之為 from-space 和 to-space。接下來,我們來看一下 Scavenge GC 的具體實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

Scavenge

yCj28資訊網(wǎng)——每日最新資訊28at.com

為了提高 CPU 的使用率,Scavenge 是多線程并行進行垃圾回收的,線程數(shù)量通過 FLAG_scavenger_tasks 來決定(默認(rèn)為 2),每個工作線程處理 root object 集合中的一部分。

yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,Scavenge 會在 主線程和 多個 helper thread 上并發(fā)執(zhí)行 ParallelScavengerTask,接下來看一下 ParallelScavengerTask 的實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

ParallelScavengerTask 中會通過 ProcessRoots() 來遍歷整個 heap 上的所有根對象 以及 RememberedSet 中的對象,而 RememberedSet 中的對象不一定是根對象,也可能是普通的老年代對象,但是它的屬性中保存了新生代對象的指針,因為新生代對象在移動之后,也要更新老年代對象中的對象指針,所以ProcessRoots() 會把這類對象也看做根對象進行遍歷。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

然后再通過 ParallelScavengerVisitor 訪問所有的根對象,如果根對象是:yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

ScavengePointer() 會通過 ScavengeObject() 將當(dāng)前屬性對象轉(zhuǎn)移至新生代的 to-space 或者老年代分頁上,如果是轉(zhuǎn)移至老年代分頁上,則會將當(dāng)前屬性對象記錄到 promoted_list_ 隊列中;之后便將新對象的地址賦值到根對象的屬性,完成對象引用更新。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

ParallelScavengerTask 中執(zhí)行完 ProcessRoots() 之后,便開始執(zhí)行 ProcessToSpace() 遍歷 to-space 區(qū)域中的對象,而此時 to-space 區(qū)域中存放的正是剛剛復(fù)制過來的根對象,然后通過 ProcessCopied() 遍歷根對象中的所有屬性。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

遍歷到的屬性對象,如果是新生代對象,則繼續(xù)移動到 to-space 區(qū)域或者老年代內(nèi)存分頁中,然后用移動后的新地址更新對象屬性的對象指針。yCj28資訊網(wǎng)——每日最新資訊28at.com

移動后的對象因為放入到了 to-space 區(qū)域,此時新加入到 to-space 的對象也將被遍歷,這樣根對象的屬性遍歷結(jié)束后會緊接著遍歷屬性對象中的屬性,然后新的屬性對象又被移動到 to-space 區(qū)域。這樣周而復(fù)始,就達到了廣度優(yōu)先遍歷的效果。所有被根對象直接引用或者間接引用到的對象都會被遍歷到,對象從 from-space 轉(zhuǎn)移到 to-space 或者老年代分頁上,并完成對象引用更新。yCj28資訊網(wǎng)——每日最新資訊28at.com

在 ScavengeObject() 移動對象的過程中,本來在 from-space 區(qū)域的對象不一定是移動到 to-space 區(qū)域,也有可能移動到老年代分頁內(nèi)存上,那這些對象所關(guān)聯(lián)的屬性該怎么更新呢?這就要介紹一下 promoted_list_,在 ScavengeObject() 過程中,移動到老年代的對象,將會被放入 promoted_list_ 集合中,當(dāng) ProcessToSpace() 結(jié)束之后,則會調(diào)用 ProcessPromotedList() 方法遍歷 promoted_list_ 集合中的對象,從而對移動到老年代的對象的所有屬性進行遍歷,并將其所關(guān)聯(lián)的對象指針進行更新。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

接下來,我們來看一下 ScavengeObject() 的實現(xiàn),也就是對象移動到 to-space 的具體細(xì)節(jié)。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

代碼較長,這里就只截出了部分細(xì)節(jié),可以看到,ScavengeObject() 會先通過 ReadHeaderRelaxed() 獲取到對象頭,通過對象頭來判斷當(dāng)前對象是否已經(jīng)被轉(zhuǎn)移,如果已經(jīng)轉(zhuǎn)移,也直接通過 header 獲取新地址的對象,然后將新對象進行返回。如果未轉(zhuǎn)移,則通過 NewPage::of() 獲取到對象所在的新生代內(nèi)存分頁,通過該分頁中的 survivor_end_ 來判定該對象是否是上次 GC 中的存活對象,如果不是上次 GC 的存活對象,說明是新對象,就直接通過 TryAllocateCopy() 在 to-space 上申請內(nèi)存空間得到 new_addr。接下來,就判斷 new_addr 是否為 0,如果為 0,就存在兩種情況,一個是該對象是上次 GC 的存活對象,一個是 TryAllocateCopy() 分配內(nèi)存失敗,這兩種情況下就會通過 page_space_ 在老年代內(nèi)存上分配內(nèi)存,從而使對象從新生代轉(zhuǎn)移到老年代。接下來,就是 objcpy() 將對象數(shù)據(jù)復(fù)制到新地址中。復(fù)制完成后,就會通過 ForwardingHearder 來創(chuàng)建一個 forwarding_header 對象,并通過 InstallForwardingPointer 將其寫入到原來對象的對象頭中,這樣在遍歷對象過程中,快速判斷出對象是否已經(jīng)轉(zhuǎn)移,并通過對象頭快速獲取到轉(zhuǎn)移后的新地址。至此,ScavengeObject() 的流程就結(jié)束了,然后將新對象返回出去,然后上層調(diào)用點 ScavengePointer() 就會通過這個新對象來更新對象指針。可以看出,Scavenge 在移動對象的同時,將對象指針也進行更新了,這樣就只需遍歷一次新生代內(nèi)存上的對象,即可完成 GC 的主流程。所以,新生代的 GC 算法相對于其他 GC 算法要高效很多。yCj28資訊網(wǎng)——每日最新資訊28at.com

而 Scavenge 之所以采用 Copying GC 算法,正是因為它優(yōu)秀的吞吐量,吞吐量意思就是單位時間內(nèi) GC 的處理能力,可以簡單理解為效率更好的算法吞吐量更優(yōu)秀。對比一下,Mark-Sweep 算法的消耗是根搜索和遍歷整個 heap 花費的時間之和,Copying GC 算法則是根搜索和復(fù)制存活對象。一般來說 Copying GC 算法的吞吐量會更優(yōu)秀,堆越大差距越明顯。眾所周知,在算法上,時間維度和空間維度是成反比的,既然有了這么優(yōu)秀吞吐量,那必然要犧牲一部分空間,所以 Copying GC 在內(nèi)存使用效率上相對于其他 GC 算法是比較低的。Copying GC 算法總是有一個區(qū)域無法使用,對比其他使用整堆的算法,堆的使用效率低。這是 Copying GC 算法的一個重大缺陷。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

Mark-Sweep

yCj28資訊網(wǎng)——每日最新資訊28at.com

老年代 GC 主要分為兩種方式,一個是 Mark-Sweep,一個是 Mark-Compact,而 Mark-Sweep 相對于 Mark-Compact 更加輕量。

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

觸發(fā) GC 的方式有兩種,一個是系統(tǒng)處于空閑狀態(tài)時,一個對象分配內(nèi)存時內(nèi)存不足,上方代碼則是 Heap::NotifyIdle() 中的一段邏輯。Heap::NotifyIdle() 是 Dart VM 中的一個函數(shù),用于通知垃圾回收器當(dāng)前系統(tǒng)處于空閑狀態(tài),垃圾回收器可以利用這段空閑時間進行垃圾回收。具體來說,Heap::NotifyIdle函數(shù)會向垃圾回收器發(fā)送一個通知,告訴垃圾回收器當(dāng)前系統(tǒng)處于空閑狀態(tài),可以進行垃圾回收。垃圾回收器在收到通知后,會開始啟動垃圾回收器,對堆中的垃圾對象進行回收。這個函數(shù)可以在應(yīng)用程序中的任何時間點調(diào)用,例如當(dāng)應(yīng)用程序處于空閑狀態(tài)時,或者當(dāng)應(yīng)用程序需要高峰時期的性能時。yCj28資訊網(wǎng)——每日最新資訊28at.com

Mark-Sweep 主要分為兩個階段一個是標(biāo)記階段,另一個則是清理階段。這里我們先看一下標(biāo)記階段。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

對象標(biāo)記

對象標(biāo)記是整個老年代 GC 的一個重要流程,不管是 Mark-Sweep,還是 Mark-Compact,都建立在對象標(biāo)記的基礎(chǔ)上。而對象標(biāo)記又分為并行標(biāo)記和并發(fā)標(biāo)記,這里我們以并行標(biāo)記為例來介紹一下標(biāo)記階段。并行標(biāo)記是利用多個線程同時進行標(biāo)記任務(wù),提高多核 CPU 的使用率,從而減少 GC 流程中對象標(biāo)記所花費的時間,這里我們直接看一下 并行標(biāo)記過程中 MarkObjects() 具體實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,MarkObjects() 中的 FLAG_marker_tasks 和 Scavenge 中的 FLAG_scavenger_tasks 相似,為了充分利用 CPU,提高 GC 的性能,通過 FLAG_marker_tasks 決定線程的個數(shù),然后開啟多個線程并發(fā)標(biāo)記,F(xiàn)LAG_scavenger_tasks 默認(rèn)值也是 2。這里我們假設(shè) FLAG_scavenger_tasks 是 0,以單線程標(biāo)記 來梳理 對象標(biāo)記的整個流程。因為是單線程,所以這里忽略掉 ResetSlices() 的實現(xiàn),ResetSlices() 的主要作用是進行分片,為多個線程劃分不同的標(biāo)記任務(wù)。接下來,我們可以看到 IterateRoots(),開始遍歷根對象,從根對象開始標(biāo)記,根對象標(biāo)記之后,會將根對象添加至 work_list_。緊接著,會調(diào)用 ProcessDeferredMarking() 從 work_list_ 中取出對象,然后遍歷它的所有屬性,將屬性所關(guān)聯(lián)的對象進行標(biāo)記,并將其再次加入 work_list_ 繼續(xù)遍歷,周而復(fù)始,就會把根對象直接引用或間接引用的對象都進行了標(biāo)記,從而達到了從根對象開始的廣度優(yōu)先遍歷。接下來,我們看一下對象標(biāo)記 MarkObject() 的具體實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

MarkObject() 截圖中刪除了部分細(xì)節(jié),這里主要看一下關(guān)鍵流程。可以看到,MarkObject() 會先判斷對象是否是小整形或者是新生代對象,小整形在上文有介紹到,指針即對象,無需標(biāo)記,而新生代對象也無需標(biāo)記,緊接著就是調(diào)用 TryAcquireMarkBit() 進行標(biāo)記,標(biāo)記完成后,就會調(diào)用 PushMarked() 將對象加入到 work_list_ 中。接下來,我們看一下 DrainMarkingStack() 的實現(xiàn),也就是遍歷 work_list_ 的實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

正如上文所述,DrainMarkingStack() 會以一直從 work_list_ 中取出對象,然后通過 VisitPointersNonvirtual() 遍歷對象中的所有屬性,將屬性所關(guān)聯(lián)的對象進行標(biāo)記,標(biāo)記之后再將其加入到 work_list_,致使繼續(xù)往下遍歷,直到 work_list_ 中的對象被清空。這樣一來,根對象直接引用和間接引用的對象都將會標(biāo)記。至此,標(biāo)記對象的核心流程就大致介紹完了。yCj28資訊網(wǎng)——每日最新資訊28at.com

接下來,我們看一下 Mark-Sweep 中另外一個重要的環(huán)節(jié) Sweep。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

Sweep

Sweep 作為 Mark-Sweep 的重要一環(huán),主要作用是將未標(biāo)記對象的內(nèi)存進行清理,從而釋放出內(nèi)存空間給新對象進行分配。yCj28資訊網(wǎng)——每日最新資訊28at.com

我們直接看一下 Sweep 流程中的關(guān)鍵代碼:yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,老年代的 OldPage 主要是通過 sweeper 的 SweepPage() 來進行清理的,SweepPage() 清理完成后會返回一個 bool 值,表示當(dāng)前的 OldPage 是否還存在對象,如果已經(jīng)沒有對象了,則會調(diào)用 Deallocate()對當(dāng)前 OldPage 所占用的內(nèi)存進行釋放。yCj28資訊網(wǎng)——每日最新資訊28at.com

接下來,我們看一下 SweepPage() 的主要實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

代碼較長,這里只截出了關(guān)鍵部分,在遍歷 OldPage 上的對象時,會先取出對象的 tags 判斷是否被標(biāo)記,如果對象被標(biāo)記了,則會清除對象的標(biāo)記位,然后將對象的大小累加到 used_in_bytes 中;如果沒有標(biāo)記, 則會創(chuàng)建一個 free_end 變量來記錄可以清理的結(jié)束位置,然后通過 while 循環(huán)來遍歷后續(xù)對象,直到遍歷到一個已標(biāo)記的對象,這樣做的目的是為了一次性計算出可連續(xù)清理的內(nèi)存,這樣的話就可以釋放出一個盡可能大的內(nèi)存空間來分配新的對象,可以看到,最終是通過 free_end - current 來計算出可連續(xù)釋放的空間,然后將可釋放的起始地址與大小記錄到 freelist 中,這樣后續(xù)對象在分配內(nèi)存時 就可以通過 OldPage 的 freelist 來獲取到內(nèi)存。yCj28資訊網(wǎng)——每日最新資訊28at.com

至此,Mark-Sweep 的流程就結(jié)束了。Mark-Sweep 作為老年代 GC 最常用的算法,也存著一些缺點,例如碎片化問題。為了解決碎片化問題,就引入了 Mark-Compact。接下來,我們就介紹一下老年代的另外一個 GC 算法 Mark-Compact。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

Mark-Compact

yCj28資訊網(wǎng)——每日最新資訊28at.com

Mark-Compact 主要分為兩個部分,分別是標(biāo)記和壓縮。而標(biāo)記階段和上文中介紹的 Mark-Sweep 對象標(biāo)記是保持一致,所以這里就不再介紹標(biāo)記階段,主要看一下壓縮階段的實現(xiàn)。

yCj28資訊網(wǎng)——每日最新資訊28at.com

老年代內(nèi)存在申請內(nèi)存分頁之后,會在當(dāng)前內(nèi)存分頁尾部分配一塊內(nèi)存來存放 ForwardingPage 對象。而這個 ForwardingPage 對象中則是存放了多個 ForwardingBlock,F(xiàn)orwardingBlock 中則是存放當(dāng)前分頁存活對象即將轉(zhuǎn)移的新地址。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

從上方代碼可以看出,整個壓縮階段主要分成兩個步驟,一個 PlanPage(),一個是 SlidePage()。這里先介紹 PlanPage(),它的主要作用是計算所有存活對象需要移動的新地址,并將其記錄到上文中所提到的 ForwardingPage 的 ForwardingBlock 中。由于跟CopyingGC 不同的是在同一塊區(qū)域操作,所以可能會出現(xiàn)移動時把存活對象覆蓋掉的情況,所以這一步只做存活對象新地址的計算。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,PlanPage() 并沒有直接從 object_start() (分頁內(nèi)存中的第一個對象的地址)進行處理,而是調(diào)用了 PlanBlock() 來進行處理,顧名思義,內(nèi)存分頁會劃分成多個 Block,然后對 Block 分別處理,并將計算出的新地址記錄到 ForwardingBlock 中。接下來我們看一下 PlanBlock() 的具體實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,PlanBlock() 會先通過 first_object 計算得到 Block 中的起始地址,然后通過 kBlockSize 計算的得出 Block 的結(jié)束地址,然后通過起始地址遍歷 Block 中的所有對象。如果當(dāng)前遍歷到的對象被標(biāo)記了,則會通過 RecordLive() 記錄到 ForwardingBlock 中,而 RecordLive() 內(nèi)部使用了一些黑魔法,它并沒有直接保存對象轉(zhuǎn)移的新地址,而是先計算出對象在 Block 中的偏移量,然后通過這個偏移量對 live_bitvector_ 進行位移計算得到一個 bit 位, 用這個 bit 位來記錄該對象是否存活。當(dāng) Block 中的所有對象都遍歷完成后,通過 set_new_address() 整個 Block 中對象轉(zhuǎn)移的新地址。所以,每個 Block 都只會存儲一個新地址,那 Block 中的所有存活對象,怎么根據(jù)這個新地址進行移動呢?這就要介紹一下 SlidePage() 中的 SlideBlock(),這里我們就不再關(guān)注 SlidePage() 了,因為它的實現(xiàn)和 PlanPage() 差不多,里面循環(huán)調(diào)用了 SlideBlock(),這里我們直接看一下 SlideBlock() 的實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

SlideBlock() 代碼較長,只截了其中最關(guān)鍵的一部分,可以看到,在遍歷 Block 中的對象時,會先通過 forwarding_block 獲取到對象的新地址,然后將新地址轉(zhuǎn)化為 UntaggedObject 的對象指針,然后通過 memmove() 將舊地址中的數(shù)據(jù)移動到新地址,最后通過 VisitPointers() 將對象中的屬性所引用的對象指針進行更新。yCj28資訊網(wǎng)——每日最新資訊28at.com

看到這里,我們對 Compact(內(nèi)存壓縮) 已經(jīng)有了一個大致的了解,CompactTask 先通過 PlanPage() 遍歷老年代分頁內(nèi)存上的所有標(biāo)記對象,然后計算出他們將要移動的新地址,然后再通過 SlidePage() 再次遍歷老年代內(nèi)存上的對象,將存活的對象移動到新地址,在對象移動的同時去更新對象屬性中的對象指針(也就是對象之間的引用關(guān)系)。yCj28資訊網(wǎng)——每日最新資訊28at.com

接下來看一下 VisitPointers() 的實現(xiàn),看一下對象屬性中的對象指針是如何更新的。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,VisitPointers() 會遍歷對象屬性中的所有對象指針,然后調(diào)用 ForwardPointer() 完成對象指針更新。接下來,我們看一下 ForwardPointer() 的實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,它會先通過 OldPage::of() 找到對象指針?biāo)趦?nèi)存分頁,然后獲取到它的 forwarding_page,通過 forwarding_page 查詢出對象的新地址,然后再用新地址更新對象指針。yCj28資訊網(wǎng)——每日最新資訊28at.com

在 PlanPage() 和 SlidePage() 執(zhí)行結(jié)束之后,Compact 流程就接近尾聲了,剩下的就是掃尾工作了,其實還是對象引用的更新,SlidePage() 中移動對象同時雖然會更新對象指針,但是這僅僅是處理了老年代內(nèi)存分頁上對象之間的引用,但是像新生代對象,它的對象屬性中可能也存在老年代對象的對象指針,它們之間的引用關(guān)系還沒有被更新。所以,接下來就是更新非老年代對象中的對象指針。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

通過注釋可以看出,接下來的就主要是對 large_page 與新生代內(nèi)存中的對象進行對象指針更新。至此,Compact 的流程就基本結(jié)束了。yCj28資訊網(wǎng)——每日最新資訊28at.com

通過以上分析,可以發(fā)現(xiàn),相對于 CopyingGC、Mark-Sweep,Mark-Compact 也存在著優(yōu)缺點。yCj28資訊網(wǎng)——每日最新資訊28at.com

優(yōu)點:yCj28資訊網(wǎng)——每日最新資訊28at.com

可有效利用堆:比起 CopyingGC 算法,它可利用的堆內(nèi)存空間更大;同時也不存在內(nèi)存碎片,所以比起 Mark-Sweep,可利用空間也是更大。yCj28資訊網(wǎng)——每日最新資訊28at.com

缺點:yCj28資訊網(wǎng)——每日最新資訊28at.com

壓縮過程有計算成本。整個標(biāo)記壓縮流程必須對整個堆進行 3 次遍歷,執(zhí)行該算法花費的時間是和堆大小成正比的,吞吐量要劣于其他算法。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

并發(fā)標(biāo)記

yCj28資訊網(wǎng)——每日最新資訊28at.com

在 GC 過程中,會通過“安全點”的方式掛起所有 isolate 線程,isolate 掛起就意味著無法立即響應(yīng)用戶的操作,為了減少 isolate 掛起時間,就引入了并發(fā)標(biāo)記。并發(fā)標(biāo)記會使 GC 線程在標(biāo)記階段時,與 isolate 并發(fā)執(zhí)行。因為 GC 標(biāo)記是一個比較耗時的過程,如果 isolate 線程能夠和 GC 標(biāo)記 同時執(zhí)行,就不會導(dǎo)致用戶界面長時間卡頓,從而提高用戶體驗。

但是,并發(fā)標(biāo)記并不是在所有場景下都使用的。當(dāng)內(nèi)存到達一定閾值,相當(dāng)吃緊的情況下,還是會采取并行標(biāo)記的方式,掛起所有 isolate 線程,直到整個 GC 流程結(jié)束。yCj28資訊網(wǎng)——每日最新資訊28at.com

接下來,我們來看一下 StartConcurrentMark() 是如何實現(xiàn)并發(fā)標(biāo)記的。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,StartConcurrentMark() 會先 通過 ResetSlices() 計算分片個數(shù),新生代對象作為 GC 標(biāo)記的根對象,為了提高標(biāo)記效率,多個標(biāo)記線程會同時遍歷新生代對象,所以通過分片的方式可以讓多個標(biāo)記線程能夠盡然有序的遍歷新生代分頁內(nèi)存上的對象。接下來,就是通過 thread_pool() 來分配多個線程來執(zhí)行標(biāo)記任務(wù) ConcurrentMarkTask,num_tasks 就是并發(fā)標(biāo)記的線程個數(shù),之所以減 1,是因為當(dāng)前主線程也作為標(biāo)記任務(wù)的一員,但是主線程只會調(diào)用 IterateRoots() 來遍歷根對象,后續(xù) work_list_ 中的對象則是通過 thread_pool() 重新分配一個線程來執(zhí)行 ConcurrentMarkTask,主線程的標(biāo)記任務(wù)到此就基本結(jié)束了,接下來就是通過 root_slices_monitor_ 同步鎖,等待所有根對象遍歷完成。剩下的都交給了 ConcurrentMarkTask 來完成。接下來,我們就看一下 ConcurrentMarkTask 的實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,ConcurrentMarkTask 在調(diào)用 IterateRoots() 完成根對象標(biāo)記之后,就會調(diào)用 DrainMarkingStack() 來遍歷 work_list_ 中的對象,而 DrainMarkingStack() 的實現(xiàn)在上文的對象標(biāo)記中已經(jīng)介紹過了,這里就不再贅述了。yCj28資訊網(wǎng)——每日最新資訊28at.com

有了并發(fā)標(biāo)記,GC 標(biāo)記任務(wù)和 isolate 線程就可以并發(fā)執(zhí)行,這樣就避免了 GC 標(biāo)記因掛起 isolate 線程帶來的長時間卡頓。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

寫入屏障

yCj28資訊網(wǎng)——每日最新資訊28at.com

有了并發(fā)標(biāo)記之后,就會引入另外一個問題。因為并發(fā)標(biāo)記允許 isolate 線程與 GC 標(biāo)記線程同時執(zhí)行,所以就存在標(biāo)記過程中,isolate 線程修改了對象引用。也就是說,兩個對象被標(biāo)記線程遍歷之后,一個未被標(biāo)記的對象引用 在 isolate 線程中被賦值給一個已經(jīng)標(biāo)記對象的屬性,此時,未標(biāo)記對象被標(biāo)記對象所引用,此時未標(biāo)記的對象理論上已經(jīng)被根對象間接引用,應(yīng)該 GC 過程中不能被清理,但是因為并發(fā)標(biāo)記階段沒有被標(biāo)記,所以在最終 Sweep 階段將會被清理,這明顯出現(xiàn)了錯誤。為了解決這個問題,就引入了寫入屏障。

在標(biāo)記過程中,當(dāng)未標(biāo)記的對象(TARGET)被賦值給已標(biāo)記對象(SOURCE)的屬性時,此時 TARGET 對象理應(yīng)也該被標(biāo)記,為了防止 TARGET 對象逃逸標(biāo)記,寫入屏障會對未標(biāo)記的 TARGET 對象進行檢查。yCj28資訊網(wǎng)——每日最新資訊28at.com

如果 TARGET 對象與 SOURCE 對象都是老年代對象時,寫入屏障就會對未標(biāo)記的 TARGET 對象進行標(biāo)記,并將該對象加入到標(biāo)記隊列,致使該對象關(guān)聯(lián)的其他對象也會被標(biāo)記。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,SOURCE對象在保存 TARGET 對象指針建立引用關(guān)系時,會判斷 TARGET 對象 是否是 heap 上的對象,如果是 heap 上的對象,則會調(diào)用 CheckHeapPointerStore() 對其進行檢查。接下來,我們看一下 CheckHeapPointerStore() 的具體實現(xiàn)。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

在 CheckHeapPointerStore() 方法中,會判斷 TARGET 對象是否是新生代對象,如果是新生代對象,則會調(diào)用 EnsureInRememberedSet() 將 SOURCE 對象加入到 RememberedSet 中(主要作用于上文中介紹的 Scavenge,新生代對象轉(zhuǎn)移時能夠更新老年代對象中存儲的對象指針),但并未對 TARGET 對象進行特殊處理,這是因為新生代對象在老年代 GC 標(biāo)記過程中本身就作為根對象,而且在標(biāo)記結(jié)束時,會重新遍歷這些根對象。接下來,就是非新生代對象,非新生代對象只有兩種 Smi 和老年代對象,因為在外層函數(shù) StorePointer() 中有判斷是否是 heap 上的對象,所以這里不可能是 Smi,只能是老年對象。老年代對象則調(diào)用 TryAcquireMarkBit() 進行標(biāo)記,標(biāo)記成功后,將其加入到標(biāo)記隊列中(也就是上文中所提到的 work_list_),使其關(guān)聯(lián)到的對象也被遍歷標(biāo)記。yCj28資訊網(wǎng)——每日最新資訊28at.com

有了寫入屏障,就確保了在并發(fā)標(biāo)記時,isolate 修改 heap 對象之間的引用關(guān)系時,不會導(dǎo)致對象遺漏標(biāo)記被 GC 清理。但是寫入屏障也會帶來額外的開銷,為了減少這種開銷,就要介紹到另外一個優(yōu)化:寫入屏障消除。yCj28資訊網(wǎng)——每日最新資訊28at.com

yCj28資訊網(wǎng)——每日最新資訊28at.com

寫入屏障消除

yCj28資訊網(wǎng)——每日最新資訊28at.com

通過上面對寫入屏障的介紹,我們可以得知,當(dāng) TARGET對象賦值給 SOURCE 對象的屬性時,寫入屏障主要作用于以下兩種情況:
  • SOURCE 對象是老年代對象,而 TARGET 對象是新生代對象,且 SOURCE 對象不在 RememberedSet 中。
  1. 此場景下,會將 SOURCE 對象加入到 RememberedSet 中,作用于新生代 GC Scavenge。yCj28資訊網(wǎng)——每日最新資訊28at.com

  • SOURCE 對象是老年代對象,TARGET 對象也是老年代且沒有被標(biāo)記,此時 GC 線程正在標(biāo)記階段。
  1. 此場景下,會對 TARGET 對象進行標(biāo)記,并將 TARGET 對象加入到 work_list_ 中。yCj28資訊網(wǎng)——每日最新資訊28at.com

而在這兩種情況下,其實也存在著一些場景無需寫入屏障,只要在編譯時能夠判定出是這些場景,就可以消除這類的寫入屏障。我們簡單列舉一些場景:yCj28資訊網(wǎng)——每日最新資訊28at.com

  • TARGET 對象是一個常量(因為常量必定是老年代對象,即使在賦值給 SOURCE 對象時沒有被標(biāo)記,也會在 GC 過程中通過常量池被標(biāo)記)。
  • TARGET 對象是 bool 類型(bool 類型只可能有三種情況:null、false、true,而這三個值都是常量,所以如果是 bool 類型,必定是一個常量)。
  • TARGET 對象是小整形(小整形在上文中也介紹過,指針即對象,所以他不算是 heap 上的對象)。
  • SOURCE 對象和 TARGET 對象是同一個對象(自身屬性持有自己)。
  • SOURCE 對象是新生代對象或者是已經(jīng)被添加至 RememberedSet 的老年代對象(上文中也介紹過,新生代對象作為根對象,在標(biāo)記結(jié)束時,會重新遍歷這些根對象)。

我們可以知道,當(dāng) SOURCE 對象是通過 Object::Allocate() 進行分配的(而不是從 heap 中加載的),它的 Allocate() 和它最近一次的屬性賦值之間如果不存在觸發(fā) GC 的 instruction,那它的屬性賦值也可以消除寫入屏障。這是因為 Object::Allocate() 分配的對象一般情況下是新生代對象,如果是老年代對象,在 Allocate() 時會被直接修改為標(biāo)記狀態(tài), 預(yù)先添加至 RememberedSet 和標(biāo)記隊列 work_list_ 中。yCj28資訊網(wǎng)——每日最新資訊28at.com

container <- AllocateObject<intructions that do not trigger GC>StoreInstanceField(container, value, NoBarrier)

在此基礎(chǔ)上, 當(dāng) SOURCE 對象的 Allocate() 和它的屬性賦值之間不存在函數(shù)調(diào)用,我們可以進一步來消除屬性賦值帶來的寫入屏障。這是因為在 GC 之后,Thread::RestoreWriteBarrierInvariant() 會將 ExitFrame 下方的棧幀中的所有老年代對象添加至 RememberedSet 和標(biāo)記隊列 work_list_ 中(ExitFrame 是表示函數(shù)調(diào)用棧退出的特殊幀,當(dāng)函數(shù)執(zhí)行完畢時,虛擬機會將 ExitFrame 推入棧頂,以表示函數(shù)的退出)。yCj28資訊網(wǎng)——每日最新資訊28at.com

container <- AllocateObject<instructions that cannot directly call Dart functions>StoreInstanceField(container, value, NoBarrier)

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,Thread::RestoreWriteBarrierInvariant() 遍歷到 ExitFrame時,會開始掃描下一個棧幀,會通過 RestoreWriteBarrierInvariantVisitor 遍歷棧幀中的所有對象,并將其 RememberedSet 和標(biāo)記隊列 work_list_ 中。所以,這個寫入屏障消除必須保證 AllocateObject 和 StoreInstanceField 必須在同一個DartFrame 中,如果它們之間存在函數(shù)調(diào)用,就無法確保它們在ExitFrame 下方的同一個 DartFrame 中。yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,寫入屏障消除通過在編譯時和運行時的一些推斷,避免了一些不必要的額外開銷。yCj28資訊網(wǎng)——每日最新資訊28at.com

四、Safepoints

任何可以分配、讀寫 Heap 的非 GC 線程或任務(wù)可以稱為 mutator,因為它可以修改對象之間的引用關(guān)系。

GC 的某些階段要求 Heap 不允許被 mutator 使用,我們稱之為 safepoint operations。例如:老年代 GC 并發(fā)標(biāo)記時的根對象標(biāo)記,以及標(biāo)記結(jié)束后的對象清理。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

為了執(zhí)行這些操作,所有 mutator 都需要暫時停止訪問 Heap,此時 mutator 就到達了“安全點”。已經(jīng)達到安全點的 mutator 將不能訪問 Heap,直到 safepoint operations 完成。yCj28資訊網(wǎng)——每日最新資訊28at.com

在 GC 過程中,GcSafepointOperationScope 會致使當(dāng)前線程等待所有 isolate 線程到達“安全點”之后才能繼續(xù)執(zhí)行,這樣就保證了后續(xù)流程中 isolate 線程不會修改 Heap 上的對象。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

NotifyThreadsToGetToSafepointLevel() 會通知所有 isolate 線程當(dāng)前需要掛起。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

而 WaitUntilThreadsReachedSafepointLevel() 會等待所有 isolate 線程進入安全點。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

對應(yīng) isolate 在發(fā)送 OOB 消息時,會處理當(dāng)前線程狀態(tài)中的 interrupt 標(biāo)記位,如果當(dāng)前線程狀態(tài)的 interrupt 標(biāo)記位滿足 kVMInterrupt,則會調(diào)用 CheckForSafepoint() 檢查當(dāng)前 isolate 是否被請求進入“安全點”,如果當(dāng)前 isolate 的 safepoint_state_ 被標(biāo)記需要進入“安全點”,則會調(diào)用 BlockForSafepoint() 標(biāo)記 safepoint_state_ 已進入“安全點”,并掛起當(dāng)前線程,直到“安全點操作”結(jié)束。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

因此,當(dāng) isolate 發(fā)送 OOB 消息時,就會觸發(fā)“安全點”檢查,從而導(dǎo)致線程掛起進入“安全點”。那什么是 OOB 消息,而 OOB 消息發(fā)送又是何時被觸發(fā)的,這就要簡單介紹一下 isolate 的事件驅(qū)動模型。正如大部分的 UI 平臺,isolate 也是通過消息隊列實現(xiàn)的事件驅(qū)動模型。不過,在 isolate 中有兩個消息隊列,一個隊列是普通消息隊列,另一個隊列叫 OOB 消息隊列,OOB 是 "out of band" 縮寫,翻譯為帶外消息,OOB 消息用來傳送一些控制類消息,例如從當(dāng)前 isolate 生成(spawn)一個新的 isolate。我們可以在當(dāng)前 isolate 發(fā)送OOB消息給新 isolate,從而控制新 isolate。比如,暫停(pause),恢復(fù)(resume),終止(kill)等。yCj28資訊網(wǎng)——每日最新資訊28at.com

有了“安全點”,就保證了其他線程在 GC 過程中不能隨意訪問、操作 Heap 上的對象,確保 GC 過程中一些重要操作(根對象遍歷、內(nèi)存清理、內(nèi)存壓縮等等) 不受其他線程影響。yCj28資訊網(wǎng)——每日最新資訊28at.com

五、GC問題定位

先看一下 GC 的報錯堆棧:

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

可以看到,問題發(fā)生在 GC 過程中的對象遍歷標(biāo)記。起初,猜想會不會是多個 isolate 線程都觸發(fā)了 GC,多線程 GC 導(dǎo)致的,但是看了 Safepoints 實現(xiàn)之后,發(fā)現(xiàn)這種情況不可能存在,于是排除了此猜想。yCj28資訊網(wǎng)——每日最新資訊28at.com

因為 DartVM 中的老年代內(nèi)存分頁是通過 OldPage 進行管理的,在這些 OldPage 中,除了 code pages,其他 OldPage 都是可讀可寫的。yCj28資訊網(wǎng)——每日最新資訊28at.com

而 DartVM 也提供了相應(yīng)的 API 來修改 OldPage 的權(quán)限。yCj28資訊網(wǎng)——每日最新資訊28at.com

  • PageSpace::WriteProtectCode()
  • PageSpace::WriteProtect()

在 GC 標(biāo)記前,會通過 PageSpace::WriteProtectCode() 將“老年代” 中的  code pages 權(quán)限修改為可讀可寫,以便在標(biāo)記過程中對 Instructions 對象進行標(biāo)記,在 GC 結(jié)束后,再通過 PageSpace::WriteProtectCode() 將  code pages 的權(quán)限修改為只讀。yCj28資訊網(wǎng)——每日最新資訊28at.com

因為 code pages 是用來動態(tài)分配的可執(zhí)行內(nèi)存頁,用來生成 JIT 的機器指令,所以 code pages 只讀權(quán)限導(dǎo)致的 SEGV_ACCERR 問題,只會在 Debug 包上才能復(fù)現(xiàn),所以 release 包不會存在此問題。yCj28資訊網(wǎng)——每日最新資訊28at.com

而 PageSpace::WriteProtect() 也可以修改 OldPage 對應(yīng)分頁的讀寫權(quán)限,該方法可以將“老年代”上的所有 OldPage 修改為只讀權(quán)限。目前通過搜索代碼,發(fā)現(xiàn)只有一個調(diào)用時機,就是 ioslate 退出時,清理 ioslate 時會通過 WritableVMIsolateScope 對象的析構(gòu)會將 "老年代" 上的 所有 OldPage 改為只讀。OldPage 修改為只讀之后,再對 OldPage 上的對象進行標(biāo)記時就會出現(xiàn)問題。通過模擬 WritableVMIsolateScope 對象的析構(gòu),也復(fù)現(xiàn)了和線上完全一模一樣的 crash 堆棧。但是 isolate 正常情況下是不會退出的,所以在前期排除了這種可能。yCj28資訊網(wǎng)——每日最新資訊28at.com

后來,還是把猜測轉(zhuǎn)向了寫入屏障消除,會不是寫入屏障消除導(dǎo)致了對象逃逸了 GC 標(biāo)記,致使所在 OldPage 被清理釋放,再次觸發(fā) GC,遍歷到此對象指針時,對象所在的內(nèi)存已經(jīng)被釋放,野指針導(dǎo)致的 SEGV_ACCERR 問題。如果是這種情況的話,想到了一個臨時的解決方案,在 GC 標(biāo)記過程中,對 ObjectPtr 所指向地址做校驗,判斷是否是一個合法地址。因為標(biāo)記訪問的對象對存儲在 OldPage 上,所以我們只判斷一下該地址在不在 當(dāng)前"老年代"的 OldPage 的內(nèi)存區(qū)域內(nèi),如果地址在 OldPage 內(nèi)存區(qū)域內(nèi),說明 ObjectPtr 所指向的對象所在 OldPage 還存在,沒有被釋放,此塊內(nèi)存區(qū)域肯定是可以訪問的。yCj28資訊網(wǎng)——每日最新資訊28at.com

修復(fù)代碼

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

通過 PageSpace::ContainsUnsafe(uword addr) 方法,來判斷對象地址是否在 "老年代" 分頁內(nèi)存上,這個方法本來是輕量級的,但是 GC 過程中,需要標(biāo)記大量對象,每次標(biāo)記都要進行這個判斷,導(dǎo)致此方法的總開銷較大,整個 GC 時間被拉長。實測下來,每次 GC,都會導(dǎo)致界面 3~5s 的卡頓。所以,此方法還需要優(yōu)化。在上文中,也介紹過并發(fā)標(biāo)記,理論上 GC 標(biāo)記 和 isolate 是并發(fā)執(zhí)行的,不會影響到用戶交互。但是,GC 標(biāo)記并不是整個流程都和 isolate 并發(fā)執(zhí)行的,上文中也提到過 GcSafepointOperationScope,在 GC 標(biāo)記之前,會通過 GcSafepointOperationScope 掛起除當(dāng)前線程的所有 isolate 線程,直到當(dāng)前 GC 方法執(zhí)行結(jié)束,如果并發(fā)標(biāo)記階段,則是標(biāo)記方法執(zhí)行結(jié)束,上文中也提到過,GC 標(biāo)記的主線程會等待所有根對象標(biāo)記結(jié)束,所以根對象標(biāo)記結(jié)束后,才會進入真正的并發(fā)標(biāo)記階段。因為大部分問題都是發(fā)生在 work_list_ 中的對象標(biāo)記,我們是不是可以直接忽略根對象的標(biāo)記,在根對象標(biāo)記之后,才開啟對象指針校驗(這樣就只能保證 work_list_ 中的對象標(biāo)記,根對象標(biāo)記還是存在問題,但是至少能減少問題出現(xiàn)的頻次)。yCj28資訊網(wǎng)——每日最新資訊28at.com

于是通過 GCMarker 中 root_slices_finished_ 變量來判斷根對象是否標(biāo)記結(jié)束,結(jié)束之后,才開啟對象指針校驗。修改之后,確實不存在卡頓了,于是就開啟了上線灰度。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

但是上線后,并不是很理想,GC 問題還是存在。既然猜測是寫入屏障消除導(dǎo)致的,干脆就大膽一點,直接把寫入屏障消除這一優(yōu)化給移除掉。寫入屏障消除這一優(yōu)化移除灰度上線之后,發(fā)現(xiàn) GC 問題還是存在。此時,思緒萬千,難道真的是 PageSpace::WriteProtect() 導(dǎo)致的,為了驗證這一猜測,于是就在對象標(biāo)記之前加入了 RELEASE_ASSERT,判斷老年代分頁內(nèi)存是否真的被修改為了只讀權(quán)限。yCj28資訊網(wǎng)——每日最新資訊28at.com

上線之后,果不其然,GC 問題的堆棧信息發(fā)生了改變,錯誤正是新加入的斷言。這就說明,老年代分頁內(nèi)存確實被修改為了只讀權(quán)限,此時去修改對象的標(biāo)記位肯定是有問題。yCj28資訊網(wǎng)——每日最新資訊28at.com

圖片圖片yCj28資訊網(wǎng)——每日最新資訊28at.com

當(dāng)我們準(zhǔn)備更近一步時,卻因為高頻 GC 問題的幾臺設(shè)備不再使用,失去了可用于灰度的設(shè)備,導(dǎo)致無法進一步去驗證問題。也因為這幾臺高頻 GC 問題設(shè)備的下線,GC 問題在 crash 占比中顯得不那么重要,問題就這樣淡出了我們的視線。不過還是希望后續(xù)能夠找到根因,徹底解決此問題。yCj28資訊網(wǎng)——每日最新資訊28at.com

六、總結(jié)&感悟

通過對 DartVM GC 整個流程的梳理,才真正理解了什么是分代 GC,新生代和老年代在 GC 上是相互隔離的,使用著不同的 GC 算法,而老年代自身也存在兩種 GC 算法 Mark-Sweep 和 Mark-Compact。通過對 GC 問題的定位,也讓我們更加意識到日志的重要性,在不能復(fù)現(xiàn)問題的前提下,日志才是排查問題的重要線索。DartVM GC 流程中的埋點日志不僅能幫助我們來排查問題,也能反映出 Dart 代碼中是否存在內(nèi)存泄漏問題,例如對 GC 過程中 heap 的使用情況進行日志輸出。后續(xù),也希望能夠?qū)?GC 的日志進行持久化,便于回?fù)疲玫胤治鰬?yīng)用的內(nèi)存使用情況和 GC 頻率,為今后的應(yīng)用性能優(yōu)化提供思路和方向。

本文鏈接:http://www.www897cc.com/showinfo-26-74674-0.htmlDartVM GC 深度剖析

聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: 深度剖析C++類的大小:內(nèi)存中的精密布局探秘

下一篇: 面試官:如何防止短信盜刷和短信轟炸?

標(biāo)簽:
  • 熱門焦點
  • 7月安卓手機性能榜:紅魔8S Pro再奪榜首

    7月份的手機市場風(fēng)平浪靜,除了紅魔和努比亞帶來了兩款搭載驍龍8Gen2領(lǐng)先版處理器的新機之外,別的也想不到有什么新品了,這也正常,通常6月7月都是手機廠商修整的時間,進入8月份之
  • Raft算法:保障分布式系統(tǒng)共識的穩(wěn)健之道

    1. 什么是Raft算法?Raft 是英文”Reliable、Replicated、Redundant、And Fault-Tolerant”(“可靠、可復(fù)制、可冗余、可容錯”)的首字母縮寫。Raft算法是一種用于在分布式系統(tǒng)
  • 不容錯過的MSBuild技巧,必備用法詳解和實踐指南

    一、MSBuild簡介MSBuild是一種基于XML的構(gòu)建引擎,用于在.NET Framework和.NET Core應(yīng)用程序中自動化構(gòu)建過程。它是Visual Studio的構(gòu)建引擎,可在命令行或其他構(gòu)建工具中使用
  • 一篇聊聊Go錯誤封裝機制

    %w 是用于錯誤包裝(Error Wrapping)的格式化動詞。它是用于 fmt.Errorf 和 fmt.Sprintf 函數(shù)中的一個特殊格式化動詞,用于將一個錯誤(或其他可打印的值)包裝在一個新的錯誤中。使
  • 十個簡單但很有用的Python裝飾器

    裝飾器(Decorators)是Python中一種強大而靈活的功能,用于修改或增強函數(shù)或類的行為。裝飾器本質(zhì)上是一個函數(shù),它接受另一個函數(shù)或類作為參數(shù),并返回一個新的函數(shù)或類。它們通常用
  • 微信語音大揭秘:為什么禁止轉(zhuǎn)發(fā)?

    大家好,我是你們的小米。今天,我要和大家聊一個有趣的話題:為什么微信語音不可以轉(zhuǎn)發(fā)?這是一個我們經(jīng)常在日常使用中遇到的問題,也是一個讓很多人好奇的問題。讓我們一起來揭開這
  • Python異步IO編程的進程/線程通信實現(xiàn)

    這篇文章再講3種方式,同時講4中進程間通信的方式一、 Python 中線程間通信的實現(xiàn)方式共享變量共享變量是多個線程可以共同訪問的變量。在Python中,可以使用threading模塊中的L
  • 2022爆款:ROG魔霸6 冰川散熱系統(tǒng)持續(xù)護航

    喜逢開學(xué)季,各大商家開始推出自己的新產(chǎn)品,進行打折促銷活動。對于忠實的端游愛好者來說,能夠擁有一款夢寐以求的筆記本電腦是一件十分開心的事。但是現(xiàn)在的
  • Meta盲目擴張致超萬人被裁,重金押注元宇宙而前景未明

    圖片來源:圖蟲創(chuàng)意日前,Meta創(chuàng)始人兼CEO 馬克&middot;扎克伯發(fā)布公開信,宣布Meta計劃裁員超11000人,占其員工總數(shù)13%。他公開承認(rèn)了自己的預(yù)判失誤:&ldquo;不僅
Top 主站蜘蛛池模板: 方城县| 宜君县| 江西省| 亳州市| 东明县| 茌平县| 正安县| 涟源市| 全椒县| 永川市| 九龙县| 霍山县| 神池县| 长丰县| 南溪县| 松江区| 色达县| 邵东县| 岗巴县| 屯门区| 双鸭山市| 咸丰县| 通城县| 潞西市| 自治县| 桃园县| 宣汉县| 和政县| 惠州市| 宝山区| 台南市| 南靖县| 阿城市| 额敏县| 南宁市| 长顺县| 顺平县| 恩施市| 涟源市| 万山特区| 宁蒗|