究竟什么是垃圾回收根,如何在 HotSpot JVM 中找到它们?[已关闭]

介绍:

在大学里,人们了解到Java(和类似语言)中典型的垃圾回收根是加载类的静态变量,当前运行的线程的线程局部变量,“外部引用”(如JNI句柄)和GC特定属性,例如代际垃圾回收器的次要GC期间的旧到年轻指针。从理论上讲,这听起来并不难。


问题:

我正在阅读HotSpot源代码,并对如何在VM内部检测这些垃圾回收根感兴趣,即在JVM源代码中使用哪些方法来访问所有根。


调查:

我发现各种文件(例如,psMarkSweep.cpp),属于各种GC实现,包含非常相似的结构。

以下是psMarkSweep的方法.cpp我认为它涵盖了强大的根源:PSMarkSweep::mark_sweep_phase1

ParallelScavengeHeap::ParStrongRootsScope psrs;    
Universe::oops_do(mark_and_push_closure());    
JNIHandles::oops_do(mark_and_push_closure());   // Global (strong) JNI handles    
CLDToOopClosure mark_and_push_from_cld(mark_and_push_closure());
MarkingCodeBlobClosure each_active_code_blob(mark_and_push_closure(), !CodeBlobToOopClosure::FixRelocations);    
Threads::oops_do(mark_and_push_closure(), &mark_and_push_from_cld, &each_active_code_blob);    
ObjectSynchronizer::oops_do(mark_and_push_closure());    
FlatProfiler::oops_do(mark_and_push_closure());    
Management::oops_do(mark_and_push_closure());    
JvmtiExport::oops_do(mark_and_push_closure());    
SystemDictionary::always_strong_oops_do(mark_and_push_closure());    
ClassLoaderDataGraph::always_strong_cld_do(follow_cld_closure());    
// Do not treat nmethods as strong roots for mark/sweep, since we can unload them. 
//CodeCache::scavenge_root_nmethods_do(CodeBlobToOopClosure(mark_and_push_closure()));    

psScavenge.cpp以下代码似乎为不同类型的GC根添加了任务:

if (!old_gen->object_space()->is_empty()) {
  // There are only old-to-young pointers if there are objects
  // in the old gen.
  uint stripe_total = active_workers;
  for(uint i=0; i < stripe_total; i++) {
    q->enqueue(new OldToYoungRootsTask(old_gen, old_top, i, stripe_total));
  }
}

q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::universe));
q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::jni_handles));
// We scan the thread roots in parallel
Threads::create_thread_roots_tasks(q);
q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::object_synchronizer));
q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::flat_profiler));
q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::management));
q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::system_dictionary));
q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::class_loader_data));
q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::jvmti));
q->enqueue(new ScavengeRootsTask(ScavengeRootsTask::code_cache));

查看 ,我们看到熟悉的代码类似于以下代码:ScavangeRootsTaskpsMarkSweep

void ScavengeRootsTask::do_it(GCTaskManager* manager, uint which) {
  assert(Universe::heap()->is_gc_active(), "called outside gc");

  PSPromotionManager* pm = PSPromotionManager::gc_thread_promotion_manager(which);
  PSScavengeRootsClosure roots_closure(pm);
  PSPromoteRootsClosure  roots_to_old_closure(pm);

  switch (_root_type) {
    case universe:
      Universe::oops_do(&roots_closure);
      break;    
    case jni_handles:
      JNIHandles::oops_do(&roots_closure);
      break;    
    case threads:
    {
      ResourceMark rm;
      CLDClosure* cld_closure = NULL; // Not needed. All CLDs are already visited.
      Threads::oops_do(&roots_closure, cld_closure, NULL);
    }
    break;    
    case object_synchronizer:
      ObjectSynchronizer::oops_do(&roots_closure);
      break;    
    case flat_profiler:
      FlatProfiler::oops_do(&roots_closure);
      break;    
    case system_dictionary:
      SystemDictionary::oops_do(&roots_closure);
      break;    
    case class_loader_data:
    {
      PSScavengeKlassClosure klass_closure(pm);
      ClassLoaderDataGraph::oops_do(&roots_closure, &klass_closure, false);
    }
    break;    
    case management:
      Management::oops_do(&roots_closure);
      break;    
    case jvmti:
      JvmtiExport::oops_do(&roots_closure);
      break;
    case code_cache:
      {
        MarkingCodeBlobClosure each_scavengable_code_blob(&roots_to_old_closure, CodeBlobToOopClosure::FixRelocations);
        CodeCache::scavenge_root_nmethods_do(&each_scavengable_code_blob);
      }
      break;    
    default:
      fatal("Unknown root type");
  }    
  // Do the real work
  pm->drain_stacks(false);
}

