v8內存分配淺談

前言

本文會經過V8中對String對象的內存分配開始分析,對中間出現的源碼進行解讀,v8博大精深,確實有不少東西我也只能根據一些信息推測,有不對的地方還請指正。對heap內存的新生代分配和老生代內存分配的過程解讀。首先,咱們來看一張流程圖,該流程圖給出整個分配過程當中的前期流程圖,其中省略了一些步驟,只給出了關鍵的步驟。html

image1

從String::NewFromUtf8開始

咱們從String::NewFromUtf8這個函數開始,首先咱們來看一下使用,從samples/hello-world.cc中咱們能夠看到node

Local<String> source =
    String::NewFromUtf8(isolate, "'Hello' + ', World!'",
                        NewStringType::kNormal).ToLocalChecked();
複製代碼

這裏出現了一個isolate的指針,是經過segmentfault

Isolate* isolate = Isolate::New(create_params);
複製代碼

語句產生的,這個isolate在v8中是很是重要的一個對象,我感受相似於句柄的做用,一個v8執行實例幾乎全部信息都在上面,包括heap,threadData等等重要信息,這裏主要不講這個就先跳過,接下來的第二個參數,就是一個字符串,第三個參數表示的是String的類型,在源碼中分爲kNormal和kInternalized兩個類型,等會讓咱們還會講到他們。api

NewFromUtf8方法在src/api.cc中定義以下:緩存

NEW_STRING(isolate, String, NewFromUtf8, char, data, type, length);
複製代碼

NEW_STRING的宏定義就在上面代碼的上方,主要流程代碼以下:數據結構

i::Handle<i::String> handle_result =                                   \
    NewString(i_isolate->factory(), type,                              \
              i::Vector<const Char>(data, length))                     \
        .ToHandleChecked();                                            \
result = Utils::ToLocal(handle_result);      
複製代碼

在這裏咱們能夠看到函數是經過NewString方法來獲取到string的handle,其中isolate->factory()返回的i::Factory對象在代碼中的註釋ide

Interface for handle based allocation.
複製代碼

由此咱們可見這個對象包含了全部分配內存並生成對應handle對象的方法。下面是NewString的代碼:函數

if (type == v8::NewStringType::kInternalized) {
	return factory->InternalizeUtf8String(string);
}
return factory->NewStringFromUtf8(string);
複製代碼

這裏出現了咱們以前提起過的StringType,從註釋裏,咱們能夠發現優化

kNormal:Create a new string, always allocating new storage memory.
kInternalized:Acts as a hint that the string should be created in the old generation heap space and be deduplicated if an identical string already exists.
複製代碼

kNormal的狀況下會建立一個新的string,而且必定會分配內存。而kInternalized則先在老生代(不懂新生代老生代的能夠參考這篇文章淺談V8引擎中的垃圾回收機制)的StringTable(存在Heap中,經過factory對象取得)中搜索是否已經有entry存在,若是存在則直接返回,若是不存在就在老生代中分配內存生成。而KNormal的狀況是調用方法NewStringFromUtf8,該方法的原型以下:ui

MaybeHandle<String> NewStringFromOneByte(
  Vector<const uint8_t> str, PretenureFlag pretenure = NOT_TENURED)
複製代碼

該原型中可知,PretnureFlag爲 NOT_TENURED,咱們能夠看看這個flag的的註釋:

A flag that indicates whether objects should be pretenured when allocated (allocated directly into the old generation) or not (allocated in the young generation if the object size and type allows).
複製代碼

從註釋裏和flag的名字中咱們均可以判斷出,這是一個判斷分配內存是 長存的(TENURED)仍是非長存(NOT_TENURED)的,也就是新生代中仍是老生代中。後面的方法Heap::SelectSpace中很明確的說明了這一點:

static AllocationSpace SelectSpace(PretenureFlag pretenure) {
	return (pretenure == TENURED) ? OLD_SPACE : NEW_SPACE;
}
複製代碼

因此咱們能夠看出,在kNormal的狀況下新創建的string對象都是在新生代的內存區中。這裏咱們直接從NewStringFromOneByte中的代碼流程來講明分配內存的機制,忽略InternalizeUtf8String方法。由於後面實際上是異曲同工,不過InternalizeUtf8String中有很大一部分代碼是在StringTable中尋找是否已經存在了這個String,因此不太適合咱們主題。

Factory::NewStringFromUtf8初現端倪

