https://gist.github.com/pandening/8b941997851ff6bec29d83a9af821602
首选想探索一下GC是怎么开始工作的,或者说,GC到底是以什么样的方式在工作的;java应用在启动的时候会创建一个jvm进程,JVM内部通过调用create_vm来实现,该方法做了大量的工作来创建一个jvm进程,并且将java应用的main方法启动起来,运行在main线程中(主线程);在create_vm中,有一个地方值得关注,下面是thread.cpp中create_vm方法的代码片段: VMThread是一种特殊的jvm线程,用于执行比如GC等操作,java代码的Thread和JVM里面的JavaThread对应,这一点后续再研究;上面的代码片段首先关注【VMThread::create()】这个函数调用,在VMThread.cpp中实现了该函数: create函数在new了一个VMThread对象实例同时,为该VMThread创建了一个VMOperationQueue,VMThread有一个重要的成员叫_vm_queue,看看它的定义: 根据注释可以将该queue理解为是VMThread的任务队列,但是队列内部存放的任务都是VMOperation,不能是其他类型的任务,那VMOperation是什么呢?其实有一个基类叫VM_Operation,有一个子类叫VM_GC_Operation,就是专门来做GC的任务,在对象申请内存分配失败的时候会生成一个VM_CollectForAllocation任务来做GC,_vm_queue队列就是用来存储这些任务的,VMThread会不断来check该队列是否有任务需要执行,这种工作模式类似于特殊的线程池,这个线程池只有一个VMThread,_vm_queue就是线程池中的任务队列; thread_native_entry就是上面提到的代码入口,可以在thread_native_entry函数内部看到执行了VMThread的run方法,到此create_vm函数可以继续执行;
无论如何,接下来就是要执行队列中取出来的任务了,所以evaluate_operation(_cur_vm_operation)方法应该是我们接下来应该关注的;在evaluate_operation函数内部看到了调用了evaluate()函数,接着看看evaluate函数; 关键的是doit()函数,这里面就是具体的任务执行内容,不同的Operation的doit内容都是不一样的,就算是GC_Opertion,还是有多种不同的方式的,比如上面提到了VM_GenCollectForAllocation的doit内容做的工作就是这样的: gch->satisfy_failed_allocation就是为了解决空间分配失败的,去看satisfy_failed_allocation函数的注释,可以看到: 这个函数会被VM_GenCollectForAllocation执行的时候回调,也就是doit函数执行的时候调用这个函数,这个函数会做类似于垃圾收集,堆扩展等工作来满足一个"allocation request";当然,回调这个函数之前必然已经尝试进行空间分配申请了,并且已经失败了,所以该函数需要极尽所能去做工作来腾出空间(申请新的空间)来满足已经失败的空间分配申请;collectorPolicy类实现了垃圾收集的策略,所谓垃圾收集策略就是应该在什么时候做GC,做什么类型的GC等,参考价值很大;下面可以试着来看一下satisfy_failed_allocation函数具体是怎么做的; 从上面这张图可以看到,如果发现gc_lock是活动的,也就说明已经有其他的线程触发了GC,那么这个时候策略就是扩展堆来满足内存申请。 看if条件,如果增量GC是安全的,那么就执行增量安全,所谓增量GC,就是按照从轻到重的程度来做垃圾回收,大概分这么几个级别,首先是进行一次MinorGC,其次是进行一次FullGC,最后是进行一次带soft reference清理的FullGC;上面的图片对应的是第一种情况,进行一次MinorGC,然后尝试申请空间,如果成功就打住了,否则就要进行一次清理soft reference的FullGC了,硕大soft reference,可以大概说一下,java中的引用分四个级别,strong reference > soft reference > weak reference > phantom reference;强度梯度下降,strong reference只要对象还在被引用就不会被回收,而soft reference就不一样了,JVM在尝试进行GC来解决内存不足的状况下,如果发现还是无法满足内存申请,那么就会将这部分引用类型的对象回收回来,所以,在使用soft reference的时候不应该强依赖于对象,因为不知道什么时候就被回收了,这种引用可以用在缓存的场景中;weak reference的强度比soft弱一些,它只能存活到下次GC发生,而phantom reference就更弱了,弱到你根本无法获取到一个phantom reference对象,它唯一的作用就是可以在发生GC的时候告诉你它已经被回收了;下面的代码展示了进行FullGC的两种情况: ** 总结一下再allocate fail的应对策略,首先判断是否有其他线程触发了GC操作,如果是的话则不会进行GC操作,而是尝试去扩展堆来解决allocate fail,否则判断是否可以进行增量GC,如果可以,那么执行一次MinorGC,否则执行一次不回收soft reference的FullGC,之后判断是否可以解决allocate fail了,如果可以了就到此打住,否则进行一次彻底的FullGC,也就是将soft reference也回收回来 ** 其实在(3)的时候,漏掉一个细节,VMThread会将任务队列中填充好的任务都执行完成,才会继续执行接下来的代码;最后希望能看一下到底在什么地方会将任务填充到VMThread的任务队列中去;还是拿VM_GenCollectForAllocation来说,可以在collectorPolicy.cpp中的mem_allocate_work看到执行了类似下面的代码: 然后又回来VMThread看execute方法,可以看到下面的细节: 到此,GC任务是怎么运行的大概梳理了一下,具体的GC细节还是需要再梳理。 |
OwnerAuthor
pandening commented on 11 Nov 2018 •
edited
这个comment希望能分析一下GenCollectedHeap::do_collection这个函数的具体执行流程,根据函数名字可以猜测该函数实现的功能就是做垃圾回收,下面是它的方法声明(声明和定义是有区别的,声明仅仅是告诉别人有这样一个函数,而定义则是说这个函数具体实现了什么功能): 参数full代表是否是FullGC,clear_all_soft_refs参数表示是否要回收sort reference,size参数需要多说明一点,在一些情况下,GC发生是因为发送了"Allocate Fail",这个size就代表了申请分配的内存大小;is_tlab表示是否使用 TLAB(线程分配Buffer,可以避免多线程在堆上并发申请内存),max_generation参数表示最大的回收代,只有两种类型,YoungGen或者OldGen;下面来仔细分析一下这个函数。
下面,来分析上面几个步骤中出现的一些关键函数,首先是should_collect函数,该函数用于判断某一个分代是否需要做垃圾回收下面来看看该方法的细节 如果是FullGC,那么无论哪个分代都应该被回收,如果不是FullGC,那么就使用should_allocate函数继续判断是否需要在该分代进行收集,比如对于DefNew(Serial GC下新生代)分代来说,其具体实现就如下: 接着一个重要的函数就是collect_generation,这个函数将回收给定的分代中的垃圾,主要看下面的这段代码片段: 接着看gen->collect函数调用,这里面就是做具体的垃圾收集工作,比如下面分析在DefNew分代中的gen->collect实现。
collection_attempt_is_safe函数的实现如下: 正常来说,用区域内两个survivor中有一个区域总是空闲的,但是在某些情况下也会发生意外,使得两个survivor都不为空,这种情况是有可能发生的,首先DefNew在进行YoungGC之后,会将Eden + From中存活的对象拷贝到To中去,并且将一些符合晋升要求的对象拷贝到old区域中去,然后调换两个survivor的角色,所以按理来说其中某个survivor区域总是空的,但是这是在YoungGC顺利完成的情况,在发生"promotion failed"的时候就不会去清理From和To,这一点在后续会再次说明;但是肯定的是,如果To区域不为空,那么就说明前一次YoungGC并不是很顺利,此时DefNew就举得没必要再冒险去做一次可能没啥用处的Minor GC,因为有可能Minor GC之后需要出发一次Full GC来解决某些难题,所以DefNew基于自己的历史GC告诉Old去做一些较为彻底的GC工作时必要的;如果没有发生"promotion fail"这种不愉快的事情,那么接下来就让old区自己判断是否允许本次Minor GC的发生,也就是_old_gen->promotion_attempt_is_safe的调用,下面来看看该函数的具体实现; 老年代也会看历史数据,如果发现老年代的最大连续空间大小大于新生代历史晋升的平均大小或者新生代中存活的对象,那么老年代就认为本次Minor GC是安全的,没必要做一次Full GC;当然这是有一些冒险的成分的,如果某一次minorGC发生之后符合晋升条件的对象大小远远大小评价晋升大小,而且这个时候老年代连续空间小于这些符合晋升的对象大小的时候,悲剧就发生了,也就是上面说到的"promotion fail",这个时候就要做一次Full GC。
FastEvacuateFollowersClosure是一个递归的过程,Closure后缀代表 它是一个回调操作,所谓递归,就是在判断对象存活并且copying的工作是递归进行的,首先找到root objects,然后根据root objects去标记存活的对象,并且将它们转移到合适的区域中去;gch->young_process_roots做的工作就是将root objects转移到其他空间去的函数: 这里面关键的函数是process_roots,该函数会对设置的各种Closure进行回调,比如FastScanClosure,具体的回调工作将在Closure的do_oop_work进行: 如果对象已经被复制过了,那么就不用再复制一次了,否则调用copy_to_survivor_space将该对象复制到to区域中去,下面是copy_to_survivor_space函数的具体实现: 这个函数的流程大概是这样的:首先判断对象是否达到了晋升到老年代的年龄阈值,如果到了,那么就要将对象拷贝到老年代中去,否则就要将对象拷贝到to区域中去,这里面也包括一个细节,如果对象没有达到晋升老年代的年龄阈值,但是无法拷贝到To区域中去,那么也试图将对象晋升到老年代,也就是将对象提前晋升,晋升是有风险的,可能晋升失败,那么就要通过调用handle_promotion_failure来处理晋升失败的情况,如果对象成功拷贝到了To区域中来,那么就要将对象的年龄更新一下,最后,需要需要标记对象已经被转移,如果可能,那么就把老的对象清空吧;下面来先来看看promote函数,该函数用于将对象晋升到老年代: 这个函数较为简单,首先通过allocate函数试图在老年代申请一块可以容纳对象的内存,如果成功了,那么就将对象复制到里面去,否则通过handle_failed_promotion函数来处理晋升失败的情况,晋升失败的前提下,handle_failed_promotion在handle_promotion_failure前执行,看起来都是处理晋升失败的情况,下面先看看handle_failed_promotion: 可以看到,oldGen将试图去扩展自己的堆空间来让更多的新生代对象可以成功晋升,但是很多情况下,堆空间被设置为不可扩展,这种情况下这个方法也就做了无用功,接着会调用handle_promotion_failure,调用handle_promotion_failure代表老年代也就明确告诉新生代无法将本次晋升的这个对象放置到老年代,来看看handle_promotion_failure会有什么对策: 看起来DefNew还是比较乐观的,既然老年代容纳不了你,那么这个晋升的对象就还呆在新生代吧,说不定下次老年代发生GC就可以成功把它拷贝过去呢。这个时候_promotion_failed也被标记物为了true,这个标记之后会有用,发生"promotion fail"之后From区域可能存在一些对象没有成功晋升到老年代,但是又不是垃圾,这个时候From和To区域都不为空了,这是个难题。 接着,是时候执行递归标记&复制的过程了,也就是evacuate_followers.do_void(),这个过程是非常复杂的,下面来稍微看看这个函数: 不断使用oop_since_save_marks_iterate来做递归遍历的工作,结束条件是通过no_allocs_since_save_marks来决定的,下面是no_allocs_since_save_marks函数的具体实现: 看名字应该是说没有分配发生了,比如看看DefNew的no_allocs_since_save_marks函数实现: top()指向To区域空闲空间的起点,上面已经说过的一个过程是将root objects先标记并且拷贝到To区域或者老年代,这个时候To区域内已经存在的对象是存活的,需要递归遍历这些对象引用的对象,然后也进行拷贝工作,saved_mark_at_top就是判断是否还在有对象呗拷贝到To区域中来,如果还有对象拷贝进来,那么就说明GC还没有完成,继续循环执行oop_since_save_marks_iterate,否则就可以停止了;下面来看看oop_since_save_marks_iterate函数的实现: 在深入下去的部分就比较复杂了,不再做分析,但是需要注意的一点是,DefNew在将存活对象复制到To区域的时候,Eden + From区域的对象是否存活不仅仅会看是否被To区域的对象引用,还会看老年代是否存在跨代引用新生代的对象的情况,这种情况也需要将存活的对象转到To或者老年代。
无论如何,新生代发生了GC,经过这次GC,需要转换From和To两个survivor的角色,swap_spaces函数实现了这个功能: 这个函数较为简单,只是swap了一下From和To;再说一句,如果没有发生"Promotion Fail",那么在Minor GC之后,需要将From和Eden清空,因为没有发生晋升失败事件,就说明所以在新生代(Eden + From)存活的对象都安全的转移到了To或者老年代,所以可以清空,但是发生晋升失败意味着有部分存活的对象依然还留在原地等待,所以不能clear掉。 |
OwnerAuthor
pandening commented on 11 Nov 2018 •
edited
DefNew的GC属于Minor GC,使用copying算法进行垃圾收集,是Serial GC(-XX:+UseSerialGC)的新生代部分,接下来分析一下Serial GC的老年代部分,也就是Serial Old;TenuredGeneration是Serial Old的堆实现,这里还是要说一下什么情况下可能会发生Old GC,在分析DefNew的时候提到了所谓的"空间分配担保",也就是YoungGen在即将进行Minor GC的时候,让OldGen判断一下是否可以进行这次Minor GC,判断的方法是OldGen可用的连续空间大于新生代的对象大小或者大于新生代历史晋升的平均大小,如果这个条件成立的话,那么Minor GC就会进行,否则就会进行一次Major GC;下面将以TenuredGeneration的实现来分析一下OldGC的实现细节。 主要关注GenMarkSweep::invoke_at_safepoint函数调用,这是整个TennredGeneration垃圾收集的核心,invoke_at_safepoint函数通过调用下面四个函数来做具体的垃圾收集工作。 下面根据每个步骤分别来分析一下具体的GC过程。
来看看full_process_roots函数的具体情况: process_roots是需要重点关注的函数,这个函数将扫描出所有可以作为GCRoot的对象,扫描的地方非常多,可以参考下面这个代码片段: 这个strong_roots就是上面提到的follow_root_closure,他负责标记存活的对象,去它对应的do_oop函数看看到底是怎么做的: 对象是否被标记过时存储在对象头里面的,如果一个对象没有被标记过,就会用mark_object将会标记一个对象,具体看看mark_object的实现: 调用了oop的set_mark方法进行对象标记,如果对象头里面的信息需要被保存起来稍后GC完成需要恢复,那么就要调用preserve_mark将对象头的信息存储起来,mark是对象的oldMark,厦门市hipreserve_mark的实现: 这个函数较为简单,如果_preserved_marks里面存储了太多的对象头信息超出限制了,那么就将对象头信息分别存储在_preserved_mark_stack和_preserved_oop_stack两个栈里面,否则存储在_preserved_marks里面去;说完了对象的标记,下面来看看follow_object; follow_object根据名字可以猜测是处理obj的引用,我想这也是一个递归的过程,具体看看上面的代码片段,如果对象是一个数组对象,那么就使用follow_array来处理,否则使用对象的oop_iterate函数来处理,数组对象单独处理的原因是如果数组对象和普通对象一起处理,数组对象非常大的时候可能会影响普通对象的处理;follow_array最后依然还是使用follow_object来处理数组元素中的对象的,看看follow_array: 如果数组长度大于0,那么就使用push_objarray来处理这个数组: push_objarray将数组push到了_objarray_stack栈里面,follow_stack函数会去处理_objarray_stack栈中的数组对象: 从_marking_stack中拿出数组对象之后,调用follow_object继续处理,但是这时候follow_object里面的已经不是一个纯粹的数组对象了,已经是一个ObjArrayTask对象了,具体的标记过程泰国复杂就不继续深入了。 这个处理过程和Minor GC时的处理是一样的;接下来会做一些清理工作: SystemDictionary::do_unloading用于卸载一些不再使用到的类;CodeCache::do_unloading用于卸载一些不再使用到的方法(编译好的方法会放在CodeCache里面去);Klass::clean_weak_klass_links用于清理weak reference;StringTable::unlink(&is_alive)用于删除一些不再使用的字符串常量;SymbolTable::unlink()用于从符号表中清理那些不再使用的符号;
首先看prepare_for_compaction这个函数: 首先看oldGen的prepare_for_compaction函数实现: 这是一个循环处理过程,通过prepare_for_compaction函数来处理: scan_and_forward这个函数名字非常直观,扫描并且做forward,forward可以理解为将对象转移到一个新的位置,整个步骤(2)只是计算出一个对象的新地址,并没有将对象转移到新的地址去,转移对象到新地址的工作将在接下来的步骤(3)里面进行,下面的代码片段是步骤(2)处理的核心: 这个代码较长,主要完成的就一件事情,就是找到那些存活的对象,然后给这些存活的对象计算一个新的地址;为对象计算新地址的工作由CompactibleSpace::forward完成: CompactibleSpace::forward首先试图找到一块合适的内存来存放存活的对象,然后判断这块内存是否和存活对象目前所在的位置一样,如果一样的话就没必要移动了,否则就要改变指针来移动对象,移动的工作将在(3)中进行。 上面说了对存活对象的处理,对于死亡对象,首先找到下一个存活的对象,也就是找到一段连续的死亡对象,然后判断是否可以将这段死亡对象也当成是"活的"对象,判断条件还是比较严格的,首先,这段死亡对象的起点应该是compact_top,也就是空闲的空间起点(对于forward来说),并且通过dead_spacer.insert_deadspace的校验: _allowed_deadspace_words是允许死亡对象存储的空间大小,这部分空间是属于浪费调的,如果太大那就不行了,那为什么还要将死亡对象也当成"活着"的对象对待呢?因为对象拷贝也是有损耗的,如果一段死亡对象刚好不需要移动,并且浪费掉的空间在可以接受的范围内,那么何乐而不为呢?insert_deadspace这个方法就是做这件事情的,当然,这段死亡的对象会被使用一个新的长度和原来这段死亡对象长度相等的一个对象替换。
转移对象到新地址的工作由AdjustPointerClosure来完成,直接来看这个Closure的do_oop方法吧; 接着看adjust_pointer这个函数; adjust_pointer这个函数的目的是将对象p转移到new_obj里面去,在实现上,就是将new_obj的地址赋值给p即可:
GenCompactClosure会遍历老年代和新生代,做内存整理的工作;generation_iterate会根据配置从老年代或者新生代开始进行压缩工作: 下面是GenCompactClosure的do_generation函数: 跟着CompactibleSpace的compact函数看,CompactibleSpace::scan_and_compact是具体实现压缩工作的函数,下面来分析一下这个方法的实现细节; (1)、如果这块内存内部没有存活的对象,那么可以直接忽略这块内容 |
OwnerAuthor
pandening commented on 12 Nov 2018 •
edited
JVM可以帮我们管理内存,这是一件非常有意义的事情,我们再也不用担心allocate出来的内存没有在适当的时候free掉了,这个comment希望能去探索一下JVM是如何处理内容申请的,因为垃圾收集的发生就是因为申请了太多的内存,需要清理或者整理哪些已经没有价值的对象来释放空间,以满足新的内存分配申请;下面将以一个具体的内存申请问题出发,从源码角度去分析一下JVM的内存分配处理链路; JVM是如何为一个对象在堆上申请一块空间的? 在java语言中我们通过使用new关键字来创建一个新的对象,在虚拟机中对应着new指令,当然本文并不打算从new指令说起;instanceOopDesc对应java语言中的对象实例,所以创建一个新对象就是在JVM里面创建一个新的instanceOopDesc实例,InstanceKlass::allocate_instance用于创建一个新的instanceOopDesc实例,下面就从allocate_instance函数开始说起。 整个函数大概分三个步骤执行,首先取到实例所需要的空间大小size,然后使用CollectedHeap::obj_allocate去堆上申请一块大小为size的空间,最后判断实例是否实现了finalizer,如果有的话,那么就要使用register_finalizer注册finalizer;这里主要关心内存申请的部分,也就是CollectedHeap::obj_allocate函数; 这个函数做了一些校验,然后调用common_mem_allocate_init去申请内存,下面看看common_mem_allocate_init这个函数的实现: 这个函数分两步,首先使用common_mem_allocate_noinit来申请内存,然后使用init_obj初始化这块内存;,依然只关系内存申请相关函数common_mem_allocate_noinit: 申请分两组情况,如果使用TLAB(Thread-Local Allocation Buffer),那么就使用allocate_from_tlab来分配内存,否则使用Universe::heap()->mem_allocate来分配内存;先来看看从TLAB分配内存的情况: 依然还是分两种情况,首先通过thread->tlab().allocate来分配内存,如果无法满足要求,那么就通过allocate_from_tlab_slow来进行内存分配,还是先来看thread->tlab().allocate; 这个函数还是比较简单明了的,top指针指向空闲内存开始处,判断tlab里面剩下的内存是否可以满足要求,如果可以,那么就分配size大小的空间,并且移动空闲指针到合适的地方;否则就代表无法成功在TLAB上分配到足够的内存; 如果thread->tlab().allocate分配失败,那么allocate_from_tlab_slow就要开始工作了,首先,如果JVM认为TLAB空闲的内存足够大,那么就不能抛弃这部分空闲的内存,那就得去堆中去分配了; 接着,就说明TLAB里面已经没有空闲的空间了,或者TLAB里面空闲的空间可以忍受浪费,那么就新申请一块TLAB,首先需要计算新的TLAB的大小,thread->tlab().compute_size将承担这个工作: 首先将申请的对象大小规整为aligned_obj_size,然后计算出目前可申请的空间大小available_size,这个大小的值可能是新生代中Eden的空闲空间;new_tlab_size是最终确定的申请的TLAB的大小;接着判断是否满足要求,如果new_tlab_size的大小还不足以满足申请的对象实例,那么就放弃神奇这次TLAB; 这个函数较为复杂,下面按几个关键步骤来分析一下该函数的实现;
should_allocate函数在DefNew里面的实现如下: 如果申请的内存在可控的范围之内,那么就可以在该DefNew里面进行分配,否则就不行;如果判断可以在Young里面分配内存,那么就通过young->par_allocate函数来执行内存分配的工作: eden()->par_allocate是关键,最后将由par_allocate_impl来实现具体的内存分配工作: 这个函数还是比较容易理解的,通过CAS技术来循环尝试分配内存,top指向空闲内存的起始地址,尝试分配内存就是将top指针向前移动size长度即可,当然,如果申请的内存大小大于Eden的空闲内存,那么直接就会返回NULL以代表内存分配失败; 如果无法从Young区域成功申请到内存,那么就要使用attempt_allocation来从其他的分代尝试获取足够的内存了; 依然是用各个分代的should_allocate来判断是否可以在某个分代进行内存分配,首先尝试在Young区域进行分配,然后在尝试从Old区域分配内存,从Young区分配内存的过程已经在上面分析过,就不再赘述了;下面来分析如何判断是否可以在old区域进行内存分配,以及具体是如何进行内存分配的; 老年代是不支持TLAB分配的,只有DefNew是支持的,supports_tlab_allocation函数用于判断某个分代是否支持TLAB分配,除了DefNew,其他分代都是不支持的,当然,如果不是TLAB分配请求,那么如果申请分配的内存大于0并且小于最大极限,那么就支持在该分代内申请内存,否则就是不支持的。 接着看看具体如何在老年代进行内存分配(对于Serial Old); 接着去看ContiguousSpace的allocate_impl: 依然是一段比较清晰简单的代码,top依然是空闲内存的起始地址,申请一段内存就是将top向前移动一段距离; 堆扩展的顺序是老年代到新生代,内存分配的顺序是从新生代到老年代,这个细节需要注意一下!expand_and_allocate函数永远做堆分代扩展及内存分配的具体工作,首先看Serial Old的expand_and_allocate是如何实现的: parallel代表是否是多线程版本的GC,Serial Old是单线程的,所以看else分支即可;expand函数实现堆扩展,allocate函数用于从堆中申请内存,先看看expand函数的实现;CardGeneration::expand是最终指向expand的实际函数: 通过不断尝试缩小扩展的大小来进行堆扩展,grow_by函数用于实际执行扩展工作,暂时就不深入了; eden()->par_allocate试图从Eden空闲区域中去申请内存,上文已经分析过这个函数的实现细节,不再赘述;如果从Eden区域分配失败,那么就尝试在From区域进行分配,allocate_from_space函数用于执行这个工作; 是否需要从From区域进行内存分配需要做一些判断,should_allocate_from_space用于判断是否应该在From区域进行内存分配,当然,如果已经有线程触发了GC,那么也是可以从From区域进行内存分配的;下面先来看看should_allocate_from_space的判断标准; 如果在做一次FullGC,并且collection_attempt_is_safe返回了false,并且Eden不是空的,那么就可以在From分配内存,collection_attempt_is_safe是做什么的? 如果to区域不为空,那么说明发生了"Promotion fail",这种情况下是false,以及_old_gen->promotion_attempt_is_safe也是false; 如果老年代连续的可用内存空间大于新生代的对象大小或者大于新生代历史平均晋升大小,那么就是true,否则就是false; 回到GenCollectorPolicy::mem_allocate_work函数中来,如果尝试扩展堆之后还是无法申请到内存,那么就只能触发一次VM_GenCollectForAllocation类型的GC Operation了,完成之后再尝试申请内存; 现在回头看看CollectedHeap::common_mem_allocate_noinit,如果TLAB这个分支无法完成内存申请工作,那么就要交给Universe::heap()->mem_allocate来执行内存分配的工作;下面来分析一下Universe::heap()->mem_allocate的流程,这个流程分析完了整个对象实例分配的流程也就分析完了: 上面已经分析过mem_allocate_work这个函数的具体实现,和TLAB分支唯一的区别就是is_tlab是false,所以接下来的分析就不进行了; JVM对象内存申请流程总结如下:
|
OwnerAuthor
pandening commented on 12 Nov 2018 •
edited
本comment希望能系统的探索一下GC发生的时机,以及各个GC的具体工作内容(流程),GC包括Minor GC和Major GC,下面将分别看看Minor GC和Major GC会在什么时候执行、怎么执行的,也就是希望能了解触发GC的条件和GC原理。 其中VM_CollectForAllocation表示内存申请失败,它有三个子类,分别是VM_GenCollectForAllocation、VM_ParallelGCFailedAllocation、VM_G1OperationWithAllocRequest;带Full字符的Operation代表是一次FullGC,有VM_GenCollectFull、VM_G1CollectFull;VM_ParallelGCSystemGC虽然不带Full,但是也是FullGC操作;下面来看看触发这些VM_GC_Operation的时机到底是什么时候。 VM_GenCollectForAllocation 可以在collectorPolicy.cpp的mem_allocate_work函数里面发现除了了一个VM_GenCollectForAllocation;mem_allocate_work函数用于申请内存空间,前面的文章也分析过这个函数,简单来说,这个函数将首先在YoungGen里面申请内存,如果无法得到满足,那么就去OldGen试试,如果OldGen也不可以满足话,那么就去尝试扩展堆之后再试试,如果还是不行,那就只能触发一个VM_GenCollectForAllocation了; VMThread::execute函数会将这个VM_GenCollectForAllocation放到VMThread的任务队列里面去,VMThread就会执行这个VM_GenCollectForAllocation的doit函数,下面来看看VM_GenCollectForAllocation的doit函数的具体实现: satisfy_failed_allocation函数前面的文章也已经说过了,再总结一下这个函数的具体工作;
总结一下,VM_GenCollectForAllocation会在内存申请失败的时候进行工作,它可能触发Minor GC和FullGC,首先是Minor GC,如果Minor GC并不奏效,那么就要进行FullGC; VM_ParallelGCFailedAllocation VM_GenCollectForAllocation工作在DefNew,是SerialGC的年轻代;VM_ParallelGCFailedAllocation工作在ParallelScavengeHeap,ParallelScavengeHeap是UseParallelGC和UseParallelOldGC的年轻代,属于"吞吐量"GC,该类型的GC注重的是系统的吞吐量,和CMS注重"响应时间"不同,"吞吐量"类型GC可以设定用于GC的时间,JVM会自动调整堆来满足要求; mem_allocate函数先从YounYoungGen申请内存,如果无法得到满足,那么就去OldGen去申请内存;如果还是无法满足要求,那么就触发一个 ParallelScavengeHeap::failed_mem_allocate函数将会处理接下来的工作,下面来分析一下ParallelScavengeHeap::failed_mem_allocate这个函数的具体实现细节; 这个函数分下面几个步骤来处理Allocation Fail;
来看看PSScavenge::invoke()的具体实现细节; PSScavenge::invoke_no_policy()首先将被调用进行一次MinorGC,在MinorGC的过程中可能有一些对象达到了晋升阈值,但是可能老年代因为空间不够的问题无法将所有晋升的对象都放到老年代,这个时候就发生了Promotion Fail;因为Scavenge GC的一个特点是可以自动调整各个分代的大小以满足设定的参数,这个过程较为复杂,可以在PSScavenge::invoke_no_policy()里面找到这些代码;Minor GC的过程大概和DefNew是一样的,但是和DefNew不一样的地方就是ParallelScavengeHeap使用了多线程来做GC,所以代码要复杂很多,但是流程还是那样,首先标记GCRoot,然后根据GCRoot去遍历存活对象,之后标记-清除; 判断条件很简单,如果发现YoungGen里面等待晋升到OldGen的对象大小大于oldGen的空闲空间,那么就有必要执行FullGC了;接着看进行FullGC的代码,UseParallelOldGC用于判断老年代使用的堆类型,如果我们在JVM启动的时候使用了-XX:+UseParallelOldGC,那么新生代和老年代的组合就是(Parallel Scavenge + Parallel Old),如果使用的是-XX:+UseParallelGC,那么新生代和老年代的组合就是(Parallel Scavenge + Serial Old);这里假设使用了-XX:+UseParallelGC,那么就看PSMarkSweep::invoke_no_policy(clear_all_softrefs);而Serial Old的GC过程前面的文章已经分析过就不继续了。 现在回到ParallelScavengeHeap::failed_mem_allocate函数,看看剩下的部分;PSScavenge::invoke()执行过后,可能进行了一次MinorGC,或者是FullGC,可能将soft reference清理掉了,但是总得来说执行了PSScavenge::invoke()之后已经清理了一波垃圾了,young_gen()->allocate(size)试图从新生代申请空间;如果申请失败,那么就看刚才是否做了FullGC,如果做了,那么就只能oom了,否则通过do_full_collection(false)做一次FullGC,但是soft reference依然还在;接着分别从young 和 old去申请空间,如果还是无法满足要求,那么就通过do_full_collection(true)来做一次清理FullGC,并且将soft reference清理掉,然后再从young 和 old中去试图申请内存,如果还是无法申请成功,那么就交给上层处理吧。(OOM) VM_G1OperationWithAllocRequest VM_G1OperationWithAllocRequest有两个子类:VM_G1CollectForAllocation和VM_G1IncCollectionPause,属于G1的内容,暂时不做分析,后续专门分析G1的相关实现细节; VM_GenCollectFull VM_GenCollectFull用于支持一些外部的GC命令,比如System.gc(),可以在GenCollectedHeap::collect_locked函数里面发现VM_GenCollectFull操作: genCollectedHeap::collect函数是该操作发生的一个起点,而genCollectedHeap::collect是为了响应类似于System.gc()调用,比如: 这就是一个System.gc()的请求,而调用的就是Universe::heap()->collect函数,Universe::heap()返回的是JVM的一个高层堆管理器,目前JVM里面有三个这样的堆管理器,分别是GenCollectedHeap、ParallelScavengeHeap和G1CollectedHeap,分别对应不同种类型的GC;GenCollectedHeap对应-XX:+UseSerialGC和-XX:+UseConcMarkSweepGC;ParallelScavengeHeap对应-XX:+UseParallelGC和-XX:+UseParallelOldGC以及-XX:+UseParNewGC;G1CollectedHeap对应-XX:+UseG1GC;这些对应关系是在create_vm的时候创建的,关于堆初始化这部分内容将在后续的文章中分析。 结论 Minor GC发生的原因较为简单,就是"Allocation Fail";发生"Allocation Fail"的原因就是没有足够的内存了,这个时候就要去做Minor GC,但是,内存不足之后不一定进行Minor GC,可能因为某些原因直接进行了FullGC,在JVM里面有大量的用于判断是否应该在某个分代进行垃圾收集的函数,这些函数将根据一些统计数据来判断是否应该在该区域进行垃圾收集;比如在某次Eden区域分配失败的时候,Old区域就需要判断是否允许Young区进行一次Minor GC,因为进行MinorGC的时候一些符合晋升年龄的对象将会晋升到老年代中来,还有一部分对象因为无法移动到To区域(To区满了或者连续空间小于存活对象大小)也需要提前拷贝到老年代,这些对象转移到老年代对老年代来说是一种负担,并且也是有风险的,比如可能老年代根本没有足够的内存容纳这次Minor GC之后晋升的对象,这个时候MinorGC就要报"Promotion Fail",这就需要开启一次FullGC来回收掉一些不再使用的对象,也可能包括正在使用的soft reference;还有一些发生FullGC的条件(或者说是触发FullGC)本文没有分析到,主要原因是关于G1和CMS还不太了解,CMS和G1是相对复杂的GC,需要花费大量的时间去研究分析以及描述出来。 发生GC有两种原因,主动进行GC和被动进行GC,被动GC就是类似于System.gc(),主动GC发生在allocate的时候,如果可以,应该尽量避免让GC被动GC,因为这会打乱JVM的GC计划,应该相信JVM可以做得足够好,让我们不需要担心GC的问题,这也是Java相比于类似于C/C++的主要优势之一。 |
OwnerAuthor
pandening commented on 13 Nov 2018 •
edited
JVM参数解析以及Heap初始化过程分析在create_vm的时候,我们设置的JVM参数会被解析出来,然后生成各种策略,比如设置了 -XX:+UseSerialGC,那么JVM就会适应Serial GC来作为堆的管理者,当然,也就会初始化新生代和老年代,不同的参数设置会生成不同的GC策略,JVM参数众多,不同参数之间有可能互相影响,有些参数可能导致非常诡异的现象,所以在设置JVM参数的时候,如果对一个参数并不是很了解,不要轻易设置。本文将从JVM参数解析开始说起,然后会分析一下堆的初始化,分析堆的初始化的过程也就是去分析JVM是如何使用我们设置的JVM参数的过程。 JVM参数解析Arguments::parse(args)函数是JVM参数解析的入口,在thread.cpp里面的create_vm函数里面可以找到这个函数调用,因为JVM可配置的参数特别多,所以本文不打算将所有的JVM参数都讲一下,下面的文章将只是介绍一下JVM解析参数的流程,会拿几个参数来具体分析其解析的流程;parse_vm_init_args是我比较关注的函数,类似于-XX:+UseSerialGC这样的参数将在从这里开始进行解析,当然,具体的解析是在parse_each_vm_init_arg里面完成的,所以直接来关注parse_each_vm_init_arg函数;下面来看看JVM参数-Xms,-Xmx以及类似于-XX:+UseConcMarkSweepGC这样的参数是怎么解析的。 -Xms用于设置堆的最小容量,-Xmx(或者-XX:MaxHeapSize)用于设置堆的最大容量,如果-Xms设置的大小和-Xmx一样大,那么堆就是不可扩展的,否则堆就是可以动态扩展的;上面的代码片段就是用来解析-Xms和-Xmx两个参数的,解析好的参数会设定到相应的全局共享变量中去,比如-Xms就会被设置到InitialHeapSize中去,-Xmx会设置到MaxHeapSize中去;这两个是数值型的参数,下面来看一个flag类型的参数设置,比如我们使用-XX:+UseConcMarkSweepGC,那么这个参数是怎么被JVM识别出来的呢?下面来分析一下。 还是在同样的函数里面解析,上面的代码片段是解析类似-XX:+UseConcMarkSweepGC参数的入口,parse_argument负责具体的解析工作,下面来看看parse_argument函数是怎么实现解析这样的参数并且设置到全局变量中去的。parse_argument函数可以在process_argument中找到; 图中标注的就是解析的关键,+%或者-%用于匹配-XX:+UseSerialGC中的+,下面可以看一个实际的启动时参数解析例子: set_bool_flag会将解析到的flag对应的全局变量设置为true,可以具体看看set_bool_flag函数是如何做到这一点的。 find_flag将找到对应的flag信息,可以在下面的debug界面中看到找到了我们设置的flag,找到flag之后会调用boolAtPut函数来设置全局变量: 在设置了JVM参数之后,我们也不知道参数这样设置是否存在问题,或者是否有冲突,但是JVM必须能够发现这种冲突,并且及时给出提示,check_vm_args_consistency函数将完成JVM参数设置校验的工作,比如校验GC设置是否合理是通过调用check_gc_consistency函数来完成的: JVM参数的解析部分析就到这里,但是还是得说一下,为什么有时候我们什么参数也不配置,JVM也能运行起来呢?Arguments::apply_ergo()就是做这个工作的,它会进行一些自动的配置来启动JVM,比如选择GC等,select_gc就是做这件事情的: gc_selected首先判断是否设置了GC,判断条件很简单: 如果没有设置,select_gc_ergonomically将选择一个合适的GC,在java9里面的实现如下: 选择的策略和当前JVM的Mode有关,如果是client模式,则默认选择SerialGC,这也是Client模式下的最优的GC;如果是在Server模式下,那么如果没有设置UseAutoGCSelectPolicy的话,就默认使用G1(所以说java9默认的GC是G1),如果设置了UseAutoGCSelectPolicy,那么根据should_auto_select_low_pause_collector的结果来选择; 如果should_auto_select_low_pause_collector返回true,那么就选择CMS,否则使用UseParallelGC;前者是相应时间优先GC,后者则是吞吐量优先GC。 JVM堆的初始化JVM参数解析之后,在初始化JVM堆的时候就可以使用我们设置的JVM参数了,不同的参数使用的堆是不一样的,GC策略也是有所差异的,下面来分析一下堆的初始化过程;initialize_heap函数用于初始化堆,下面简单分几个步骤分析一下这个函数具体做了些什么工作。
首先要做的事情就是要创建使用的堆,创建哪种类型的堆和设置的GC参数有关,create_heap函数将完成创建堆的工作; 创建什么类型的堆依赖于选择了什么类型的GC,JVM提供了四种类型的GC,分别是并行GC(UseParallelGC),也就是使用多线程来做GC,G1 (UseG1GC),CMS以及串行GC(UseSerialGC);Universe::create_heap_with_policy函数用于创建对应的堆,它的两个泛型类型,一个是堆的类型Heap,一个是管理堆的策略Policy,比如对于UseSerialGC,那么创建的堆就是GenCollectedHeap,堆管理的策略就是MarkSweepPolicy;在HotSpot中,堆的实现是一种典型的分代实现,简单来说分为新生代和老年代,不同的分代存放的对象具有不一样的特征,但是不同特征的对象也可能放在一起,分在不同分代中的特征包括对象的GC年龄以及对象的大小等因素,对象将优先在Eden中存活,经过多次Minor GC依然存活的对象将晋升(Promotion)到老年代,但是晋升可能失败,所以有部分本该晋升到老年代的对象依然存活在新生代,而在做Minor GC的时候,如果Eden + From中存活的对象无法拷贝到To区域,那么也会直接转移到老年代,这称为提前晋升,还有一些比较大的对象会直接在老年代申请空间;下面的文章将以UseSerialGC为例,看看堆创建的后续流程。 先来看一下create_heap_with_policy函数的实现: 对于UseSerialGC来说,policy就是MarkSweepPolicy,Heap就是GenCollectedHeap;下面分别看看策略的初始化和堆的初始化。 Policy初始化 initialize_all函数应该是我们应该主要关心的,这个函数在基类GenCollectorPolicy中实现: CollectorPolicy::initialize_all()函数的实现在CollectorPolicy里面,实现如下: initialize_alignments会根据os的page大小来设置空间对齐参数,稍后会根据这些对齐参数来将我们设置的各种堆大小对齐到合理的值,所以JVM里面的实际堆大小并不会精确的等于我们设置的大小,而是会做对齐操作; _min_heap_byte_size表示堆的最小值,align_size_up函数用于对齐堆的大小;aligned_initial_heap_size是对齐之后的堆初始化大小,如果和InitialHeapSize大小不一样,就要重新设置一下InitialHeapSize;MaxHeapSize也是同样的处理方法;initialize_size_info函数相对来说比较复杂,它的工作就是确定新生代和老年代的堆大小,比如新生代的初始化堆大小,以及最大堆大小等信息,下面看看细节: 这段代码要确定_max_young_size的大小,也就是新生代的大小,如果我们使用-Xmn设置了新生代的大小,那么就不用执行这段代码,否则就要通过scale_by_NewRatio_aligned函数来确定新生代的大小,scale_by_NewRatio_aligned的实现如下: 我们可以使用-XX:NewRatio来设置新生代的占用整个堆的比例,NewRatio默认为2,也就是young_gen_size = heap_size / (NewRatio + 1);接着看下面的代码: 如果堆不可扩展,也就是-Xms和-Xmx是相等的,那么就会执行这段代码,_max_young_size会根据是否设定了NewSize来确定,如果设定了那就取设定的NewSize(-Xmn),接着_initial_young_size会被设定了_max_young_size,也就是新生代不可扩展了;这里稍微说一下,DefNew不会进行堆扩展,如果Eden无法满足申请空间的要求的时候,他就会尝试去From去申请内存;如果堆可扩展,那么就会执行下面的代码: 至此,新生代_min_young_size、_initial_young_size、_max_young_size都已经确定了,下面就是确定老年代的这三个变量;这部分内容就不再赘述了,后续再专门研究吧。 可以看到新生代是DefNew,老年代是MarkSweepCompact,上面计算好的新生代老年代的堆大小也被设置到GenerationSpec对象中了,后续会使用这些参数来创建具体的堆以及初始化堆空间。 Heap初始化 下面以GenCollectedHeap为例看看堆是如何初始化的;在initialize_heap中调用create_heap之后,就会调用创建好的堆的initialize函数来初始化堆,对应着看GenCollectedHeap的initialize函数; 主要看标记出来的两行代码,分别初始化了新生代和老年代,gen_policy()->young_gen_spec()函数将返回上面设定的GenerationSpec,然后init函数将根据具体的堆类型进行创建新生代和老年代并且初始化; 比如对于+UseSerialGC,新生代就是DefNew,老年代就是MarkSweepCompact;下面看看新生代是如何进行初始化的,DefNewGeneration::DefNewGeneration这个构造函数将用来创建一个DefNew,下图展示了几个关键的地方: |
OwnerAuthor
pandening commented on 14 Nov 2018 •
edited
UseConcMarkSweepGC下的内存申请流程分析-XX:+UseConcMarkSweepGC俗称CMS,是一种减少GC停顿时间的堆管理方案,使用的堆管理器是GenCollectedHeap,新生代堆类型是ParNew,老年代是ConcurrentMarkSweepGeneration,新生代使用多线程版本的copy算法来进行垃圾收集,将新生代分为Eden + From + To三个空间区域;老年代使用CMS来进行周期性的垃圾收集,可以通过设置CMSInitiatingOccupancyFraction来让CMS检测是否需要进行一次CMS GC,CMSInitiatingOccupancyFraction的默认值为92%,也就是如果老年代的空间使用占了92%,那么就会进行一次CMS GC,这个默认值是计算出来的: 参数io是CMSInitiatingOccupancyFraction,trCMSTriggerRatio;是如果设置了CMSInitiatingOccupancyFraction,那么_initiating_occupancy就是(double)io / 100.0,否则通过else分支中的计算分支来计算,假设没有设置CMSTriggerRatio,默认就是80,MinHeapFreeRatio是40;那么计算结果就是0.92;CMS的GC分为background gc和foreground gc,前者是CMS线程进行不但检测是否需要进行CMS GC来实现垃圾回收的,属于后台任务;而后者是被"Allocation Fail"或者“Promotion Fail”触发的,是一种主动的GC,而主动GC是要全程STW的,在实现上使用了SerialOld的策略,使用标记-清除-整理算法来进行整个堆空间的垃圾回收;关于CMS GC的详细细节另论,本文的重点在于UseConcMarkSweepGC下的对象内存分配策略探索。 在UseConcMarkSweepGC下对象依然首先在Eden中进行内存申请,UseConcMarkSweepGC新生代使用的是ParNew,是DefNew的子类,ParNew上的GC是DefNew上GC的多线程版本,在ParNew上进行空间分配应该也和DefNew差不多,下面来看看UseConcMarkSweepGC下内存分配的全流程。 因为前面的文章已经分析过对象在UseSerialGC下的内存申请流程,所以对于CMS的内存申请直接从CollectedHeap::common_mem_allocate_noinit函数开始看起,在UseSerialGC的时候也说过该函数,这个函数首先allocate_from_tlab函数来试图从TLAB申请空间,如果无法满足,那么就重新申请一块TLAB,申请一块TLAB和为对象申请空间的流程对于堆来说都是内存申请,所以后续的流程是一致的;如果通过TLAB无法申请到内存,那么就通过Universe::heap()->mem_allocate来直接在堆中申请内存,这个时候就要加锁了,因为堆面向的是所有线程,不像TLAB是线程私有的,所以会存在多线程竞争的问题,所以但愿TLAB可以有效;GenCollectorPolicy::mem_allocate_work将完成再堆中内存申请的流程,下面就主要来分析一下这个函数的具体实现。 young->should_allocate用于判断是否应该在新生代进行空间申请,大对象应该直接在老年代进行分配,如果不是大对象,那么就会通过young->par_allocate来进行空间申请,young->par_allocate使用的是DefNew的实现,ParNew继承了DefNew的young->par_allocate实现; 可以看到是向Eden空间申请内存,具体实现时通过ContiguousSpace::par_allocate_impl来进行的,关于这块的内容前面的文章已经分析过,不再赘述,因为使用copying算法来进行垃圾回收,不会存在内存碎片问题,所以可以使用指针碰撞算法来进行空间分配,所谓指针碰撞就是使用一个top指针,来标记当前空闲内存的起始地址,分配一块size大小的内存空间的实现就是将top指针向前移动size即可实现;如果无法从Eden空间分配到内存,那么就要试图从From区域分配内存了,gch->attempt_allocation将实现先尝试从Eden区域申请内存,如果无法成功,那么尝试从From区域分配,如果还不可以,那么就从Old区域分配的逻辑,具体实现如下: _young_gen->allocate将会从From区域尝试申请内存: eden()->par_allocate将从Eden区域申请内存,如果无法满足,那么就通过allocate_from_space从From区域进行内存分配; 当然,需要判断是否允许在From区域进行内存分配,如果不允许,那么还是无法在From区域进行分配;should_allocate_from_space将完成这个判断,当然,如果当前有线程在进行GC,那么是运行从From区域进行内存分配的,下面看看should_allocate_from_space函数的具体判断逻辑: 很简单,直接返回_should_allocate_from_space的值,所以来看看在什么时候设置了该值即可找到判断逻辑: 判断条件还是比较严格的,首先collection_attempt_is_safe是true,并且Eden已经满了,collection_attempt_is_safe函数的实现如下: 如果To区域不为空,那么就直接不可以在From区域进行分配,To区域不为空就说明发生了“Promotion Fail”,如果没有发生过“Promotion Fail”,那么判断晋升是否是安全的,通过_old_gen->promotion_attempt_is_safe函数来实现: available是老年代可用内存大小,av_promo是新生代评价晋升对象大小,max_promotion_in_bytes是新生代的使用量(Eden + From),所以,如果老年代的可用空间大于新生代评价晋升对象大小,或者大于新生代的使用量,那么就说明年轻代晋升是安全的,否则就是不安全的; 总结一下,如果当前有线程在进行GC,或者Eden区域已经满了,或者老年代判断晋升是安全的,那么就运行在From区域进行分配,否则只能到老年代去分配了; 如果上述函数判断是true,那么就通过_old_gen->allocate来从老年代申请内存: CompactibleFreeListSpace将会负责CMS老年代的内存分配工作,这里需要说一下的是,CMS老年代和DefNew或者ParNew都不一样,CMS老年代堆可能会产生内存碎片,所以无法使用指针碰撞算法来进行内存分配,CMS老年代使用了称为空闲列表(Free-List)的算法来管理老年代的内存,下面来看看CompactibleFreeListSpace的allocate函数的实现: allocate_adaptive_freelists函数将尽最大努力来找到一块合适的内存,这里面的流程也是非常复杂的,但是这里的实现像极了C++ STL中内存池的实现,所以如果有条件的话还是希望去分析一下C++ STL内存池的相关实现。下面来看看allocate_adaptive_freelists函数的具体实现。 这里顺便说一下,如果从Old区域中也无法满足申请要求,那么就得去通过expand_heap_and_allocate扩展堆再来allocate了,如果还不行,那么就执行进行GC了,VM_GenCollectForAllocation将会被放在VMThread中等待执行,具体执行Minor GC还是FullGC需要具体判断,这部分内容在前面的文章中分析过,就不再赘述,下面将详细分析CMS Free-List内存分配的实现细节,也就是allocate_adaptive_freelists函数的具体实现细节。 CMS使用的Free-List分配算法策略复杂,当然复杂带来的好处是高效的内存分配速率;这一块内容日后再来整理。 |
OwnerAuthor
pandening commented on 20 Nov 2018 •
edited
UseConcMarkSweepGC下的GC流程分析相比于SerialGC,CMS要复杂得多,因为他是第一个GC线程可以和用户线程并发执行的GC,GC线程和用户线程并发执行这件事情是非常困难的,也是极其复杂的,因为垃圾收集的同时,用户线程还在不断的产生垃圾,或者改变引用关系,使得已经被GC线程标记为垃圾的对象活起来了,这些情况都需要CMS能够很好的去解决; CMS GC分为foreground gc和background gc,foreground gc是一种主动式GC,是Minor GC造成的一种FullGC,foreground gc将和Serial old使用同样的垃圾收集算法来做FullGC(单线程,mark-sweep-compact);如果触发了foreground gc,但是发现此时background gc正在工作,那么就会发生"Concurrent model fail";background gc也就是CMS old GC,只会收集老年代(ConcurrentMarkSweepGeneration),是一种周期性被动GC,ConcurrentMarkSweepThread会周期性的检测是否需要触发一次background gc,判断条件一般是老年代空间使用超过了设置的触发CMS old GC的阈值,默认为92%,可以通过CMSInitiatingOccupancyFraction来设置具体的值,建议开启-XX:+UseCMSInitiatingOccupancyOnly,否则CMS会根据收集到的数据进行判断,这样可能情况就变得更加复杂了。 UseConcMarkSweepGC依然使用GenCollectedHeap作为堆管理器,所以GC策略还是和Serial GC一样,这里就不再赘述,本文剩下的内容主要分析CMS Old GC的实现细节,以及background gc和foreground gc之间是如何相互配合来回收垃圾的。CMS过程复杂,下面是CMS Old GC可能经过的状态枚举: Idling状态是初始状态,也代表background gc目前不在进行垃圾收集,此时进行foreground gc是不会发生 "Concurrent mode fail"的,简单说,CMS Old GC需要经过初始标记(STW)、并发标记、最终标记(STW)、清理垃圾这么几个关键的步骤,看起来CMS Old GC的过程中一直在做标记的工作,这主要是CMS希望能尽量缩短暂停用户线程的时候,所以有些阶段就直接和用户线程并发运行了,这就导致会产生“浮动垃圾”,使得CMS整体实现非常复杂难懂,下面按照一些关键步骤尝试分析每一步所做的事情,以及每一步存在的意义以及可能存在的一些运行时表现。 CMSCollector::collect_in_background函数完成的工作就是background gc的工作,foreground gc的工作由CMSCollector::collect函数完成,下面的分析的入口均从这连个函数进入。 InitialMarking (初始标记) 初始标记是一个STW的过程,当CMS 发现当前状态_collectorState为InitialMarking的时候就会执行初始化标记的工作,下面是InitialMarking工作的入口代码: VM_CMS_Initial_Mark的doit函数将被VMThread调度执行,下面来看看VM_CMS_Initial_Mark的doit函数的具体工作内容。 _collector->do_CMS_operation将被执行,看参数中CMSCollector::CMS_op_checkpointRootsInitial可知接下来会进行初始化标记的过程,CMSCollector::do_CMS_operation函数内容如下: 这个函数在FinalMarking阶段也会被调用,对应的Operation就是CMS_op_checkpointRootsFinal,无论是CMS_op_checkpointRootsFinal还是CMS_op_checkpointRootsInitial都是STW的,现在来看看CMS_op_checkpointRootsInitial对应的流程;checkpointRootsInitial函数将被调用: checkpointRootsInitialWork是需要重点关注的函数调用;CMSParallelInitialMarkEnabled默认是true的,所以将会执行下面这段代码: CMSParInitialMarkTask就是具体的任务,CMSParInitialMarkTask::work将完成具体的InitialMarking工作,下面是CMSParInitialMarkTask::work的具体细节,从图中的代码片段可以看出来InitialMarking需要完成的工作是哪些: InitialMarking阶段将以GCRoot和新生代对象为Root扫描老年代,来标记出老年代存活的对象;在具体实现上,CMS使用称为“三色标记”的算法来进行存活对象标记,白色代表没有被标记,灰色代表自身被标记,但是引用的对象还没有被标记,黑色代表自身被标记,并且引用的对象也已经标记物完成,具体的算法实现非常复杂,本文就不继续分析研究了。 Marking (并发标记) 该阶段称为并发标记,这里的并发,指的是用户线程和GC线程并发执行,介于这种并发执行的情况,可能在GC线程标记的过程中存在新生代对象晋升的情况,或者根据内存分配策略大对象直接在老年代分配空间,以及Minor GC的时候存活对象无法转移到To Survivor中去而提前晋升转移到老年代中来,或者更为复杂的是对象引用关系发生变化,这些对象都需要被重新标记,否则就会错误的以为这部分对象不可达而被清理,造成严重的运行时错误。 markFromRoots函数将负责并发标记阶段的全部工作,下面来分析一下这个阶段的主要流程; markFromRoots函数中的markFromRootsWork函数调用将完成主要的工作,然后判断该阶段的任务是否成功执行,如果是的话,那么就转移状态到Precleaning,接着GCThread就会进行下一阶段Precleaning的工作;下面来看看markFromRootsWork函数实现的细节: 如果设置了CMSConcurrentMTEnabled,并且ConcGCThreads数量大于0,那么就会执行do_marking_mt,也就是多线程版本,否则就会执行do_marking_st,也就是单线程版本;为了分析简单,下面只分析单线程版本的内容: markFromRootsClosure是一个闭包函数对象,它里面的do_bit函数将会被BitMap::iterate来调用,调用关系可以在CMSCollector::do_marking_st函数中看到,先开看看BitMap::iterate的实现: 可以看到不断的调用了BitMapClosure的do_bit函数,这里的BitMapClosure就是MarkFromRootsClosure;下面来看看do_bit的具体实现: 主要关系MarkFromRootsClosure::scanOopsInOop函数: 看到oop_iterate,就是进行对象标记工作了,当然,具体的工作还是由PushOrMarkClosure的闭包函数do_oop完成的,下面来看看实现细节: 可以看到,do_oop函数会将对象标记,并且将对象push到_markStack中去,然后在MarkFromRootsClosure::scanOopsInOop的while循环中将从_markStack中pop出一个obj继续遍历标记,整个过程是类似于递归完成的;所以并发标记阶段完成的工作就是根据初始化标记阶段标记出来的对象为Root,递归标记这些root可达的引用,只是在标记的过程中用户线程也是并发执行的,所以情况就会比较复杂,这也是为什么CMS需要有多次标记动作的原因,如果不执行多次标记,那么就可能会将一些存活的对象漏标记了,那么清理的时候就会误清理。 Precleaning (预清理) 通过Marking之后,_collectorState就会被更新为Precleaning,该阶段的入口如下: preclean函数就完成Precleaning阶段的工作; CMSPrecleaningEnabled用于控制是否进行Precleaning阶段,CMSPrecleaningEnabled默认是true的,也就是默认会进行CMSPrecleaningEnabled,除非特殊情况,应该使用默认配置;preclean_work函数用于完成Precleaning的具体工作,Precleaning阶段需要完成的工作包括:
AbortablePreclean AbortablePreclean其实是一个为了达到CMS的终极目标(缩短STW时间)而存在的,AbortablePreclean阶段要做的工作和Precleaning相似,并且是一个循环的过程,但是是有条件的,达到某些条件之后就会跳出循环,执行STW的Final Mark阶段,AbortablePreclean阶段(包括Precleaning阶段)所要做的事情就是尽最大努力减少Final Mark需要标记的对象,这样STW的时间就减下来了。 abortable_preclean函数将负责完成AbortablePreclean阶段的工作; CMSScheduleRemarkEdenSizeThreshold默认值为2M,只有当Eden区域的使用量大于该值的时候才会进行接下来的工作;接下来看到的while循环里面做的工作和Precleaning是一样的,因为和Precleaning阶段一样使用了preclean_work函数来完成具体的工作;这个while循环执行下去的条件值得分析一下;
_foregroundGCIsActive代表正在进行Serial Old GC,incremental_collection_will_fail代表已经发生了"Promotion Fail",那么就不用进行“递增式GC了”,也就是JVM建议直接进行FullGC,这些情况下should_abort_preclean都会返回true;
FinalMarking (最终标记) FinalMarking属于ReMark,需要STW,下面来分析一下这个阶段需要完成的工作;首先大概猜测一下会进行哪些工作;首先,ReMark阶段需要将最终要清理掉的对象标记出来,也就是这个阶段完成之后,被标记为"垃圾"的对象将会在稍后的阶段回收内存,初始标记阶段完成了从GCRoot和新生代可达的老年代对象,两个preclean阶段是一种修正手段,将那些在GC线程和用户线程并发执行时发生的变化记录起来,并且因为FinalMark阶段是STW的去扫描整个新生代来发现那些可达的老年代对象的,所以,新生代存活的对象如果很多的话,需要扫描的对象就很多,整个社会STW的时间就会上升,所以AbortablePreclean阶段将尽力使得新生代发生一次YGC,这样FinalMark时需要扫描的新生代对象就变少了。因为并发标记阶段GC线程和用户线程并发运行,所以可能会发生下列情况:
这些情况FinalMark阶段需要全部考虑到,下面具体来看看该阶段完成的工作; VM_CMS_Final_Remark类型的任务将被添加到VMThread里面执行,所以直接来看VM_CMS_Final_Remark的doit函数实现就可以知道具体的工作内容了; 和初始化标记一样使用了do_CMS_operation函数,但是执行类型变为了CMSCollector::CMS_op_checkpointRootsFinal,下面看看do_CMS_operation内部执行CMSCollector::CMS_op_checkpointRootsFinal的那部分代码; 如果设置了CMSScavengeBeforeRemark,那么就在执行FinalMark之前执行一次YGC,具体原因前面说过,因为FinalMark阶段是STW的,如果新生代存活对象很多的话,就需要扫描很多对象,这个STW时间就上来了,所以提前进行一次YGC,那么就可以让新生代中废弃的对象回收掉,使得FinalMark阶段扫描的对象减少;CMSScavengeBeforeRemark默认是false的,这个参数还是建议不要轻易设置,因为有preclean阶段的存在,可能在preclean阶段已经发生了一次YGC,如果再进行一次YGC,是没有必要的,所以让CMS自己去按照自己的节奏去工作,除非特别不否和预期的时候才去干涉他的执行。 Sweeping (清除) 就像名字一样,该阶段就是进行垃圾对象清理的,这个阶段是并发的,整个CMS周期性GC过程中,除了initMark和FinalMark之外,其他阶段都是可以并发的;sweep函数将完成清理的工作,在sweep函数内部调用了一个关键的函数sweepWork,下面是sweepWork的具体实现: CMS只会回收CMSGen,也就是老年代,这里需要重新说明一下;除了ConcMarkSweepGC外,其他GC类型的OldGC都可以说是FullGC(G1暂未了解),具体的sweep算法就不继续分析了。 foreground gc上面说到的属于CMS周期性GC,也就是background gc,是一种被动的GC,通过监控老年代空间使用率来启动GC,foreground gc属于主动gc,发生foreground gc一般来说就是年轻代发生了Minor GC,并且发生了"Promotion fail",老年代空间不足等原因,具体原因和GenCollectedHeap堆的GC策略相关,这一点可以看前面的分析文章;下面来简单分析一下foreground gc的一些情况; 发生foreground gc的入口是ConcurrentMarkSweepGeneration::collect; acquire_control_and_collect函数将完成foreground gc的工作,看函数名字就可以猜测它要干嘛,首先要acquire control,也就是获取到堆的控制权,因为在触发foreground gc的时候,background gc可能正在工作,因为不可能同时两中gc同时运行,而foreground gc的优先级明显高于background gc,所以需要让background gc放弃gc,然后foreground gc来完成收集老年代垃圾的工作,当然,foreground gc顺带会回收新生代,所以是一次FullGC,下面具体看看acquire_control_and_collect函数的流程; 这一段会尝试等background gc主动把堆的控制权转移给foreground gc,在collect_in_background(background gc)中,开始之前会判断是否在进行foreground gc(_foregroundGCIsActive = true),如果在执行foreground gc,那么就会直接退出本次background gc;否则再每完成一个阶段之后都会尝试判断是否foreground gc在等待; waitForForegroundGC函数完成等待foreground gc 发生的工作: 如果此时进行(或者等待)foreground gc,那么就放弃此次background gc;否则告诉后续来到的foreground gc等待一下,等本阶段CMS GC完成会再次来判断的; 在foreground gc中,获取到了堆的控制权之后,就会执行下面的代码片段: 我们在观察CMS GC日志的时候,偶尔会看到“Concurrent mode interrupted”或者“Concurrent mode failure”这样的日志,就是因为在进行foreground gc的时候发现background gc已经在工作了;如果是类似于System.gc()这样的用户请求GC,那么就会打印“Concurrent mode interrupted”,否则就是“Concurrent mode failure”; 之后CMSCollector::do_compaction_work函数将做一次Mark-sweep-compact的工作,具体的工作在GenMarkSweep::invoke_at_safepoint函数中完成,这个函数在前面分析Serial Old的时候提到过,所以不再赘述; 总结整个CMS GC其实是非常复杂的,涉及用户线程和GC线程并发执行,以及foreground gc和background gc相互配合的过程,当然还涉及大量的参数,这些参数稍微不注意就会让JVM工作得不好,所以建议在不了解某个参数的具体表现的时候不要轻易使用; 其实CMS Old GC为什么分这么多步骤呢?主要原因是为了降低STW的时候,所以将mark和sweep两个阶段都设计成并发了,initMark和FinalMark会STW,但是initMark阶段所做的mark非常有限,GCRoot-> cms gen , YoungGen -> cms gen,而且因为两个preclan阶段和Dirty Card的存在,使得FinalMark阶段需要扫描的对象大大减小,如果在实际的运行过程中发现每次FinalMark过程都非常长,那么就设置参数在进行FinalMark之前进行一次YGC,使得FinalMark需要扫描的对象减少;CMS Old GC Mark 和 preclean阶段允许用户线程和GC线程并发执行,所以会存在:
解决这些问题就需要FinalMark的存在,FinalMark将扫描新生代,标记出yong gen -> old gen的部分,老年代内部的对象引用关系如果在并发阶段发生变化,会记录到DirtyCard中去,所以在FinalMark阶段扫描DirtyCard即可; 最后要说一下foreground gc和background gc,最好不要发生foreground gc,因为foreground gc会认为此时已经没有什么办法满足对象分配了,那么就要做一次彻底清理的工作,也就是FullGC,并且foreground gc是单线程运行的,并且是mark-sweep-compact的,所以速度可想而知,如果发现foreground gc发生的频繁,就要分析一下原因了,建议去研究GenCollectedHeap::do_collection,搞明白GC的策略,当然不同GC对应的堆是不一样的,Serial 和 CMS对应的是GenCollectedHeap,其他的就不是了,这个前面的文章说过。 |