洞察力:

源代码中的GC根列表看起来比我最初在第一句话中写的要大得多,所以我试着在下面列出它们,并附上一些注释:

  • 宇宙:好的,宇宙。主要是某些类的镜像。
  • JNI句柄:同样清晰,通过JNI创建的句柄使对象保持活动状态。
  • 线程:这个访问了thead-local根。但在这里,我们看到了第一个区别。 使用 CLDToOopClosure 并对代码 blob 执行某些操作,而不执行。Afaik,CLD代表类加载器数据,但我不知道为什么在一种情况下使用它,而在另一种情况下却不使用它。代码 blob 的帐户相同。psMarkSweep.cpppsScavange.cpp
  • 对象同步器:用于同步的监视器。
  • 平面探查器:作为 Hotspot 一部分的探查器,可保持其类装入器处于活动状态。
  • 管理:为某些管理服务保持活动状态的对象,例如 MemoryPoolMXBean 等。
  • JVMTI:保持 JVMTI 分配的 JVMTI 断点和对象处于活动状态。
  • 系统字典:使 Java 系统类装入器加载的所有类保持活动状态,从而使类的静态字段引用的对象保持活动状态。
  • 类加载器数据图:这个对我来说有点不清楚。我认为这是不是Java系统类装入器的类装入器,也就是说,这涵盖了由不同类装入器装入的类(及其静态字段)?
  • 代码缓存:代码缓存包含某些代码 blob,但我仍然不确定这些代码 blob 到底是什么。似乎代码 blob 表示有关(已编译的)代码帧的信息,我对此是否正确?但我仍然不明白为什么在遍历线程堆栈时有时会访问这些代码 blob(如 中所做的那样),有时使用 CodeCache 访问这些代码 blob(如 中所做的那样)。psMarkSweep.cpppsScavenge.cpp
  • (仅适用于次要GC)从老到年轻的根:清除。

问题:

虽然在源代码中可以找到很多东西,但我仍然很难理解其中一些GC根,或者这些GC根是如何找到的。

  • 什么是代码 blob?它包含哪些 GC 根,这些根尚未通过访问具有 oop 闭包的线程来覆盖?什么是代码缓存?
  • 收集所有线程本地根:将 a 和 a 组合使用(如 中所示)与访问具有 oop 闭包并附加执行 和 (如 所用)的线程相比,有什么区别。CLDToOopClosureMarkingCodeBlobClosureThreads::oops_dopsMarkSweep.cppClassLoaderDataGraph::oops_doCodeCache::scavenge_root_nmethods_dopsScavenge.cpp
  • 什么是类装入器数据图(与系统字典相比)?它是应用程序类装入器的集合吗?
  • 那么实习字符串呢,它们如何在GC中生存?它们是否位于堆外的某个位置,垃圾回收不会影响它们?
  • 其他垃圾回收器(例如 G1 GC)是否引入了新型根指针?(我不认为情况应该如此)

备注:

我知道这是一个很长的问题,有各种子问题,但我认为很难把它分成多个问题。我很感激每一个发布的答案,即使它没有涵盖上面提出的所有问题的答案,即使部分问题的答案也会对我有所帮助。谢谢!


答案 1

正如您已经发现自己的那样,是 HotSpot JVM 中访问 的 GC 根的典型机制。顺便说一句,分析很好。只需继续浏览 VM 源,你就会找到答案,因为代码中有很多有用的注释。<Subsystem>::oops_do()<Subsystem>

请注意,的目的不仅是标记可访问的对象,而且还要处理引用本身,特别是在压缩期间重新定位它们。oops_do


CodeBlob 是一段生成的代码。它不仅涵盖了JITted方法(又名),还涵盖了运行时生成的各种VM存根和例程。nmethods

// CodeBlob - superclass for all entries in the CodeCache.
//
// Suptypes are:
//   nmethod            : Compiled Java methods (include method that calls to native code)
//   RuntimeStub        : Call to VM runtime methods
//   DeoptimizationBlob : Used for deoptimizatation
//   ExceptionBlob      : Used for stack unrolling
//   SafepointBlob      : Used to handle illegal instruction exceptions

