零维护

 找回密码
 立即注册
快捷导航
搜索
热搜: 活动 交友 discuz
查看: 89|回复: 1

《Chrome V8 源码》61. Semisapce Collection Scavenge

[复制链接]

1

主题

1

帖子

3

积分

新手上路

Rank: 1

积分
3
发表于 2022-11-8 07:37:11 | 显示全部楼层 |阅读模式
本文讲解 V8 新生代垃圾回收器 Scavenge 的工作流程,V8 版本:10.6。  开始之前,我们简单总结一下分代回收器的基础知识:
(1) 弱分代假设,绝大多数据对象会在下一次回收中消亡。
(2) 强分代假设,跨越多次垃圾回收的对象一般都有很长的生命期。
这两个假设共同奠定了垃圾回收器的常用设计原则:把堆划分成不同的区域,然后依据对象年龄分配到不同的区,回收器可以回收某个或全部区域。  
衡量一个回收器好坏的三项指标是:内存占用率、吞吐量和延迟。其中,内存占用是指回收器工作时自身所需的内存,随着硬件的发展,回收器工作时多使用一些内存是可以容忍的;吞吐量是指回收器的工作效率,单位时间内回收内存的效率;延迟是我们最关心的指示,我们希望程序不卡顿,运行流畅。这三项指示之间的相互制约关系是:硬件性能增长,有助于降低回收器工作时对应用程序的影响,吞吐量会更高。延迟方面,硬件规格提升,准确地说是内存的扩大,会给延迟带来负面的效果----回收1TB大小的堆内存,比回收1GB的堆内存耗费更多时间。
1 Scavenge 源码分析

分代回收的常见问题是如何处理代间对象引用,V8 采用记忆集的方法来处理代间引用,如图 1 所示。


Scavenge 的算法是 Cheney 的 semispace copying,工作流程是堆被划分为两个大小相等的半区(from 和 to),垃圾回收时先交换两个半区,然后把原来 from 中的活跃对象复制到 to 中。semispace copying 方法采用可达性分析来判定对象是否活跃。  
1.  void Heap::Scavenge() {
2.  //省略...................
3.    SemiSpaceNewSpace::From(new_space())->EvacuatePrologue();
4.    // We also flip the young generation large object space. All large objects
5.    // will be in the from space.
6.    new_lo_space()->Flip();
7.    new_lo_space()->ResetPendingObject();
8.    // Implements Cheney's copying algorithm
9.    scavenger_collector_->CollectGarbage();
10.    SetGCState(NOT_IN_GC);
11.  }
上述代码,第 5 行 EvacuatePrologue 方法中交换两个半区(见图 2),请读者自行分析,图 2 给出调用堆栈。


下面是 CollectGarbage 源码:
1.  void ScavengerCollector::CollectGarbage() {
2.    {
3.      for (int i = 0; i < num_scavenge_tasks; ++i) {
4.        scavengers.emplace_back(
5.            new Scavenger(this, heap_, is_logging, &empty_chunks, &copied_list,
6.                          &promotion_list, &ephemeron_table_list, i));
7.      }
8.      std::vector<std::pair<ParallelWorkItem, MemoryChunk*>> memory_chunks;
9.      RememberedSet<OLD_TO_NEW>::IterateMemoryChunks(heap_, [&memory_chunks](MemoryChunk* chunk) {
10.            memory_chunks.emplace_back(ParallelWorkItem{}, chunk);});
11.      RootScavengeVisitor root_scavenge_visitor(scavengers[kMainThreadId].get());
12.      {// Identify weak unmodified handles. Requires an unmodified graph.
13.        isolate_->global_handles()->ComputeWeaknessForYoungObjects(
14.            &JSObject::IsUnmodifiedApiObject);
15.      }
16.      {// Copy roots.
17.        base::EnumSet<SkipRoot> options({SkipRoot::kExternalStringTable,
18.                                         SkipRoot::kGlobalHandles,
19.                                         SkipRoot::kOldGeneration});
20.        if (V8_UNLIKELY(FLAG_scavenge_separate_stack_scanning)) {
21.          options.Add(SkipRoot::kStack);
22.        }
23.        heap_->IterateRoots(&root_scavenge_visitor, options);
24.        isolate_->global_handles()->IterateYoungStrongAndDependentRoots(
25.            &root_scavenge_visitor);
26.        scavengers[kMainThreadId]->Publish();
27.      }
28.      {// Parallel phase scavenging all copied and promoted objects.
29.        TRACE_GC(heap_->tracer(), GCTracer::Scope::SCAVENGER_SCAVENGE_PARALLEL);
30.        V8::GetCurrentPlatform()
31.            ->CreateJob(v8::TaskPriority::kUserBlocking,
32.                        std::make_unique<JobTask>(this, &scavengers,
33.                                                  std::move(memory_chunks),
34.                                                  &copied_list, &promotion_list))->Join();
35.      } }
36.    {// Update references into new space
37.      heap_->UpdateYoungReferencesInExternalStringTable(
38.          &Heap::UpdateYoungReferenceInExternalStringTableEntry);
39.      heap_->incremental_marking()->UpdateMarkingWorklistAfterYoungGenGC();
40.    }
41.    SemiSpaceNewSpace* semi_space_new_space =
42.        SemiSpaceNewSpace::From(heap_->new_space());
43.    ProcessWeakReferences(&ephemeron_table_list);
44.  }
上述代码中,第 3 行可以看出 Scavenge 是多线程回收器;   
第 5 行代码 copied_list, 注意:可达性分析需要已知的源对象,也就是从哪里开始分析。copied_list 用来存储待分析的对象,此时它为空,稍后会把“源点对象”,也就是根对象、栈对象、全局对象等加入到 copied_list中,即可达性分析的初始对象;
第 6 行代码 promotion_list 和 ephemeron_table_list。其中,promotion_list 存储即将从新生代转移到老年代的对象----代晋升;
第 9 行代码记忆集 RememberedSet,处理跨代引用对象;  
第 23 行把根对象等源对象添加到 copiled_list 中,见第 2 节;
第 24 行执行垃圾回收,也就是 semispace copying,见第 3 节;
第 37 行更新引用,因为活跃对象已被复制到了新地址,所以要更新引用。  
2 添加 copiled_list

