標記-清除的GC機制要包括以下四個流程。

UE4 GC機制解析(一):GC資訊收集

接下來介紹UE4中的資訊收集過程。

引擎載入流程中收集資訊

由於記憶體資訊,包括型別資訊在編譯期就已經確定,所以在反射支援的情況下,便能夠在初始化的時候進行收集。之前介紹反射時,有講到引擎載入的流程中會呼叫

ProcessNewlyLoadedUObjects()

函式(多次)。

UE4 GC機制解析(一):GC資訊收集

ProcessNewlyLoadedUObjects()

函式中會呼叫

UClass::AssembleReferenceTokenStreams()

,後者是一個靜態函式,直接透過類名呼叫,程式碼也很簡短:

void

UClass

::

AssembleReferenceTokenStreams

()

{

SCOPED_BOOT_TIMING

“AssembleReferenceTokenStreams (can be optimized)”

);

// Iterate over all class objects and force the default objects to be created。 Additionally also

// assembles the token reference stream at this point。 This is required for class objects that are

// not taken into account for garbage collection but have instances that are。

for

FRawObjectIterator

It

false

);

It

++

It

// GetDefaultObject can create a new class, that need to be handled as well, so we cannot use TObjectIterator

{

if

UClass

*

Class

=

Cast

<

UClass

>

((

UObject

*

)(

It

->

Object

)))

{

// Force the default object to be created (except when we‘re in the middle of exit purge -

// this may happen if we exited PreInit early because of error)。

//

// Keep from handling script generated classes here, as those systems handle CDO

// instantiation themselves。

if

GExitPurge

&&

Class

->

HasAnyFlags

RF_BeingRegenerated

))

{

Class

->

GetDefaultObject

();

// Force the default object to be constructed if it isn’t already

}

// Assemble reference token stream for garbage collection/ RTGC。

if

Class

->

HasAnyFlags

RF_ClassDefaultObject

&&

Class

->

HasAnyClassFlags

CLASS_TokenStreamAssembled

))

{

Class

->

AssembleReferenceTokenStream

();

}

}

}

}

函式的執行流程如下:

遍歷所有的UClass(每一個用

UCLASS()

宏修飾的類都會生成一個UClass物件)

對每個UClass生成一個預設的物件(指被UClass修飾的類的物件)

對每個UClass,如果沒有收集gc的token stream資訊,則呼叫每個UClass物件的

AssembleReferenceTokenStream()

函式(非靜態)

所以繼續進入

AssembleReferenceTokenStream()

函式(非靜態)。

void

UClass

::

AssembleReferenceTokenStream

bool

bForce

{

// Lock for non-native classes

FScopeLockIfNotNative

ReferenceTokenStreamLock

ReferenceTokenStreamCritical

ClassFlags

&

CLASS_Native

));

UE_CLOG

IsInGameThread

()

&&

IsGarbageCollectionLocked

(),

LogGarbage

Fatal

TEXT

“AssembleReferenceTokenStream for %s called on a non-game thread while GC is not locked。”

),

*

GetFullName

());

if

HasAnyClassFlags

CLASS_TokenStreamAssembled

||

bForce

{

if

bForce

{

ReferenceTokenStream

Empty

();

ClassFlags

&=

~

CLASS_TokenStreamAssembled

}

TArray

<

const

FStructProperty

*>

EncounteredStructProps

// Iterate over properties defined in this class

for

TFieldIterator

<

FProperty

>

It

this

EFieldIteratorFlags

::

ExcludeSuper

);

It

++

It

{

FProperty

*

Property

=

*

It

Property

->

EmitReferenceInfo

*

this

0

EncounteredStructProps

);

}

if

UClass

*

SuperClass

=

GetSuperClass

())

{

// We also need to lock the super class stream in case something (like PostLoad) wants to reconstruct it on GameThread

FScopeLockIfNotNative

SuperClassReferenceTokenStreamLock

SuperClass

->

ReferenceTokenStreamCritical

SuperClass

->

ClassFlags

&

CLASS_Native

));

// Make sure super class has valid token stream。

SuperClass

->

AssembleReferenceTokenStream

();

if

SuperClass

->

ReferenceTokenStream

IsEmpty

