新生代DefNewGeneration的实现
内存初始化实现
分别通过EdenSpace
和ContiguousSpace
类实现新生代的eden
和from to
区域,(其中Contiguous是"连续"的意思,表示一块连续的内存空间),其中EdenSpace
在实现上是继承自ContiguousSpace
的。
1、如果_eden_space、_from_space、_to_space
其中任何一个为空,说明新生代分配内存失败,则虚拟机退出;
2、根据SurvivorRatio
计算from to
内存区应该分配的大小;
3、剩余的空间大小分配给eden
内存区;
GC过程实现
基本思路:
1、扫描内存堆所有的根对象集T,并把它们复制到新的内存块,一般为新生代的to
内存区或老年代;
2、分析扫描这些根对象集T的所有引用对象集T1,并把它们复制到新的内存块;
3、继续分析对象集T1所引用的对象集T2,一直迭代下去,直到对象集Tn为空;
GC过程位于collect()
方法中:
GC检测
1、确保当前是一次FGC
,或需要分配的内存大小size
大于0,否则不需要执行一次gc操作;
2、因为当前是最年轻代的管理器,确保有下一个内存管理器;
否则,设置_incremental_collection_failed
为false
,即当前minor gc
不可用,通知内存堆管理器不要再尝试增量式GC了,因为肯定会失败;
GC开始
GC准备工作:
1、初始化IsAliveClosure
和ScanWeakRefClosure
;
2、清空age_table
数据和to
区;
3、初始化FastScanClosure,负责存活对象的标识和复制;
广度优先遍历扫描活跃对象
1、循环条件no_allocs_since_save_marks()
主要检查当前代、更高代以及永久代scanned指针_saved_mark_word是否与当前空闲分配指针位置相同
因为在scanned指针到空闲分配指针之间的区域是已分配但未扫描的对象,对这块区域的对象调用遍历函数进行处理,标记所引用的对象,并保存新的scanned指针。
对象的标记和复制实现
对象的标记和复制过程最终由FastScanClosure
的do_oop
方法实现,其中do_oop
方法又调用了do_oop_work
方法,do_oop_work
究竟做了什么?
使用模板函数解决不同类型的指针(实际oop和压缩过的narrowOop):
1、当该指针对象非空时,通过decode_heap_oop_not_null
方法获取对象obj
;
2、如果该对象obj
在遍历区域(_boudary是在FastScanClosure
初始化的时候,为初始化时指定代的结束地址,与当前遍历代的起始地址_gen_boundary
共同作为对象的访问边界),则通过obj->is_forwarded()
判断该对象是否已经标记过,如果对象没有被标识过,即其标记状态不为marked_value,则通过_g->copy_to_survivor_space(obj)
方法把该对象复制到to
区域;
3、根据是否使用指针压缩将新的对象地址进行压缩;
处理晋升成功
如果GC过程中没有发生对象的晋升失败,则执行如下逻辑:
1、既然所有对象都晋升成功了,说明存活对象都转移到了to
区域或老年代,则通过clear
方法清空eden
和from
区;
2、通过swap_spaces
方法交换from
和to
区域,为下次GC作准备
通过交换_from_space
和_to_space
的起始地址实现from
和to
区的角色互换,并重新设置eden
的_next_compaction_space
,即eden
的下一个内存区域;
3、from
和to
区互换之后,当前的to
区应该已经是块空区域了;
4、调用ageTable
的compute_tenuring_threshold
方法对晋升阀值_tenuring_threshold
重新设置,实现如下:
当前年龄age=6,如果年龄age=0的对象,到年龄age=6的对象总空间累加,大于了survivor空间的50%。那么就把这个Math.min(age,15) 当做晋升的阈值。如果累加的空间为40%,小于了50%,说明了当前age=6仍然过低,age++,变为age=7,继续下一次while循环计算,看看累计占用空间是否会超过survivor区的50%。
处理晋升失败
如果GC过程中存在晋升失败,则执行如下逻辑:
1、当对象被标记为活跃对象时,其对象头markword
指向经过复制后对象的新地址,remove_forwarding_pointers
负责恢复晋升失败对象的markOop
当对象晋升失败时,对象的oop
会被保存在_objs_with_preserved_marks
栈中,对应的对象头markOop
被保存在_preserved_marks_of_objs
栈中,通过这两个栈,可以对晋升失败的对象的对象头进行恢复;
2、对from
和to
区进行互换,并设置from
的下一片需要进行压缩的区域为to
区,因为当有对象晋升失败时,并不会清空eden
和from
区,这时对from
和to
区互换,但to
区还有活跃对象,这样在随后触发的FGC
能够对from
和to
进行压缩处理;
3、设置新生代的minor gc
失败标识,并通知下一个内存代(老年代)发生晋升失败,比如在ConcurrentMarkSweepGeneration
会根据参数CMSDumpAtPromotionFailure
进行dump输出以供JVM问题诊断
1 为什么要有Survivor区
如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。
2 为什么要设置两个Survivor区
设置两个Survivor区最大的好处就是解决了碎片化
那么,顺理成章的,应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。下图中每部分的意义和上一张图一样,就不加注释了
上述机制最大的好处就是,整个过程中,永远有一个survivor space是空的,另一个非空的survivor space无碎片。
那么,Survivor为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果Survivor区再细分下去,每一块的空间就会比较小,很容易导致Survivor区满,因此,我认为两块Survivor区是经过权衡之后的最佳方案。