咱們首先來看一下NewStringFromUtf8的源碼:

// 首先查看字符串是否爲一個全是ASCII符號的字符串
....
if (non_ascii_start >= length) {
	//若是字符串爲ASCII字符串,咱們就不須要將UTF8字符串轉爲ASCII字符串
	return NewStringFromOneByte(Vector<const uint8_t>::cast(string), pretenure);
}
//非ASCII字符串則須要轉義
Access<UnicodeCache::Utf8Decoder>
  decoder(isolate()->unicode_cache()->utf8_decoder());
  ...
  ASSIGN_RETURN_ON_EXCEPTION(
  isolate(), result,
  NewRawTwoByteString(non_ascii_start + utf16_length, pretenure),
  String);
  ...
複製代碼

從註釋中咱們能夠看出,在分配內存時會檢查是否爲ASCII字符串,若是不是,則須要生成一個decoder的對象來處理,在生成了decoder對象後將utf8的字符串轉化成ASCII,而後再生成了Handle對象後再轉化回UTF8字符串。不過,無論是什麼類型在內存分配方面則是同樣的,因此咱們直接挑選NewStringFromOneByte來繼續深刻分析。

NewStringFromOneByte

在NewStringFromOneByte方法中,代碼以下,我只列出了最重要的兩個方法:

//處理length爲0和爲1的狀況
...
//爲handle分配內存
Handle<SeqOneByteString> result;
ASSIGN_RETURN_ON_EXCEPTION(
	isolate(),
	result,
	NewRawOneByteString(string.length(), pretenure),
	String);
...
//將字符串複製進分配的內存
CopyChars(SeqOneByteString::cast(*result)->GetChars(),
        string.start(),
        length);
return result;
複製代碼

從上面的代碼和註釋中咱們能夠看出整個流程,首先經過宏定義以下:

do {                                                               \
	if (!(call).ToHandle(&dst)) {                                    \
	  DCHECK((isolate)->has_pending_exception());                    \
	  return value;                                                  \
	}                                                                \
} while (false)
複製代碼

能夠看出,這個中主要的邏輯在call裏面,也就是NewRawOneByteString函數。而下面對生成的result賦值,下面的copyChars函數就是把string中的字符複製進分配出來的內存,很簡單也跟咱們主題無關就略過不表了。咱們主要來看上面的函數NewRawOneByteString,這個函數的源代碼以下:

//檢查length是否超過最大閾值,檢查length是否大於0
...
//分配內存宏方法
CALL_HEAP_FUNCTION(
	isolate(),
	isolate()->heap()->AllocateRawOneByteString(length, pretenure),
	SeqOneByteString);
複製代碼

首先咱們來看CALL_HEAP_FUNCTION這個宏,源碼太長咱們就不貼了,它主要是執行isolate()->heap()->AllocateRawOneByteString(length, pretenure)這個FUNCTION_CALL,在執行完了之後若是未分配成功,則進行垃圾回收,而後再調用FUNCTION_CALL,在未成功的時候會再重試一次,這裏爲何會是嘗試兩次呢,註釋中給出瞭解釋:

Two GCs before panicking.  In newspace will almost always succeed.
複製代碼

在恐慌以前先作兩次GC,這樣在新生代中基本都會成功。這個恐慌我以爲用的頗有意思,由於在兩次失敗之後,系統會作調用一個函數CollectAllAvailableGarbage,以前的兩次GC調用的是CollectGarbage函數,從這兩個函數的名字咱們就能夠看出,第二個函數作的操做應該是比較大的一次GC,他內部主要是調用CollectGarbage對老生代進行GC,就會涉及Mark和Compact,從以前的文章中咱們能夠意識到,這樣的操做,甚至會形成工做線程的暫停,因此恐慌一詞用在這裏很傳神。 GC方面的代碼不是咱們此次工做的主要內容,因此這裏簡要的敘述一下,咱們接下來看此次的主題函數FUNCTION_CALL,也就是Heap::AllocateRawOneByteString函數。

從Heap::AllocateRawOneByteString進入分配核心邏輯

首先咱們來看流程圖:

image2
從上圖咱們能夠看出,首先經過以前給出的string的length參數算出應該分配的內存空間大小,SizeFor的代碼以下:

return OBJECT_POINTER_ALIGN(kHeaderSize + length * kCharSize);
複製代碼