())

{

// Prepend super‘s stream。 This automatically handles removing the EOS token。

ReferenceTokenStream

PrependStream

SuperClass

->

ReferenceTokenStream

);

}

}

else

{

UObjectBase

::

EmitBaseReferences

this

);

}

{

check

ClassAddReferencedObjects

!=

NULL

);

const

bool

bKeepOuter

=

true

//GetFName() != NAME_Package;

const

bool

bKeepClass

=

true

//!HasAnyInternalFlags(EInternalObjectFlags::Native) || IsA(UDynamicClass::StaticClass());

ClassAddReferencedObjectsType

AddReferencedObjectsFn

=

nullptr

#if !WITH_EDITOR

// In no-editor builds UObject::ARO is empty, thus only classes

// which implement their own ARO function need to have the ARO token generated。

if

ClassAddReferencedObjects

!=

&

UObject

::

AddReferencedObjects

{

AddReferencedObjectsFn

=

ClassAddReferencedObjects

}

#else

AddReferencedObjectsFn

=

ClassAddReferencedObjects

#endif

ReferenceTokenStream

Fixup

AddReferencedObjectsFn

bKeepOuter

bKeepClass

);

}

if

ReferenceTokenStream

IsEmpty

())

{

return

}

// Emit end of stream token。

static

const

FName

EOSDebugName

“EndOfStreamToken”

);

EmitObjectReference

0

EOSDebugName

GCRT_EndOfStream

);

// Shrink reference token stream to proper size。

ReferenceTokenStream

Shrink

();

check

HasAnyClassFlags

CLASS_TokenStreamAssembled

));

// recursion here is probably bad

ClassFlags

|=

CLASS_TokenStreamAssembled

}

}

程式碼有點長,但總結一下,就是為每個被UClass修飾的類,生成對應的token stream,用於描述GC資訊。同時透過

CLASS_TokenStreamAssembled

標記防止重複生成token stream。

較為詳細的流程如下:

首先加上鎖,防止另外一個執行緒同時進入該函式執行一樣的流程,造成記憶體讀寫衝突

遍歷UClass中的每個Property,呼叫每個Property的

EmitReferenceInfo()

方法,會將UClass物件的指標傳入,主要是為了後續再次呼叫UClass的

EmitObjectReference()

方法,將每個Property的記憶體偏移資訊、型別資訊傳回,並存入到每個UClass物件的

ReferenceTokenStream

成員中。需要說明的是,不同型別的Property的

EmitReferenceInfo()

方法都被覆蓋/重寫(override)了,也就是說每個Property類都有自己的

EmitReferenceInfo()

方法。

如果這個類有父類,則會遞迴地呼叫

AssembleReferenceTokenStream()

方法,收集父類的上述資訊到

ReferenceTokenStream

中,同時將父類的 token stream 新增到自己的 token stream 之前;該步驟會一直持續到

UObjectBase

類,因為這個類沒有父類。

最後完成收集會將

ClassFlags

新增上

CLASS_TokenStreamAssembled

標誌,表示該類的token stream資訊已經收集完畢。

也就是說,沒有被

UProperty

宏修飾的成員就不會加入到GC引用鏈中,每次GC都會被清除掉,同時指標置為

nullptr

,當然也有其他方式避免被清除。

看到這裡的時候我腦子裡出現了三個疑問:

如果是一直遞迴收集父類的記憶體偏移資訊直到

UObjectBase

類,那必然有很多重複收集的資訊,很簡單的例子就是類

UObject

作為整個物件系統的基類,不是會被重複收集很多次嗎?

為什麼需要把父類成員的token stream新增到子類之前呢?

在記憶體中,類成員的偏移資訊是如何記錄的(位元組對齊)?

首先第一個問題,每個

類的token stream是不會被重複記錄

的。經過繼承後,各個類之間的關係,有點類似於一棵多叉樹。

而遞迴呼叫

AssembleReferenceTokenStream()

函式(非靜態)的過程,就是從多叉樹的某個葉子結點,向整棵樹的根結點回溯。每到達一個新結點,首先就會檢查是否該結點是否已經被遍歷過了,也就是看每

UClass::ClassFlags & CLASS_TokenStreamAssembled

