JVM YGC

Introduction

预备知识

otSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次YGC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次YGC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。YGC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。


YGC - Young GC

YGC是JVM GC当前最为频繁的一种GC,一个高并发的服务在运行期间,会进行大量的YGC,发生YGC时,会进行STW,一般时间都很短,除非碰到YGC时,存在大量的存活对象需要进行拷贝。

一次YGC过程主要分成两个步骤:

  1. 查找GC Roots,拷贝所引用的对象到 to 区;
  2. 递归遍历上一步中查找到的对象,并拷贝其所引用的对象到 to 区,当然可能会存在自然晋升,或者因为 to 区空间不足引起的提前晋升的情况;

Find GC Roots

YGC的第一步根据GC Roots找出第一批活跃的对象,Hotspot中通过 gch->gen_process_strong_roots 方法实现

Serial GC源代码为例

  • 在黄色框的实现中,SharedHeap::process_strong_roots()扫描了所有一定是GC Roots的内存区域;
  • 红色框中的实现逻辑对于YGC来说是没有意义的,因为level=0,Hotspot中唯一用到这个地方的只有CMS GC实现,默认只收集old generation,所以需要扫描young generation作为它的Strong root;
  • 如果一个old generation的对象引用了young generation,那么这个old generation的对象肯定也属于Strong root的一部分,这部分逻辑并没有在process_strong_roots中实现,而是在绿色框中实现了,其中rem_set中保存了old generation中dirty card的对应区域,每次对象的拷贝移动都会检查一下是否产生了新的跨代引用,比如有对象晋升到了old generation,而该对象还引用了young generation的对象,这种情况下会把相应的card置为dirty,下次YGC的时候只会扫描dirty card所指内存的对象,避免扫描所有的old generation对象。

process_strong_roots的实现,主要包括了以下东西:

  • Universe类中所引用的一些必须存活的对象 Universe::oops_do(roots)
  • 所有JNI Handles JNIHandles::oops_do(roots)
  • 所有线程的栈 Threads::oops_do(roots, code_roots)
  • 所有被Synchronize锁持有的对象 ObjectSynchronizer::oops_do(roots)
  • VM内实现的MBean所持有的对象 Management::oops_do(roots)
  • JVMTI所持有的对象 JvmtiExport::oops_do(roots)
  • (可选)所有已加载的类 或 所有已加载的系统类 SystemDictionary::oops_do(roots)
  • (可选)所有驻留字符串(StringTable) StringTable::oops_do(roots)
  • (可选)代码缓存(CodeCache) CodeCache::scavenge_root_nmethods_do(code_roots)
  • (可选)PermGen的remember set所记录的存在跨代引用的区域 rem_set()->younger_refs_iterate(perm_gen(), perm_blk)
    注意:YGC在执行时只收集young generation,不收集old generation和perm generation,并不会做类的卸载行为,所以上述可选部分都作为Strong root,但是在FGC时就不会当作Strong root了。

Recurse Roots

在查找GC Roots的步骤中,已经找出了第一批存活的对象,这些存活对象可能在 to-space,也有可能直接晋升到了 old generation,这些区域都是需要进行遍历的,保证所有的活跃对象都能存活下来。
遍历过程的实现由FastEvacuateFollowersClosure类的do_void方法完成,这是一个*-Closure 方式命名的类,实现如下:

每个内存区域都有两个指针变量,分别是 _saved_mark_word 和 _top,其中_saved_mark_word 指向当前遍历对象的位置,_top指向当前内存区域可分配的位置,其中_saved_mark_word 到 _top之间的对象是已拷贝,但未扫描的对象。

GC Roots引用的对象拷贝完成后,to-space的_saved_mark_word和_top的状态如上图所示,假设期间没有对象晋升到old generation。每次扫描一个对象,_saved_mark_word会往前移动,期间也有新的对象会被拷贝到to-space,_top也会往前移动,直到_saved_mark_word追上_top,说明to-space的对象都已经遍历完成。

其中while循环条件 while (!_gch->no_allocs_since_save_marks(_level),就是在判断各个内存代中的_saved_mark_word是否已经追到_top,如果还没有追上,就执行_gch->oop_since_save_marks_iterate进行遍历,实现如下:

to-space对象的遍历实现:

这里的blk变量是传递过来的FastScanClosure回调函数,oop_iterate方法会遍历该对象的所有引用,并调用回调函数的do_oop_work方法处理这里引用所指向的对象。

do_oop_work的实现:

在FastScanClosure回调函数的do_oop_work方法实现中,红框的是重要的部分,因为可能存在多个对象共同引用一个对象,所以在遍历过程中,可能会遇到已经处理过的对象,如果遇到这样的对象,就不会再次进行复制了,如果该对象没有被拷贝过,则调用 copy_to_survivor_space 方法拷贝对象到to-space或者晋升到old generation,这里提一下ParNew的实现,因为是并发执行的,所以可能存在多个线程拷贝了同一个对象到to-space,不过通过原子操作,保证了只有一个对象是有效的。

copy_to_survivor_space 的实现:

拷贝对象的目标空间不一定是to-space,也有可能是old generation,如果一个对象经历了很多次YGC,会从young generation直接晋升到old generation,为了记录对象经历的YGC次数,在对象头的mark word 数据结构中有一个位置记录着对象的YGC次数,也叫对象的年龄,如果扫描到的对象,其年龄小于某个阈值(tenuring threshold),该对象会被拷贝到to-space,并增加该对象的年龄,同时to-space的_top指针也会往后移动,这个新对象等待着被扫描。


Full GC 和 YGC 触发条件

YGC:对新生代堆进行GC。频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小。

YGC触发条件:

  • edn空间不足

FGC:全堆范围的GC。默认堆空间使用到达80%(可调整)的时候会触发FGC。
FGC触发条件:

  • old空间不足;
  • perm空间不足;
  • 显示调用System.gc(),包括RMI等的定时触发;
  • YGC时的悲观策略;
  • dump live的内存信息时(jmap –dump:live);
  • heap dump。

悲观策略:当准备要触发一次 young GC时,如果发现统计数据说之前 young GC的平均晋升大小比目前的 old gen剩余的空间大,则不会触发young GC而是转为触发 full GC。
因为HotSpot VM的GC里,除了垃圾回收器 CMS 的 concurrent collection 之外,其他能收集 old gen 的GC都会同时收集整个GC堆,包括young gen,所以不需要事先准备一次单独的young GC。


Reference

http://ifeve.com/jvm-yong-generation/
https://www.jianshu.com/p/9af1a63a33c3
https://juejin.im/post/5b8d2a5551882542ba1ddcf8
https://www.zhihu.com/question/41922036/answer/93079526