這個OBJECT_POINTER_ALIGN的宏定義是將分配大小校準,這個手段在相似Linux內核中常常會用到,好比不足一頁大小的內存,擴展到一頁,這樣能夠方便cpu緩存的存取,增長存取速度。接下來的方法SelectSpace咱們以前已經聊過了,這裏就再也不贅述了。這個方法中最核心方法(也是跟咱們主題關聯最緊的方法)就是Heap::AllocateRaw方法,這個咱們等會兒來講。先說最後初始化這個Handle的方法,代碼以下:

//部分初始化須要的object
result->set_map_after_allocation(one_byte_string_map(), SKIP_WRITE_BARRIER);
String::cast(result)->set_length(length);
String::cast(result)->set_hash_field(String::kEmptyHashField);
複製代碼

第一個set_map函數是把one_byte_string_map將map加入到對象中,這個Map應該就是String對象的HiddenClass,裏面存儲了String對象的方法和屬性以及其偏移,這樣作最大的好處就是String對象基本都同樣,對象變更的機會不大,很容易利用到InlineCache的優化(HiddenClass以及InlineCache的文章Hidden Classes)。後面兩個set_length方法和set_hash方法就基本跟他的名字同樣,就沒必要贅言了。 咱們如今來重點討論一下Heap::AllocateRaw,首先從流程圖中咱們能夠看到有五種狀況,對應着四個函數,這裏咱們主要講新生代和老生代的AllocateRaw方法。

分配核心之NewSpace::AllocateRaw

咱們首先來說新生代的內存分配方法,下面是NewSpace::AllocateRaw的流程圖:

image3

這裏在32位機器且alignment是kDoubleAligned的狀況下會使用函數NewSpace::AllocateRawAlign,這個函數跟NewSpace::AllocateRawUnaligned實際上是差很少的,不過會計算一下須要多少大小來填充對象實現Align的過程,因此咱們重點分析NewSpace::AllocateRawUnaligned就好了,在NewSpace::AllocateRawUnaligned方法中最重要的函數就是NewSpace::EnsureAllocation方法了,他是確保新生代的內存中有足夠的內存來分配給object,至於後面兩個方法一個是從當年的頂部生成heapObject,另一個是將當前的新生代的頂部重置,重置爲 當前頂部+此次須要分配的大小,因此咱們能夠看出整個新生代的內存模型就是不停的在頂部分配內存給對象,是個比較標準的堆內存分配。咱們重點來說解一下 NewSpace::EnsureAllocation的邏輯,下面咱們看一下這個方法的主要邏輯:

Address old_top = allocation_info_.top();
Address high = to_space_.page_high();
int filler_size = Heap::GetFillToAlign(old_top, alignment);
int aligned_size_in_bytes = size_in_bytes + filler_size;

if (old_top + aligned_size_in_bytes > high) {
//沒有足夠的內存page了,增長一個
	if (!AddFreshPage()) {
		return false;
	}
	//讓一些allocation observers作統計的方法
	InlineAllocationStep(old_top, allocation_info_.top(), nullptr, 0);
	//重置參數		
	old_top = allocation_info_.top();
	high = to_space_.page_high();
	filler_size = Heap::GetFillToAlign(old_top, alignment);
}
複製代碼

從上面咱們能夠看出,當前的top+須要分配的內存大於目前空間最大值時,會選擇添加一個新的page到新生代的to_space空間中,具體邏輯以下:

Address top = allocation_info_.top();
DCHECK(!Page::IsAtObjectStart(top));
if (!to_space_.AdvancePage()) {
// 沒有更多的page了
	return false;
}

// 清除現有頁中的殘留內存
Address limit = Page::FromAllocationAreaAddress(top)->area_end();
int remaining_in_page = static_cast<int>(limit - top);
heap()->CreateFillerObjectAt(top, remaining_in_page, ClearRecordedSlots::kNo);
UpdateAllocationInfo();
return true;
複製代碼

上面代碼中有兩個函數是最核心的,第一個是SemiSpace::AdvancePage,這個函數將Space中的當前頁變成下一頁來增長空間,若是沒有頁了或是到達最大頁了就會返回false,另一個函數則是NewSpace::UpdateAllocationInfo函數,這個函數最核心的代碼就是這一句:

allocation_info_.Reset(to_space_.page_low(), to_space_.page_high());
複製代碼

他將AllocationInfo對象的top設置爲新頁的page_low並將high設置爲新頁的page_high,因此剛剛NewSpace::AddFreshPage的註釋中有一個建立一個fillerObject到以前頁剩餘的空間中的操做。