是不是為1,即遍歷過就會直接返回,而對每個結點/每個

UClass

處理的過程是加鎖的。所以每個結點都不會被重複遍歷,也就是每個類的 token stream 是不會被重複記錄的。

2、3問題與C++物件記憶體模型和位元組對齊有關,以下介紹。

類成員記憶體資訊收集

token stream與記憶體偏移

很好奇token stream是如何將記憶體偏移資訊以及型別資訊編碼到一個int32的。

看了原始碼和相關的知識以後,結果還是比我想象中的要簡單的,也發現 UE4 將

UProperty

替換成了

FProperty

FProperty

沒有再從

UObject

繼承,而是繼承自

FField

FField

也沒有繼承自

UObject

UE4 GC機制解析(一):GC資訊收集

UE4 GC機制解析(一):GC資訊收集

從函式

EmitReferenceInfo()

開始看,首先發現

FProperty::EmitReferenceInfo()

是一個空的函式。

UE4 GC機制解析(一):GC資訊收集

實際上不同型別的的

EmitReferenceInfo()

是各自實現的:

UE4 GC機制解析(一):GC資訊收集

我看了部分型別的實現,發現基本都是將偏移量以及各自成員的名字傳回到UClass,這裡以

FStructProperty::EmitReferenceInfo()

為例。

void

FStructProperty

::

EmitReferenceInfo

UClass

&

OwnerClass

int32

BaseOffset

TArray

<

const

FStructProperty

*>&

EncounteredStructProps

{

check

Struct

);

if

Struct

->

StructFlags

&

STRUCT_AddStructReferencedObjects

{

UScriptStruct

::

ICppStructOps

*

CppStructOps

=

Struct

->

GetCppStructOps

();

check

CppStructOps

);

// else should not have STRUCT_AddStructReferencedObjects

FGCReferenceFixedArrayTokenHelper

FixedArrayHelper

OwnerClass

BaseOffset

+

GetOffset_ForGC

(),

ArrayDim

ElementSize

*

this

);

OwnerClass

EmitObjectReference

BaseOffset

+

GetOffset_ForGC

(),

GetFName

(),

GCRT_AddStructReferencedObjects

);

void

*

FunctionPtr

=

void

*

CppStructOps

->

AddStructReferencedObjects

();

OwnerClass

ReferenceTokenStream

EmitPointer

FunctionPtr

);

}

if

ContainsObjectReference

EncounteredStructProps

EPropertyObjectReferenceType

::

Strong

|

EPropertyObjectReferenceType

::

Weak

))

{

FGCReferenceFixedArrayTokenHelper

FixedArrayHelper

OwnerClass

BaseOffset

+

GetOffset_ForGC

(),

ArrayDim

ElementSize

*

this

);

FProperty

*

Property

=

Struct

->

PropertyLink

while

Property

{

Property

->

EmitReferenceInfo

OwnerClass

BaseOffset

+

GetOffset_ForGC

(),

EncounteredStructProps

);

Property

=

Property

->

PropertyLinkNext

}

}

}

名字是在反射資訊收集階段的時候,透過生成檔案寫入的。這裡構造了一個

FixedArrayHelper

物件,但是一直沒被用到,構造函數里也會呼叫

UClass::EmitObjectReference()

傳回記憶體資訊,這樣做的意義什麼呢?。然後對

FProperty::ArrayDim

也不是很清楚,我猜測是一個

FProperty

物件可能對應多個成員,所以需要執行多次

UClass::EmitObjectReference()

,或者是處理迴圈引用 。。。

我猜的

。。。

那記憶體偏移量呢? 這裡呼叫了一個函式是

GetOffset_ForGC()

實際也只是返回了一個成員變數而已,也就是說偏移量在對應的

FProperty

物件生成的時候就就已經決定好了。應該是在收集反射資訊的時候,就會填充好這個

Offset_Internal

。回顧一下文章UE4中型別生成檔案分析,發現生成檔案中屬性的部分,有個

STRUCT_OFFSET()

的呼叫:

const

UE4CodeGen_Private

::

FUnsizedIntPropertyParams

Z_Construct_UClass_UHH_Statics

::

NewProp_HHID

=

