赞
踩
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
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
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
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
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
edited
OwnerAuthor
edited
OwnerAuthor
edited
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。