(其實這裏我有一個疑惑,按照這個邏輯那在新生代中生成的對象都是出於to_space中的,可是關於新時代的內存回收文章都寫得是新的分配是在from_space中,而在作內存回收時將from_space中不須要回收的放到to_space中去。並且在Heap::Scavenge方法中有這樣一段註釋和方法

// Flip the semispaces.  After flipping, to space is empty, from space has live objects.
new_space_->Flip();
new_space_->ResetAllocationInfo();
複製代碼

在這裏會清空全部to_space裏的對象,並將存活的直接放入from_space,而後在後面又將沒有升入老生代的object又重新分配進to_space,這個邏輯跟關於V8新生代內存回收的文章中講的都不太同樣,要複雜不少。不過這個跟當下主題也不太符合,只是延伸說一下,下次有時間會再仔細說下這裏面的邏輯。)

說回主題來,NewSpace::UpdateAllocationInfo中還有一個方法咱們要講到,由於等會兒的邏輯就是從這裏來的,就是設置AllocationInfo的limit的方法NewSpace::UpdateInlineAllocationLimit,這裏會根據三種狀況設置不一樣的limit,正常的狀況下,也就是在allocation_observers_paused的狀況下,limit是等於high的,也就是當前頁的末尾;在增量標記的狀況下,limit的值爲:

new_limit = new_top + GetNextInlineAllocationStepSize() - 1;
複製代碼

最小的limit值是在線性分配被禁用的狀況下,就是new_top的值。分析完了NewSpace::AddFreshPage方法後,咱們再回到NewSpace::EnsureAllocation方法,添加了新頁之後的操做直接在註釋中能夠看到就很少分析了,咱們着重說一下下面的代碼:

if (allocation_info_.limit() < high) {
	Address new_top = old_top + aligned_size_in_bytes;
	Address soon_object = old_top + filler_size;
	InlineAllocationStep(new_top, new_top, soon_object, size_in_bytes);
	UpdateInlineAllocationLimit(aligned_size_in_bytes);
}
複製代碼

這段代碼其實有一段註釋,大概是說limit<high會在三種狀況下發生:

1.線性分配被禁用時

2.在打開增量標記狀況下,但願可以分配內存的時候作標記

3.idle scavenge但願可以記錄新分配的內存並在大於一個閾值的狀況下執行

因此咱們能夠看出,設置limit的緣由最主要是爲了爲一些allocate observers在allocate發生時執行他們各自對應的AllocationStep方法(好比profile裏面的SamplingHeapProfiler用來收集每一個函數分配內存大小的監控器就是經過這個調用的)。

到這裏新生代的內存分配以及生成object的過程就已經講完了,接下來咱們來講一下老生代的內存分配方法。

###分配核心之OldSpace::AllocateRaw

由於OldSpace中的AllocateRaw方法並無重寫,是直接使用的父類PagedSpace中的AllocateRaw方法,因此在以前圖中,咱們直接使用了PagedSpace::AllocateRaw方法來表示。接下來老規矩,咱們先看一下老生代內存分配方面的方法流程圖:

image4

從這個圖咱們就能夠看出,對於老生代的分配邏輯上覆雜了許多,由於在以前咱們在CALL_HEAP_FUNCTION中的註釋說了,分配失敗之後重試兩次GC基本就能讓new_space中出現足夠的空間分配,可是old_space方面就要複雜的多,讓咱們來慢慢分析一下其中經歷的過程吧。

首先從圖中咱們能夠看出,跟new_space的狀況一下,存在着AllocateRawAlign和AllocateRawUnalign的方法,跟以前同樣咱們直接來分析Paged::AllocateRawUnalign方法,在AllocateRawUnalign代碼中先是嘗試使用PagedSpace::AllocateLinearly的方法,這個方法跟以前咱們在new_space中的方法相似:

Address current_top = allocation_info_.top();
Address new_top = current_top + size_in_bytes;
if (new_top > allocation_info_.limit()) return NULL;

allocation_info_.set_top(new_top);
return HeapObject::FromAddress(current_top);
複製代碼

能夠看一下代碼,若是new_top沒有大於allocation_info_的limit,那麼就能直接從top生成對象便可。若是現有的空間不夠,那麼就會進行第一次邏輯上稍微簡單些的FreeList::Allocate,這裏咱們涉及到一個在old_space中出現的數據結構,FreeList對象,他是old_space中主要的管理頁和分配的函數之一,咱們先用一張圖來展現它和其餘對象的關係,這裏面的對象在之後都會遇到:

image5

這裏多說兩句來解釋一下 Page,FreeListCategory以及FreeSpace的關係相似於Linux內存中三層頁表的關係,Page對象其實起到一個全局頁目錄的做用,而FreeListCategory則是一個二級頁目錄,而FreeSpace是一個HeapObject的派生類,實際就是咱們要存儲信息的內存位置,能夠理解爲一個直接頁表項的關係,這裏V8的內存結構裏比較巧妙,他經過將FreeListCategory按類型分類,而後對不一樣的類型的FreeListCategory對象中的FreeSpace的內存大小是不同的,有點相似於控制二級頁目錄的位數來控制最後頁表項大小的方式。

講完了這些對象的關係,咱們就開始提及函數FreeList::Allocate的過程了。

FreeList::Allocate

這段代碼前,有一段註釋能說明爲何oldSpace的分配內存會複雜

該函數會作一個分配在oldSpace的freeList上,若是成功則直接使用liner allocation將對象返回並設置allocationInfo的top和limit。若是失敗,則須要經過GC的方式來回收內存再分配,或者直接拓展一個新的Page給oldSpace;
複製代碼

結合咱們的流程圖你們就能夠看到,這是第一次分配未成功的狀況,後面兩次一次是經過GC的方式來作,第二次則是直接expand一個page。在FreeList::Allocate方法中首先執行兩個函數PagedSpace::EmptyAllocationInfo以及Heap::StartIncrementalMarkingIfAllocationLimitIsReached,簡單介紹一下就行,PagedSpace::EmptyAllocationInfo方法是將剛剛liner allocation方法中不夠用可是又剩餘的內存返回到free_list中,而且標記爲未使用的空間,這樣在GC掃描時會忽略這段內存。至於Heap::StartIncrementalMarkingIfAllocationLimitIsReached函數則是在判斷了當前Heap的達到IncrementalMarkingLimit之後判斷是馬上開始增量標記的GC任務,仍是將該任務加入調度序列,仍是不進行。這個函數的判斷涉及不少Heap對象裏的屬性,具體是如何斷定我也沒有徹底搞清楚,可是根據後面的PagedSpace::SlowAllocateRaw方法中使用GC來分配內存的行爲,因此這個函數我我的判斷是爲此次分配失敗之後作準備的。接下來的FreeLIst::FindNodeFor是這個函數的核心操做了,咱們重點來看一下這個函數,由於沒有在圖中表示這個函數的內部流程,咱們先貼出他的代碼來說解:

//先經過快速分配來找到適合這個大小的最小FreeListCategory
//這個操做只須要花費常數時間
FreeListCategoryType type =
SelectFastAllocationFreeListCategoryType(size_in_bytes);
for (int i = type; i < kHuge; i++) {
	node = FindNodeIn(static_cast<FreeListCategoryType>(i), node_size);
	if (node != nullptr) return node;
}

// 若是上面的沒找到,則經過找Huge類別的FreeListCategory 
// 該時間是線性增長的,取決於huge element的數量
node = SearchForNodeInList(kHuge, node_size, size_in_bytes);
if (node != nullptr) {
	DCHECK(IsVeryLong() || Available() == SumFreeLists());
	return node;
}

//若是分配的大小須要huge類型的FreeListCategory來分配,可是卻找不到,直接返回null
if (type == kHuge) return nullptr;

// 不然找尋最適合該大小的FreeListCategory.
type = SelectFreeListCategoryType(size_in_bytes);
node = TryFindNodeIn(type, node_size, size_in_bytes);

DCHECK(IsVeryLong() || Available() == SumFreeLists());
return node;
複製代碼

以前咱們講到過FreeListCategory,他有不一樣的類型,咱們能夠從源碼中看到:

enum FreeListCategoryType {
	kTiniest,
	kTiny,
	kSmall,
	kMedium,
	kLarge,
	kHuge,

	kFirstCategory = kTiniest,
	kLastCategory = kHuge,
	kNumberOfCategories = kLastCategory + 1,
	kInvalidCategory
};
複製代碼

不一樣的類型之間對應了不一樣的FreeSpace大小,而FreeList::SelectFastAllocationFreeListCategoryType則經過須要分配的size來肯定從哪一個FreeListCategory開始尋找足夠空間的Node,咱們接着來看FreeList::FindNodeIn函數,首先來看他的源碼:

FreeListCategoryIterator it(this, type);
FreeSpace* node = nullptr;
while (it.HasNext()) {
	FreeListCategory* current = it.Next();
	node = current->PickNodeFromList(node_size);
	if (node != nullptr) {
		Page::FromAddress(node->address())
			->remove_available_in_free_list(*node_size);
		DCHECK(IsVeryLong() || Available() == SumFreeLists());
		return node;
	}
	RemoveCategory(current);
}
return node;
複製代碼

從代碼中咱們能夠看到,FreeList會對當前type的FreeListCategory生成一個Iterator的迭代器,會迭代在該FreeList的不一樣page的該類型的FreeListCategory,直到經過FreeListCategory::PickNodeFromList找到一個可用的FreeSpace爲止,若是找到了該node則會減去該頁上node的size大小的可用空間,而若是整個FreeListCategory都找不到可用空間,則直接從FreeList的該FreeListCategory鏈表中去掉該項。從FreeListCategory::PickNodeFromList的代碼中:

DCHECK(page()->CanAllocate());
FreeSpace* node = top();
if (node == nullptr) return nullptr;
set_top(node->next());
*node_size = node->Size();
available_ -= *node_size;
return node;
複製代碼

咱們能夠看出,在FreeListCategory中,FreeSpace也是一個鏈表,而top則是最近一次分配出來的FreeSpace,因此在分配成功後,除了在該FreeListCategory減去可用的內存大小之外,還要從新set_top。

當type爲Huge或者以前type<Huge中都沒有找到可用的node,則會使用FreeList::SearchForNodeInList在kHuge中去查詢kHuge中可用的FreeSpace,從函數的代碼中咱們能夠看到:

FreeListCategoryIterator it(this, type);
FreeSpace* node = nullptr;
while (it.HasNext()) {
	FreeListCategory* current = it.Next();
	node = current->SearchForNodeInList(minimum_size, node_size);
	if (node != nullptr) {
		Page::FromAddress(node->address())
			->remove_available_in_free_list(*node_size);
		DCHECK(IsVeryLong() || Available() == SumFreeLists());
		return node;
	}
	if (current->is_empty()) {
		RemoveCategory(current);
	}
}
return node;
複製代碼

這個函數的大概邏輯跟以前實際上是比較像的,只是多了一個minimum_size,這個大小是真正須要的大小,而node_size則是huge類型的FreeListCategory的node大小,之因此須要保留這個大小,主要是避免大量空間的浪費,後面咱們會說到。再來看這個函數中的核心函數FreeListCategory::SearchForNodeInList:

FreeSpace* prev_non_evac_node = nullptr;
for (FreeSpace* cur_node = top(); cur_node != nullptr;
	cur_node = cur_node->next()) {
	size_t size = cur_node->size();
	if (size >= minimum_size) {
		DCHECK_GE(available_, size);
		available_ -= size;
		if (cur_node == top()) {
			set_top(cur_node->next());
		}
		if (prev_non_evac_node != nullptr) {
			prev_non_evac_node->set_next(cur_node->next());
		}
		*node_size = size;
		return cur_node;
	}

	prev_non_evac_node = cur_node;
}
return nullptr;
複製代碼

經過代碼咱們能夠看到,在一般狀況下,對象須要size通常是小於huge類型的FreeListCategory的對象size的因此邏輯和以前的同樣。可是若是超出了大小,則會繼續往下尋找,一直找到合適大小的node而後將這個node從鏈表中排除,而不改變top。因此咱們回想以前在FreeList::FindNodeFor中的註釋,就能夠知道爲何說以前那些type的查找爲O(1),而這個type爲O(n)了。若是這樣並不能查出合適的node,那麼就會經過FreeList::SelectFreeListCategoryType方法定位更準確的type,總的來講就是定位一些須要內存更小的對象,至於FreeList::TryFindNodeIn方法跟以前的方法相似,最後一步其實使用的也是FreeListCategory::PickNodeFromList函數,O(1)查找方法,因此也不需贅言。

