V8 垃圾回收
轉自: https://github.com/yjhjstz/deep-into-node
垃圾回收器是一把十足的雙刃劍。好處是簡化程序的內存管理,內存管理無需程序員來操作,由此也減少了長時間運轉的程序的內存泄漏。然而無法預期的停頓,影響了交互體驗。本文從 V8 (node.js runtime) 的角度分析垃圾回收策略。
基本概念
垃圾回收器解決基本問題就是,識別需要回收的內存。一旦辨別完畢,這些內存區域即可在未來的分配中重用,或者是返還給操作系統。一個對象當它不是處于活躍狀態的時候它就死了。一個對象處于活躍狀態,當且僅當它被一個根對象或另一個活躍對象指向。根對象被定義為處于活躍狀態,是瀏覽器或V8所引用的對象。比如說全局對象屬于根對象,因為它們始終可被訪問;瀏覽器對象,如DOM元素,也屬于根對象,盡管在某些場合下它們只是弱引用。
堆的構成
在深入研究垃圾回收器的內部工作原理之前,首先來看看堆是如何組織的。V8將堆分為了幾個不同的區域:
新生區:大多數對象開始時被分配在這里。新生區是一個很小的區域,垃圾回收在這個區域非常頻繁,與其他區域相獨立。
老生指針區:包含大多數可能存在指向其他對象的指針的對象。大多數在新生區存活一段時間之后的對象都會被挪到這里。
老生數據區:這里存放只包含原始數據的對象(這些對象沒有指向其他對象的指針)。字符串、封箱的數字以及未封箱的雙精度數字數組,在新生區經歷一次 Scavenge 后會被移動到這里。
大對象區:這里存放體積超過1MB大小的對象。每個對象有自己mmap產生的內存。垃圾回收器從不移動大對象。
Code區:代碼對象,也就是包含JIT之后指令的對象,會被分配到這里。
Cell區、屬性Cell區、Map區:這些區域存放Cell、屬性Cell和Map,每個區域因為都是存放相同大小的元素,因此內存結構很簡單。
如上圖:在 node-v4.x 之后,區域進行了合并為:新生區,老生區,大對象區,Map區,Code區
有了這些背景知識,我們可以來深入垃圾回收器了。
識別指針
垃圾回收器面臨的第一個問題是,如何才能在堆中區分指針和數據,因為指針指向著活躍的對象。大多數垃圾回收算法會將對象在內存中挪動(以便減少內存碎片,使內存緊湊),因此即使不區分指針和數據,我們也常常需要對指針進行改寫。
V8采用了標記指針法:這種方法需要在每個指針的末位預留一位來標記這個字代表的是指針或數據。
寫屏障
如果新生區中某個對象,只有一個指向它的指針,而這個指針恰好是在老生區的對象當中,我們如何才能知道新生區中那個對象是活躍的呢? 為了解決這個問題,實際上在寫緩沖區中有一個列表 store-buffer{.cc,.h,-inl.h},列表中記錄了所有老生區對象指向新生區的情況。新對象誕生的時候,并不會有指向它的指針,而當有老生區中的對象出現指向新生區對象的指針時,我們便記錄下來這樣的跨區指向。由于這種記錄行為總是發生在寫操作時,它被稱為寫屏障.
垃圾回收三部曲
Stop-the-World 的GC包括三個主要步驟:
分代回收在 V8中分為Scavenge, Mark-Sweep。
- Scavenge: 當分配指針達到了新生區的末尾,就會有一次清理。
- Mark-Sweep: 對于活躍超過2個小周期的對象,則需將其移動至老生區, 當老生區有足夠多的對象時才會觸發。
Scavenge
void Heap::Scavenge() {RelocationLock relocation_lock(this);AlwaysAllocateScope scope(isolate());gc_state_ = SCAVENGE;// Clear descriptor cache.isolate_->descriptor_lookup_cache()->Clear();// Used for updating survived_since_last_expansion_ at function end.intptr_t survived_watermark = PromotedSpaceSizeOfObjects();SelectScavengingVisitorsTable();PrepareArrayBufferDiscoveryInNewSpace();// Flip the semispaces. After flipping, to space is empty, from space has// live objects.new_space_.Flip(); // 交換 SemiSpacenew_space_.ResetAllocationInfo(); // 重設分配指針Address new_space_front = new_space_.ToSpaceStart();promotion_queue_.Initialize(); // 晉升隊列ScavengeVisitor scavenge_visitor(this); // Scavenge迭代器// Copy roots. 枚舉跟對象IterateRoots(&scavenge_visitor, VISIT_ALL_IN_SCAVENGE); // Copy objects reachable from the old generation.{StoreBufferRebuildScope scope(this, store_buffer(),&ScavengeStoreBufferCallback);store_buffer()->IteratePointersToNewSpace(&ScavengeObject);}// Copy objects reachable from the encountered weak collections list.scavenge_visitor.VisitPointer(&encountered_weak_collections_);// Copy objects reachable from the encountered weak cells.scavenge_visitor.VisitPointer(&encountered_weak_cells_);// Copy objects reachable from the code flushing candidates list.MarkCompactCollector* collector = mark_compact_collector();if (collector->is_code_flushing_enabled()) {collector->code_flusher()->IteratePointersToFromSpace(&scavenge_visitor);}// DoScavenge 處理晉升new_space_front = DoScavenge(&scavenge_visitor, new_space_front);while (isolate()->global_handles()->IterateObjectGroups(&scavenge_visitor, &IsUnscavengedHeapObject)) {new_space_front = DoScavenge(&scavenge_visitor, new_space_front);}isolate()->global_handles()->RemoveObjectGroups();isolate()->global_handles()->RemoveImplicitRefGroups();isolate()->global_handles()->IdentifyNewSpaceWeakIndependentHandles(&IsUnscavengedHeapObject);isolate()->global_handles()->IterateNewSpaceWeakIndependentRoots(&scavenge_visitor);new_space_front = DoScavenge(&scavenge_visitor, new_space_front);UpdateNewSpaceReferencesInExternalStringTable(&UpdateNewSpaceReferenceInExternalStringTableEntry);promotion_queue_.Destroy();incremental_marking()->UpdateMarkingDequeAfterScavenge();ScavengeWeakObjectRetainer weak_object_retainer(this);ProcessYoungWeakReferences(&weak_object_retainer);DCHECK(new_space_front == new_space_.top());// Set age mark.new_space_.set_age_mark(new_space_.top());new_space_.LowerInlineAllocationLimit(new_space_.inline_allocation_limit_step());FreeDeadArrayBuffers(true);// Update how much has survived scavenge.IncrementYoungSurvivorsCounter(static_cast<int>((PromotedSpaceSizeOfObjects() - survived_watermark) + new_space_.Size()));gc_state_ = NOT_IN_GC; }這個算法大致是,新生區被劃分為兩個等大的SemiSpace:出區、入區。絕大多數內存的分配都會在出區發生(但某些特定類型的對象,如可執行的代碼對象是分配在老生區的),當出區耗盡時,我們交換出區和入區(這樣所有的對象都歸屬在入區當中),然后將入區中活躍的對象復制至出區或晉升到老生區中,其中標記的過程實際是深度優先搜索。
“標記-清除”算法
void MarkCompactCollector::CollectGarbage() {DCHECK(state_ == PREPARE_GC);MarkLiveObjects(); //枚舉并標記活對象DCHECK(heap_->incremental_marking()->IsStopped());// ClearNonLiveReferences can deoptimize code in dependent code arrays.// Process weak cells before so that weak cells in dependent code// arrays are cleared or contain only live code objects.ProcessAndClearWeakCells();ClearNonLiveReferences();ClearWeakCollections();heap_->set_encountered_weak_cells(Smi::FromInt(0));ClearInvalidStoreAndSlotsBufferEntries();SweepSpaces(); // 按不同的空間劃分清理Finish();if (marking_parity_ == EVEN_MARKING_PARITY) {marking_parity_ = ODD_MARKING_PARITY;} else {DCHECK(marking_parity_ == ODD_MARKING_PARITY);marking_parity_ = EVEN_MARKING_PARITY;} }Scavenge算法對于快速回收、緊縮小片內存效果很好,但對于大片內存則消耗過大。頻繁的拷貝對于 CPU 是不可承受之重。老生區包含有上百MB的數據,對于這么大的區域,我們采取“標記-清除”算法與“標記-緊縮”算法。
標記算法執行完畢后,我們可以選擇清理或是緊縮,這兩個算法都可以回收內存。
- 清理算法非常簡單,只需遍歷頁的位圖,搜索連續的死對象釋放,時間久了會形成內存碎片。
- 緊縮算法會嘗試將對象從碎片頁(包含大量小空閑內存的頁)中遷移整合在一起,來釋放內存。這些對象會被遷移到另外的頁上,因此也可能會新分配一些頁。alinode 對此策略進行了優化,使用 alinode runtime 即可享受到。
總結
垃圾回收非常復雜,alinode 提供了詳細的 GC 監控,幫助您分析把控性能。
總結
- 上一篇: 开发自己的 chart - 每天5分钟玩
- 下一篇: python__基础 : 类的__ini