V8 垃圾回收

简介:

转自: https://github.com/yjhjstz/deep-into-node

垃圾回收器是一把十足的双刃剑。好处是简化程序的内存管理,内存管理无需程序员来操作,由此也减少了长时间运转的程序的内存泄漏。然而无法预期的停顿,影响了交互体验。本文从 V8 (node.js runtime) 的角度分析垃圾回收策略。

基本概念

垃圾回收器解决基本问题就是,识别需要回收的内存。一旦辨别完毕,这些内存区域即可在未来的分配中重用,或者是返还给操作系统。一个对象当它不是处于活跃状态的时候它就死了。一个对象处于活跃状态,当且仅当它被一个根对象或另一个活跃对象指向。根对象被定义为处于活跃状态,是浏览器或V8所引用的对象。比如说全局对象属于根对象,因为它们始终可被访问;浏览器对象,如DOM元素,也属于根对象,尽管在某些场合下它们只是弱引用。

堆的构成

在深入研究垃圾回收器的内部工作原理之前,首先来看看堆是如何组织的。V8将堆分为了几个不同的区域:
2015-11-17 下午3.09.08

新生区:大多数对象开始时被分配在这里。新生区是一个很小的区域,垃圾回收在这个区域非常频繁,与其他区域相独立。

老生指针区:包含大多数可能存在指向其他对象的指针的对象。大多数在新生区存活一段时间之后的对象都会被挪到这里。

老生数据区:这里存放只包含原始数据的对象(这些对象没有指向其他对象的指针)。字符串、封箱的数字以及未封箱的双精度数字数组,在新生区经历一次 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包括三个主要步骤:

  1. 枚举根节点引用;
  2. 发现并标记活对象;
  3. 垃圾内存清理

分代回收在 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(); // 交换 SemiSpace
  new_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 即可享受到。

总结

GC

垃圾回收非常复杂,alinode 提供了详细的 GC 监控,帮助您分析把控性能。

目录
相关文章
|
5月前
|
存储 Java 程序员
V8垃圾回收?看这篇就够了!
V8垃圾回收?看这篇就够了!
|
存储 算法 JavaScript
V8如何进行垃圾回收的
V8如何进行垃圾回收的
39 0
|
存储 Web App开发 监控
Js中的垃圾回收及V8引擎的优化
Js中的垃圾回收及V8引擎的优化
288 0
|
缓存 JavaScript 前端开发
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
382 0
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
|
Web App开发 JavaScript 前端开发
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
154 0
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
|
算法 JavaScript Java
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
134 0
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
|
存储 前端开发 算法
V8 如何进行垃圾回收?
V8 如何进行垃圾回收?
99 0
ADI
|
算法 Java
[记录]简单聊聊 v8 垃圾回收
[记录]简单聊聊 v8 垃圾回收
ADI
104 0
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
62 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
2月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。

热门文章

最新文章

  • 1
    Java面试题:描述Java垃圾回收的基本原理,以及如何通过代码优化来协助垃圾回收器的工作
    88
  • 2
    Java面试题:如何在Java中触发一次Full GC?请详细解释垃圾回收机制和知识
    373
  • 3
    Java面试题:在Java中,对象何时可以被垃圾回收?编程中,如何更好地做好垃圾回收处理?
    68
  • 4
    Java面试题:解释垃圾回收中的标记-清除、复制、标记-压缩算法的工作原理
    58
  • 5
    Java面试题:解释分代垃圾回收策略,并说明其优势
    51
  • 6
    Java面试题:解释Java的垃圾回收机制,包括常见的垃圾回收算法。介绍一下Java的垃圾回收算法中的标记-压缩算法。
    49
  • 7
    Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
    68
  • 8
    Java面试题:请解释Java中的四种访问控制符及其作用范围,请解释Java中的垃圾回收机制及其工作原理,请解释Java中的并发工具包及其主要用途
    31
  • 9
    Java面试题:Java内存管理与多线程并发处理,设计一个Java应用,该应用需要处理大量并发用户请求,同时要求对内存使用进行优化,如何通过垃圾回收机制优化内存使用?
    40
  • 10
    Java面试题:Java内存管理、多线程与并发框架的面试题解析与知识点梳理,深入Java内存模型与垃圾回收机制,Java多线程机制与线程安全,Java并发工具包与框架的应用
    77