在分配完成後,若是分配成功則會經過PagedSpace::AccountAllocatedBytes方法將老生代中分配狀態信息中的已用size加上剛分配的對象大小,可是在禁用線性分配的狀況下,會經過PagedSpace::Free釋放掉沒有使用的大小,而且將老生代allocationInfo的top和limit都設置爲top加上須要分配對象的大小。而在剩餘大小大於一個閾值IncrementalMarking::kAllocatedThreshold且增量標記未完成的狀況下,也會經過PagedSpace::Free釋放掉沒有使用的大小,不過這裏會多留出一個liner_size的大小,而且設置allocationInfo的top爲top加上須要分配對象的大小,而limit爲當前的top再加上liner_size。除了以上兩種狀況,allocationInfo的top爲top加上須要分配對象的大小,而limit爲top加上分配的整個對象的大小。

而若是未分配成功的話,從流程圖咱們能夠知曉,接下來會調用函數PagedSpace::SlowAllocateRaw,不過PagedSpace::SlowAllocateRaw函數主要是一個過渡函數,將當前的heap的VMState設置爲GC狀態,並開始對立刻要執行的函數設置一個timer來計時,而立刻要執行的函數PagedSpace::RawSlowAllocateRaw纔是咱們真正須要瞭解的函數。

PagedSpace::RawSlowAllocateRaw

從流程圖咱們能夠看出函數的第一步,若是判斷當前的heap在作sweep操做且不是在CompactionSpace中且sweeper自己已經沒有task在運行了,則經過MarkCompactCollector::EnsureSweepingCompleted函數等待sweep操做結束再進行下面的操做。在結束了sweep之後,會從新裝填FreeList的page,由於此時sweeper線程已經釋放了一些object了,這個操做由PagedSpace::RefillFreeList來完成,其代碼以下:

if (identity() != OLD_SPACE && identity() != CODE_SPACE &&
	identity() != MAP_SPACE) {
	return;
}
MarkCompactCollector* collector = heap()->mark_compact_collector();
intptr_t added = 0;
{
	Page* p = nullptr;
	while ((p = collector->sweeper().GetSweptPageSafe(this)) != nullptr) {
		if (is_local() && (p->owner() != this)) {
			base::LockGuard<base::Mutex> guard(
				reinterpret_cast<PagedSpace*>(p->owner())->mutex());
			p->Unlink();
			p->set_owner(this);
			p->InsertAfter(anchor_.prev_page());
		}
		added += RelinkFreeListCategories(p);
		added += p->wasted_memory();
		if (is_local() && (added > kCompactionMemoryWanted)) break;
	}
}
accounting_stats_.IncreaseCapacity(added);
複製代碼

第一行的判斷比較簡單就不用多說了,咱們主要看while循環中的邏輯,Sweeper::GetSweptPageSafe能夠獲得已經sweep過的page,而後後面的邏輯中先判斷是否爲CompactionSpace且該page不屬於該space,由於咱們如今是在OldSpace中,因此這個邏輯咱們直接忽略,咱們來看下面的邏輯,首先是PagedSpace::RelinkFreeListCategoriesf方法,其執行的代碼以下:

DCHECK_EQ(this, page->owner());
intptr_t added = 0;
page->ForAllFreeListCategories([&added](FreeListCategory* category) {
	added += category->available();
	category->Relink();
});
DCHECK_EQ(page->AvailableInFreeList(), page->available_in_free_list());
return added;
複製代碼

其中經過Page::ForAllFreeListCategories方法,將遍歷Page中的每一個可用的FreeListCategory並執行後面的匿名函數(C++的lambda函數):

[&added](FreeListCategory* category) {
	added += category->available();
	category->Relink();
}
複製代碼

其中先經過added來加上page中全部FreeListCategory上能用的大小,又經過FreeListCategory::Relink將當前Page所屬的PagedSpace的FreeList設置爲該FreeListCategory的owner,完成了對FreeListCategory的操做後,再加上page中浪費了的內存量。掃描完全部的sweptPage之後,將最後的add值增長到oldSpace的容量中。在作完這一系列操做之後,會再一次重試經過咱們以前講過的FreeList::Allocate方法再一次分配,若是依然沒有成功,這個時候就會調用Sweeper::ParallelSweepSpace,這個函數做用在名字中已經代表了,這是一個作並行sweep PagedSpace的函數,代碼以下:

int max_freed = 0;
int pages_freed = 0;
Page* page = nullptr;
while ((page = GetSweepingPageSafe(identity)) != nullptr) {
	int freed = ParallelSweepPage(page, identity);
	pages_freed += 1;
	DCHECK_GE(freed, 0);
	max_freed = Max(max_freed, freed);
	if ((required_freed_bytes) > 0 && (max_freed >= required_freed_bytes))
		return max_freed;
	if ((max_pages > 0) && (pages_freed >= max_pages)) return max_freed;
}
return max_freed;
複製代碼

