|
本文讲解 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 虚拟机. 周志明. |
|