代码如下:  
1.  void Heap::IterateRoots(RootVisitor* v, base::EnumSet<SkipRoot> options) {
2.    v->VisitRootPointers(Root::kStrongRootList, nullptr,
3.                         roots_table().strong_roots_begin(),
4.                         roots_table().strong_roots_end());
5.    v->Synchronize(VisitorSynchronization::kStrongRootList);
6.    isolate_->bootstrapper()->Iterate(v);
7.    v->Synchronize(VisitorSynchronization::kBootstrapper);
8.    Relocatable::Iterate(isolate_, v);
9.    v->Synchronize(VisitorSynchronization::kRelocatable);
10.    isolate_->debug()->Iterate(v);
11.    v->Synchronize(VisitorSynchronization::kDebug);
12.    isolate_->compilation_cache()->Iterate(v);
13.    v->Synchronize(VisitorSynchronization::kCompilationCache);
14.    if (!options.contains(SkipRoot::kOldGeneration)) {
15.      IterateBuiltins(v);
16.      v->Synchronize(VisitorSynchronization::kBuiltins);
17.    }
18.  //省略..............
19.  }
上述代码的作用是添加源对象,第 2 行是从根 RootList 中选取部分对象添加到 copied_list 中,作为源对象。注意:不是全部,而是选取部分
如何选择对象呢?答案很简单:如果一个 Root 对象本身就在新生代中,它就是源对象,否则不是。
再看图 1,对象 S 和 U 引用了 P,Q,V,但 S 和 U 不在新生代中(无论它们是 Root 对象与否),不符合选取原则。那么,P,Q,V 三个对象因为不可达而被误判为死对象,这显然是不对的。所以这里引入了记忆集,也就是说,那些不符合选取原则的、但对新生代有引用的源对象由记忆集管理,这些源对象也做要可达性分析,这样就避免了误判。
谁负责维护记忆集?每次改变对象之间的引用关系时要通知 GC 重新扫描这部分引用关系(增量 GC 和多线程 GC),这时会维护记忆集。V8 中使用的通知方法是写屏障技术,具体细节以后再讲。
回来看前面提到的选取原则----如果一个 Root 对象本身就在新生代中,它就是源对象,否则不是。 它的源码如图 3 所示。  


图中的红色标记--- Heap::InYoungGeneration(object) 判断是否在新生代中。图中给出了调用堆栈,读者可以自行跟踪。  
3 Semispace Copying

copied_list 创建后,开始可达性分析,找到并复制活跃对象,源码如图 4。


红色标记从 copied_list 中弹出一个对象,执行 Visit 方法。深入 debug,会进入下面的代码:
1.  SlotCallbackResult Scavenger::ScavengeObject(THeapObjectSlot p,
2.                                               HeapObject object) {
3.    MapWord first_word = object.map_word(kAcquireLoad);
4.    if (first_word.IsForwardingAddress()) {
5.      HeapObject dest = first_word.ToForwardingAddress();
6.      HeapObjectReference::Update(p, dest);
7.      DCHECK_IMPLIES(Heap::InYoungGeneration(dest),
8.                     Heap::InToPage(dest) || Heap::IsLargeObject(dest));
9.      return Heap::InYoungGeneration(dest) ? KEEP_SLOT : REMOVE_SLOT;
10.    }
11.    Map map = first_word.ToMap();
12.    return EvacuateObject(p, map, object);
13.  }
执行上述代码,说明找到了一个活跃对象,在复制之前要判断该对象是否已经完成了复制,例如:从对象 A 找到 B,这时把 B 复制到新位置;从 C 也找到了 B,这时的 B 不需要再复制。
代码 3-10 行,如果一个对象的 map 被标记为 forward,就说明它已经被复制了。如同例子中的对象 B;
代码 12 行,EvacuateObject 把一个对象从原来的 from 复制到 to 区域,并且设置 forward 标记,避免以后的重复复制;
图 5 给出此时的调用堆栈,请自行分析 EvacuateObject。


技术总结
(1) copied_list 选取原则是在新生代中的 root 对象、全局对象等;
(2) 记忆集用于补充 copied_list,记忆集的负作用是可能引起不要必的对象晋升;
(3) 对象移动之后要更新 handle;
(4) 写屏障影响 GC 的效率;
(5) 分代垃圾回收的理论基础是弱分代假设和强分代假设。
参考文献
1. THE GARBAGE COLLECTION HANDBOOK. Richard Jones.  
2. 深入理解 Java 虚拟机. 周志明.
回复

使用道具 举报

1

主题

4

帖子

8

积分

新手上路

Rank: 1

积分
8
发表于 2025-3-23 07:44:40 | 显示全部楼层
呵呵,低调,低调!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver| 手机版| 小黑屋| 零维护

GMT+8, 2025-4-8 14:57 , Processed in 0.084437 second(s), 22 queries .

Powered by Discuz! X3.4

Copyright © 2020, LianLian.

快速回复 返回顶部 返回列表