很明顯,代碼會去拉取space中將要被sweep的頁,而後調用Sweeper::ParallelSweepPage方法直接作page的sweep,獲得的單頁釋放內存大於須要的內存時或釋放頁數到達規定頁數時則會返回。從流程圖中咱們能夠看出在Sweeper::ParallelSweepPage最重要的就是Sweeper::RawSweep函數,在sweep未完成的時候會調用。Sweeper::RawSweep天然是sweepPage的核心,不過這個函數比較複雜,咱們只能根據流程圖並提取關鍵代碼大概講一下邏輯。首先,該函數會經過代碼:

const MarkingState state = MarkingState::Internal(p);
複製代碼

獲取頁中全部對象標記狀況的bitmap,接着經過該bitmap執行函數:

ArrayBufferTracker::FreeDead(p, state);
複製代碼

這個函數的做用是根據所給page的bitmap將page中已經dead的JSArrayBuffers全部backing store(這裏爲何free掉的只有JSArrayBuffer?ArrayBufferTracker中的array_buffers_屬性的註釋中說這個集合中包含了這個頁上被GC移除的raw heap pointers,爲何不直接使用heapObject?猜想是由於JSArrayBuffer每一個索引對應HeapObject的一個字節,在Free的時候經過它的特性比較方便,因此GC把須要移除的HeapObject轉化爲JSArrayBuffer)。 在成功的釋放了page中必定的空間之後,會再經過page的bitmap找出全部存活的object,經過這些存活的object找到空閒的內存區間,並經過PagedSpace::UnaccountedFree方法將該空閒內存返回到對應他內存大小的FreeListCategory中去,而在存在有對象在新生代可是引用他的對象在老生代的的狀況下,則須要經過RememberedSet<OLD_TO_NEW>::RemoveRange方法來清理掉這個引用的記錄(就是以前關於V8 GC文章中提到的寫屏障insert的記錄)。

在作完了Sweeper::ParallelSweepPage操做後,又獲得了新的swept page,因此再一次執行函數PagedSpace::RefillFreeList,執行完成後又一次執行FreeList: Allocate嘗試分配。而若是此次失敗,程序就會判斷多是頁真的不夠了,在經過Heap::ShouldExpandOldGenerationOnSlowAllocation判斷能擴展頁之後,會調用PagedSpace::Expand來擴展頁,從流程圖中可知PagedSpace::Expand的步驟,先是經過MemoryAllocator::AllocatePage分配出頁,接着調用PagedSpace::AccountCommit更新space中的committed_屬性,最後將生成的頁插入到space中,再增長頁後繼續嘗試FreeList: Allocate,若是此次依然失敗(多是已經到達老生代內存的瓶頸了)則會調用PagedSpace::SweepAndRetryAllocation方法,這個方法相對簡單,代碼以下,就不講解了:

if (collector->sweeping_in_progress()) {
	collector->EnsureSweepingCompleted();
	return free_list_.Allocate(size_in_bytes);
}
複製代碼

基本都是以前講過的,這裏只是作最後一次嘗試。

若是PagedSpace::SlowAllocateRaw分配成功,則須要對分配成功的區域進行一個標記,標記該內存段是剛分配的不須要清理,使用Page::CreateBlackArea來完成。在以上操做成功的返回Object後,就會調用函數PagedSpace::AllocationStep,這個函數咱們也不陌生,在新生代中使用過,他會通知space中的各個allocation observers調用各自的AllocationStep方法,作一些統計方面的工做。

總結

以上就是整個V8內存分配中的過程,該過程很是複雜,還涉及了不少GC相關的東西,可是V8代碼確實是一個精湛的工藝品,裏面的函數名都取的讓人知道是作什麼的,裏面的註釋也恰到好處,不少時候我陷入困惑的時候總能從一些註釋中獲得線索,慢慢又順藤摸瓜的搞出答案,固然這個只是系統的一部分,並且整個系統異常的精密,不少東西不聯繫其餘模塊上也沒法知道,因此上面也存了一些疑惑的地方。下次有時間,我會再來一探GC的究竟,固然這塊更是一個硬骨頭,但願可以搞懂~

相關文章
相關標籤/搜索