{

“HHID”

nullptr

EPropertyFlags

0x0010000000000000

UE4CodeGen_Private

::

EPropertyGenFlags

::

Int

RF_Public

|

RF_Transient

|

RF_MarkAsNative

1

STRUCT_OFFSET

UHH

HHID

),

METADATA_PARAMS

Z_Construct_UClass_UHH_Statics

::

NewProp_HHID_MetaData

UE_ARRAY_COUNT

Z_Construct_UClass_UHH_Statics

::

NewProp_HHID_MetaData

))

};

const

UE4CodeGen_Private

::

FPropertyParamsBase

*

const

Z_Construct_UClass_UHH_Statics

::

PropPointers

[]

=

{

const

UE4CodeGen_Private

::

FPropertyParamsBase

*

&

Z_Construct_UClass_UHH_Statics

::

NewProp_HHID

};

到這裡就清晰很多了,透過

STRUCT_OFFSET()

的呼叫可以獲知某個成員在 struct/class 內的偏移量,繼續看

STRUCT_OFFSET()

UE4 GC機制解析(一):GC資訊收集

看到這裡會繼續呼叫

offsetof()

,依然是一個宏,這個宏目前在各個編譯器上都得到了支援,當然開發者也可以自己實現,也就是。。。

#ifndef offsetof

#define offsetof(STRUCTURE,FIELD) ((int)((char*)&((STRUCTURE*)0)->FIELD))

#endif

這裡很巧妙使用了以地址0作為物件地址,然後取成員地址的方式,這樣得到的成員地址就是對應的偏移量。

起初我以為計算成員的偏移量會是一個很複雜的事情,需要考慮位元組對齊以及開發者自定義的對齊之類的。不過這依然是編譯器提供的功能,成員偏移本身也是編譯器來決定的。

token stream記錄與物件模型

在以上分析的基礎上,token stream的記錄過程就很明瞭。繼續看

UClass::EmitObjectReference()

,構造了一個

FGCReferenceInfo

void

UClass

::

EmitObjectReference

int32

Offset

const

FName

&

DebugName

EGCReferenceType

Kind

{

FGCReferenceInfo

ObjectReference

Kind

Offset

);

ReferenceTokenStream

EmitReferenceInfo

ObjectReference

DebugName

);

}

FGCReferenceInfo

會將偏移量和GC型別一起寫入到一個union中,於是成員的資訊就被編碼到了一個uint32中,

FGCReferenceInfo

本身也是4位元組大小。

struct

FGCReferenceInfo

{

FORCEINLINE

FGCReferenceInfo

EGCReferenceType

InType

uint32

InOffset

ReturnCount

0

Type

InType

Offset

InOffset

{

check

InType

!=

GCRT_None

);

check

InOffset

&

~

0x7FFFF

==

0

);

}

union

{

/** Mapping to exactly one uint32 */

struct

{

/** Return depth, e。g。 1 for last entry in an array, 2 for last entry in an array of structs of arrays, 。。。 */

uint32

ReturnCount

8

/** Type of reference */

uint32

Type

5

// The number of bits needs to match TFastReferenceCollector::FStackEntry::ContainerHelperType

/** Offset into struct/ object */

uint32

Offset

19

};

/** uint32 value of reference info, used for easy conversion to/ from uint32 for token array */

uint32

Value

};

};

最後,將編碼後的uint32存入到

FGCReferenceTokenStream::Tokens

中。同時為了方便檢查和除錯,也會把成員的名字存入到

FGCReferenceTokenStream::TokenDebugInfo

中,雖然 GC 整個環節實際上用不到。。。

明確token stream記錄的流程後,再看為什麼父類成員的token stream為什麼要在子類成員之前。

估計很多人已經想到了,因為 C++ 繼承時,子類物件的記憶體佈局中就是將父類成員置為子類之前的。

目前已經有相當多的文章和資料分析了C++的物件模型,這裡僅簡單看下成員的情況。

UE4 GC機制解析(一):GC資訊收集

物件記憶體中按低地址到高地址,依次是虛表指標、父類成員、子類成員。所以在

UClass::ReferenceTokenStream

FGCReferenceTokenStream::Tokens

中,將父類成員的token stream 放前面。