这些代码片段可能包含对堆对象的嵌入式引用,例如 String/Class/MethodHandle 文本和静态最终常量。


in 的目的是标记通过未以其他方式标记的方法指针引用的对象:CLDToOopClosureThreads::oops_do

// The method pointer in the frame might be the only path to the method's
// klass, and the klass needs to be kept alive while executing. The GCs
// don't trace through method pointers, so typically in similar situations
// the mirror or the class loader of the klass are installed as a GC root.
// To minimze the overhead of doing that here, we ask the GC to pass down a
// closure that knows how to keep klasses alive given a ClassLoaderData.
cld_f->do_cld(m->method_holder()->class_loader_data());

类似地,用于标记仅从活动 nmethod 引用的对象:MarkingCodeBlobClosure

// In cases where perm gen is collected, GC will want to mark
// oops referenced from nmethods active on thread stacks so as to
// prevent them from being collected. However, this visit should be
// restricted to certain phases of the collection only. The
// closure decides how it wants nmethods to be traced.
if (cf != NULL)
  cf->do_code_blob(_cb);

请注意,在标记阶段不调用:CodeCache::scavenge_root_nmethods_do

// Do not treat nmethods as strong roots for mark/sweep, since we can unload them.
//CodeCache::scavenge_root_nmethods_do(CodeBlobToOopClosure(&mark_and_push_closure));

SystemDictionary主要负责解析类的符号名称。它不用作标记的 GC 根(Bootstrap 和 System 类装入器除外)。另一方面,维护类装入器实体的完整链接集。它确实充当 GC 根,并负责类卸载。ClassLoaderDataGraph

// A ClassLoaderData identifies the full set of class types that a class
// loader's name resolution strategy produces for a given configuration of the
// class loader.
// Class types in the ClassLoaderData may be defined by from class file binaries
// provided by the class loader, or from other class loader it interacts with
// according to its name resolution strategy.
// ...
// ClassLoaderData carries information related to a linkset (e.g.,
// metaspace holding its klass definitions).
// The System Dictionary and related data structures (e.g., placeholder table,
// loader constraints table) as well as the runtime representation of classes
// only reference ClassLoaderData.
//
// Instances of java.lang.ClassLoader holds a pointer to a ClassLoaderData that
// that represent the loader's "linking domain" in the JVM.

滞留字符串不需要在 GC 中存活。它们不是GC根。卸载无法访问的滞留字符串是可以的,而 HotSpot 实际上就是这样做的。


垃圾回收器本身不会引入新类型的根,但它可能使用影响“可访问性”含义的算法和数据结构。例如,并发收集器可以将在初始标记和最终注释之间修改的所有参考视为可访问的,即使它们不是。


答案 2

由于这篇文章有几个问题,我敢回答其中的几个问题:

1) 代码 blob 是 JITed 代码。它可能包含硬编码(作为汇编程序即时)对象指针(例如,指向类对象或静态 finals)。如果移动对象,则调整代码中的立即对象。

2)不知道

3) 类装入器数据是本机元数据对象(不在堆内),可能包含对已装入类的引用(例如)。

4) 插入的字符串与常规对象一样驻留在堆中(在 perm gen 中的旧 VM 中)。唯一的区别是(出于实习和缓存的原因),它们通常从不被收集,并且总是隐含地活着。5)据我所知,GC本身不应该引入一个新的GC根类别,毕竟,GC根作为一个独立于GC的概念。但是,每个 GC 可能以不同的方式存储和处理它们。

编辑:

只是想到了别的事情:

2)VM大量使用闭包,这基本上意味着虚拟调用。但是,虚拟调用可能很昂贵(尤其是当您经常执行虚拟调用时,例如,对于堆中的每个对象和每个指针),因此 VM 通常不会合并现有的闭包,而是经常实现专门的闭包以避免不必要的虚拟调用。这可能是它的一个原因。

1')我刚刚注意到有人可能会将我的答案解释为它只是冗余的根(因为类对象和静态最终结果也总是从其他地方引用)。首先,从 GC 的角度来看,它们不是多余的,因为如果移动对象,仍必须调整即时按钮。其次,JIT可能会决定对指向任何对象的指针进行硬编码,例如,如果它检测到在解释时,特定调用总是返回指向同一对象的指针。因此,代码 blob 根可能是特定对象的唯一根。


推荐