参考地址:
java GC工作在分代的模型上。大多数的对象要么很快的死亡,要么持续较长的时间。例如,方法栈中创建的对象只会持续几毫秒时间,而缓存中的对象会持续几分钟。
既然对象有两种不同的什么周期,直觉认为,使用不同的垃圾收集算法也许能够在不同的周期中更好的完成工作。所以,JVM把堆空间拆封为两个不同的区域,新生代,旧生代。当对象刚开始创建的时候,会被分配到新生代。当对象在新生代中经过多次gc后,依然存活时,就假设这个对象将持续较长时间,就把这个对象复制到旧生代中去。
JVM加上启动参数-XX:+UseParNewGC 和 -XX:+UseConcMarkSweepGC将为新生代启用Parallel New collector为旧生代启用Concurrent Mark-Sweep collector。
Parallel New collector工作时,会首先停止所有运行中的java线程。然后追踪对象引用以判断哪些对象是活着的。最后把活着的对象复制到一个一块空闲的堆区域,更所所有的指针到新的地址。有两个重要的地方值得注意:
每次复制一个对象后,将增加这个对象的收集计数,当一个对象在新生代中被复制了一定次数后,该算法即判定该对象是长周期的对象,把他移动到旧生代。这个阈值叫着tenuring threshold。
旧生代的Concurrent Mark Sweep(CMS)
每次Parallel New collector工作时,会把一部分对象复制到旧生代中。所以,旧生代很快就会被填满,就需要一个策略来收集它。CMS负责处理旧生代中的死亡对象。
CMS通过多个步骤来完成工作。有一些步骤会暂停所有线程(stop the world),其他步骤是和应用并行的执行的。主要的步骤为:
有几个重要地方需要注意:
如上面描述,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暂停对应用产生的致命影响。