简单理解耗时的Java GC

参考地址:

http://www.cloudera.com/blog/2011/02/avoiding-full-gcs-in-hbase-with-memstore-local-allocation-buffers-part-1/

java GC工作在分代的模型上。大多数的对象要么很快的死亡,要么持续较长的时间。例如,方法栈中创建的对象只会持续几毫秒时间,而缓存中的对象会持续几分钟。

既然对象有两种不同的什么周期,直觉认为,使用不同的垃圾收集算法也许能够在不同的周期中更好的完成工作。所以,JVM把堆空间拆封为两个不同的区域,新生代,旧生代。当对象刚开始创建的时候,会被分配到新生代。当对象在新生代中经过多次gc后,依然存活时,就假设这个对象将持续较长时间,就把这个对象复制到旧生代中去。

JVM加上启动参数-XX:+UseParNewGC 和 -XX:+UseConcMarkSweepGC将为新生代启用Parallel New collector为旧生代启用Concurrent Mark-Sweep collector。

Parallel New collector工作时,会首先停止所有运行中的java线程。然后追踪对象引用以判断哪些对象是活着的。最后把活着的对象复制到一个一块空闲的堆区域,更所所有的指针到新的地址。有两个重要地方值得注意

  1. 它会暂停所有线程,但时间非常短暂。因为新生代通常非常的小,而且是多线程收集的。它能够非常快的完成工作。生产环境中,通常建议新生代设置不超过512M,这样最坏的暂停时间也不会超过几百毫秒。
  2. 收集时,复制所有存活的对象到一块连续的空闲堆空间中去,这块空间叫做S1,S2,交替使用。如果S1或者S2空间不够使,剩下的存活对象会被复制到旧生代中区。

每次复制一个对象后,将增加这个对象的收集计数,当一个对象在新生代中被复制了一定次数后,该算法即判定该对象是长周期的对象,把他移动到旧生代。这个阈值叫着tenuring threshold。

旧生代的Concurrent Mark Sweep(CMS)

每次Parallel New collector工作时,会把一部分对象复制到旧生代中。所以,旧生代很快就会被填满,就需要一个策略来收集它。CMS负责处理旧生代中的死亡对象。

CMS通过多个步骤来完成工作。有一些步骤会暂停所有线程(stop the world),其他步骤是和应用并行的执行的。主要的步骤为:

  1. 初始标记(stop the world)。在这个步骤中,CMS放置一个标记在所有root对象中。一个root对象是被当前线程直接引用的对象,例如,线程当前使用的本地变量。这个步骤会很快完成,因为root对象是非常少的。
  2. 并行标记。收集器开始跟随并标记所有root对象的引用,直到标记完所有的引用,即存活的对象。
  3. 再次标记(stop the world)。在并行标记的过程中,有一些对象引用可以已经变化了,也有一些新的对象被创建。需要把这些变化考虑在内。这个步骤也非常的短暂,因为CMS使用一个特殊的数据结构,只处理这期间的修改
  4. 并行回收。所有没有被标记的对象,都将被删除,释放内存空间。在这个步骤中新创建的对象,会被直接标记,以免被删除。

有几个重要地方需要注意:

  1. stop the world的时间,是短暂的。所有扫描整个heap和删除的操作都是并行执行的。
  2. CMS收集器,不会移动压缩存活对象。所以空闲空间是分散在整个heap中的,会存在很多的碎片。

如上面描述,CMS收集器工作得很好,只暂停很短的时间,大多数耗时操作都是并行执行的。但运行中几分钟的长时间暂停是怎么产生的?是因为,CMS有两个特殊的失败情况需要处理。

第一种失败,Concurrent Mode Failure,经常讨论的一种失败。假如我们有个8GB的堆空间,其中已使用了7GB。CMS将开始它的第一步操作。在这个时候,更多的对象被创建并转移到旧生代中,如果转移速度太快,旧生代将在CMS标记完所有对象前被填满。因为没有更多的空间可供使用,应用程序将无法继续执行。CMS不得不暂停并行标记,并暂停所有的线程,使用单线程的复制算法,把所有堆空间的存活对象复制到堆的开始,然后回收内存空间,在这个长时间的暂停后,程序才能继续执行。这种情况看起来应该很容易避免,我们只需要让CMS在内存空间不够前完成工作。可以通过参数-XX:CMSInitiatingOccupancyFraction=N,来配置CMS开始工作的时机,N已使用的堆空间百分比。

第二种失败是因为空间碎片。CMS不会重新分配存活的对象,所以堆中的空闲空间将是分散的。例如,我们分配100W的对象,每个对象占用空间1KB,消耗了所有的堆空间,然后回收所有的偶数对象,剩下50W的对象,占用500M内存空间,还剩余500M内存空间,但是剩余空间是50W个1KB的不连续的快,如果这个时候,需要分配一个2KB的对象,将没有可用的空间供其分配。在这种情况下,CMS也不得不暂停所有线程,压缩堆空间。

所以,耗时的gc时间,主要是因为CMS两者异常工作方式产生的。应用程序设计中就需要考虑如何避免这两情况的产生,从而减少full gc暂停对应用产生的致命影响


 

你可能感兴趣的:(java)