[讨论] HotSpot VM Serial GC的一个问题

RednaxelaFX 2014-01-15
LeafInWind 写道
R神的代码没有问题,是我对top的理解有误,top其实是随着对象的分配在不断的递减的,我把它当做end了

是说递增?
LeafInWind 2014-01-15
RednaxelaFX 写道
LeafInWind 写道
R神的代码没有问题,是我对top的理解有误,top其实是随着对象的分配在不断的递减的,我把它当做end了

是说递增?

又看了代码,的确应该是递增
LeafInWind 2014-01-16
仔细看了hotspot的FastEvacuateFollowersClosure的代码,感觉好累,都是嵌套的宏定义,代码跳转都没有用了,不得不一点一点手动记下来,好累
另外也不知后缀 _v 和 _nv 是什么意义,n是表示narrow吗?
RednaxelaFX 2014-01-17
有_v与_nv后缀的方法,前者是virtual版,后者是non-virtual版。
HotSpot里有不少代码写得很纠结。一方面在设计的时候想尽量灵活,所以很多函数都声明为虚函数;另一方面某些调用频率非常高的函数如果是虚函数实在是太慢了,所以就有很hack的办法硬生生的把某些类虚函数又加上非虚版本。

这种细节在读代码了解大体实现思路的时候要尽量忽略掉⋯不然会看到吐的
LeafInWind 2014-01-20
今天仔细看了FastEvacuateFollowersClosure::do_void(),有一个问题!
首先,FastEvacuateFollowersClosure::do_void()代码如下:
  do {
    _gch->oop_since_save_marks_iterate(_level, _scan_cur_or_nonheap,
                                       _scan_older);
  } while (!_gch->no_allocs_since_save_marks(_level));

对SerialGC来说,就是要分别调用DefNewGeneration和TenuredGeneration的oop_since_save_marks_iterate,而前者的代码如下
void DefNewGeneration::                                         \
oop_since_save_marks_iterate##nv_suffix(OopClosureType* cl) {   \
  cl->set_generation(this);                                     \
  eden()->oop_since_save_marks_iterate##nv_suffix(cl);          \
  to()->oop_since_save_marks_iterate##nv_suffix(cl);            \
  from()->oop_since_save_marks_iterate##nv_suffix(cl);          \
  cl->reset_generation();                                       \
  save_marks();                                                 \
}

调用to()的oop_since_save_marks_iterate当然没问题,这正是遍历引用链需要的;
调用from()的oop_since_save_marks_iterate也就算了,对象不会分配在from上,其top和save mark都来自上一次GC时的to区域,因此应该是相等的,故不会遍历;
但调用eden()的oop_since_save_marks_iterate似乎就有问题了:
  • 除非对象在eden上分配时,修改top指针的同时也修改save mark,否则这里top不等于save mark,就将会有实际的遍历发生了啊

目前没有找到在eden区分配对象改变top的同时改变save mark的代码,不知是我不够耐心,还是我理解有误,存在其他机制。
RednaxelaFX 2014-01-21
哈哈,好问题。其实前面我的说明里已经覆盖到了这个地方,但不像楼主那么仔细的读代码的话是不会发现这种问题的。Good job。

RednaxelaFX 写道
* HotSpot VM的GC必须处理一些特殊情况,一个极端的例子是to-space和old generation的剩余空间加起来都无法容纳eden与from-space的活对象,导致GC无法完成。这使得许多地方代码看起来很复杂。但要了解主要工作流程的话可以先不关心这些旁支逻辑。

就是这里。HotSpot VM的某些GC在“generational GC framework”里,它们最初实现的时候尽量做得比较通用,以便任何young generation可以跟old generation混在一起用。DefNew就是这个框架里的一个young generation的实现。所以为了保证能处理任何可能性,DefNew里甚至能容忍在GC时在eden里分配对象…但因为没有实际代码真的用到了这个地方,说不定其实这个支持已经用不了了,只是代码还在那里…
LeafInWind 2014-01-21
谢谢R神的回复,又看了代码,终于找到了如下问题的答案
引用
但调用eden()的oop_since_save_marks_iterate似乎就有问题了:

答案就在于GenCollectedHeap::do_collection方法中
       // Do collection work
        {
          HandleMark hm;  // Discard invalid handles created during gc
          save_marks();   // save marks for all gens
          ...
          _gens[i]->collect(full, do_clear_all_soft_refs, size, is_tlab);

可以看到,在调用各个代的collect前,先调用了save_marks()方法,该方法会将所有代的所有space的save mark置为与top相等。故collect方法中最后对eden和from的遍历不会有实际动作。

另外,下面的说法似乎有问题
引用
调用from()的oop_since_save_marks_iterate也就算了,对象不会分配在from上,其top和save mark都来自上一次GC时的to区域,因此应该是相等的,故不会遍历;

DefNewGeneration::allocate_from_space就是在from区间直接分配对象。
LeafInWind 2014-01-21
再问一个问题
void GenCollectedHeap::
gen_process_strong_roots(int level,
                         bool younger_gens_as_roots,
                         bool activate_scope,
                         bool collecting_perm_gen,
                         SharedHeap::ScanningOption so,
                         OopsInGenClosure* not_older_gens,
                         bool do_code_roots,
                         OopsInGenClosure* older_gens) {
  // General strong roots.

  if (!do_code_roots) {
    SharedHeap::process_strong_roots(...);
  } else {
    bool do_code_marking = (activate_scope || nmethod::oops_do_marking_is_active());
    CodeBlobToOopClosure code_roots(not_older_gens, do_code_marking);
    SharedHeap::process_strong_roots(...);
  }

  if (younger_gens_as_roots) {
    if (!_gen_process_strong_tasks->is_task_claimed(GCH_PS_younger_gens)){
      for (int i = 0; i < level; i++) {
        not_older_gens->set_generation(_gens[i]);
        _gens[i]->oop_iterate(not_older_gens);
      }
      not_older_gens->reset_generation();
    }
  }
  // When collection is parallel, all threads get to cooperate to do
  // older-gen scanning.
  for (int i = level+1; i < _n_gens; i++) {
    older_gens->set_generation(_gens[i]);
    rem_set()->younger_refs_iterate(_gens[i], older_gens);
    older_gens->reset_generation();
  }

  _gen_process_strong_tasks->all_tasks_completed();
}


以上代码中
1。process_strong_roots负责将栈上的root直接引用的并且在DefNew中的对象移动到to区域或者年老代,但如果root引用的对象在年老代,就不动它了。
2。if (younger_gens_as_roots) {}对于DefNew没有意义,因为其level就是0。
3。for (int i = level+1; i < _n_gens; i++) {...}负责遍历年老代中dirty card的对应区域,作为根节点查找并移动DefNew中被引用的对象,同时清除dirty card,如果对象移动后仍然在DefNew中,还需将相应card 置dirty。

上面第3步正是minor GC的关键所在,因为本来第1步中栈上的root完全有可能引用年老代中的对象,但minor GC却忽略它们,而只通过第3步做一个模糊的遍历,否则年老代中的对象可能存在大量的引用,其引用链的遍历代价可能就很大了。

请问R神,我对上述三个步骤的总结以及对最后minor GC的理解是否正确。
RednaxelaFX 2014-01-22
LeafInWind 写道
1。process_strong_roots负责将栈上的root直接引用的并且在DefNew中的对象移动到to区域或者年老代,但如果root引用的对象在年老代,就不动它了。

差不多。
SharedHeap::process_strong_roots()扫描所有一定是根的东西,包括
* Universe所持有的一些必须活着的对象,
* 所有JNI handles,
* 所有线程的栈,
* 所有当前被锁着的对象
* VM内实现的MBean所持有的对象
* JVMTI所持有的对象
* (可选):所有已加载的类 或 所有已加载的系统类 (SystemDictionary)
* (可选):所有驻留字符串(StringTable)
* (可选):代码缓存(CodeCache)
* (可选):PermGen

不只是线程栈喔。

HotSpot VM的GC在minor GC的时候只收集young generation,不收集old generation和perm generation,不卸载类(class unloading),也不卸载。所以在这种条件下,下述几项都是strong root:
* 所有已加载的类
* 所有驻留字符串
* 所有动态生成的代码(CodeCache)
* PermGen的remember set所记录的存在跨代引用的区域

在full GC的时候,上述一些项就不需要是strong root了。

然后,当只收集GC堆的一部分而不是收集全堆时,GenCollectedHeap::gen_process_strong_roots()在上述基础上会进一步把跨代引用当作strong root。对Serial GC的minor GC来说,它收集young generation而不收集old/perm generation,所以后两者中的跨代引用就是strong root,所以要扫描后两者的remember set。

LeafInWind 写道
2。if (younger_gens_as_roots) {}对于DefNew没有意义,因为其level就是0。

嗯没错。
HotSpot VM里唯一用到这个地方的就是CMS的并发收集,它只收集old generation,可选收集perm generation,不收集young generation。所以它需要扫描young generation作为strong root。可以参考 CMSCollector::checkpointRootsInitialWork() 和 CMSCollector::do_remark_non_parallel()。

LeafInWind 写道
3。for (int i = level+1; i < _n_gens; i++) {...}负责遍历年老代中dirty card的对应区域,作为根节点查找并移动DefNew中被引用的对象,同时清除dirty card,如果对象移动后仍然在DefNew中,还需将相应card 置dirty。

差不多。移动对象的时候不会把原本dirty的card清掉。你想像一下,如果一个old generation里的对象原本有引用到young generation,那么被引用的对象如果被移动到to space了,它还是活在young generation里的,没必要清理掉old generation哪个对应的card。
其实每次移动对象都会检查一下是否产生了新的跨代引用(例如说有对象晋升到了old generation,而该对象还引用着在young generation里的对象)。如果有的话就得相应把card置dirty。
LeafInWind 2014-01-22
引用
移动对象的时候不会把原本dirty的card清掉。

又看了代码,还是觉得原本的dirty card会清掉。首先,理论上说,如果young gen中的对象被移到了to区域,其位置变了,因此原来的card应该清掉,然后将新位置对应card置dirty;其次,如果对象被移动了old gen,那么card已经没有意义了,当然应该清掉。

看代码,第3步中最终调用的是CardTableModRefBS::non_clean_card_iterate_possibly_parallel,其代码如下
      DirtyCardToOopClosure* dcto_cl = sp->new_dcto_cl(cl, precision(),
                                                       cl->gen_boundary());
      ClearNoncleanCardWrapper clear_cl(dcto_cl, ct);

      clear_cl.do_MemRegion(mr);

从ClearNoncleanCardWrapper的命名看,它就应该会清card,然后看它的代码,有如下语句
    if ((*cur_entry != CardTableRS::clean_card_val()) && clear_card(cur_entry)) {

当当前card not clean时,就会调用clear_card(cur_entry)将当前card清掉。

引用
其实每次移动对象都会检查一下是否产生了新的跨代引用(例如说有对象晋升到了old generation,而该对象还引用着在young generation里的对象)。如果有的话就得相应把card置dirty。

这个移动我觉得应该是不需要检查跨代引用及修改card table的。因为这些对象移动后都还在save mark和top之间,因此oop_since_save_mark_iterate方法能够完成对对象中所有oop field的scan,不需要借助card table。另外从代码看,移动对象使用的始终是gc_barrier为false的FastScanClosure,因此也不会修改card table。
Global site tag (gtag.js) - Google Analytics