前言
上几篇讲了几个术语,有必要了解一下。
正文
巨型对象
G1收集器设计巨型对象(Humongous Objects)主要是出于对内存管理和垃圾收集效率的考虑
G1对大尺寸对象(在G1中被称为“巨型对象”)分配会做特殊处理。前面有讲过,巨型对象就是大小达到甚至超过一个分区 50%空间的对象。这个尺寸包括 Java 对象头。对象头的尺寸在 32位和 64位的 HotSpot 虚拟机中是不一样的。一个指定 HotSpot 虚拟机中某个指定对象的头尺寸可以通过 Java对象布局工具来获取,也就是JOL。
JOL的全称是(Java Object Layout) 即 Java 对象内存布局。
是一个用来分析 JVM 中 Object 布局的小工具。包括 Object 在内存中的占用情况,实例对象的引用情况等等。
当发生巨型对象分配时,G1会找出一个连续的可用分区集合,这样就能汇总出足够的内存来容纳巨型对象。第一个分区被标记为“巨型开始”(humongous start)分区,其他的分区被标记为“巨型连续”(humongous continues)分区。如果没有足够的连续可用空间,G1就会启动一次 full GC来压缩 Java 堆空间。
巨型分区被认为是老年代的组成部分,但它们只包含一个对象。这个性质允许G1旦在并发标记阶段发现该对象已经不再存活,就可以尽早回收这个巨型分区。一旦发生这种情况,所有用来容纳这个巨型对象的分区都将被回收。
G1 面临的一个潜在的挑战,就是某些“短命的”巨型对象虽然已经变成未被引用,但可能一直没有被回收。JDK 8u40中实现了一个方法,某些情况下在年轻代收集时回收巨型分区。使用 G1时避免过于频繁的巨型对象分配,对达成应用性能目标有决定性的帮助。对那些有大量短命巨型对象的应用来说,增强JDK 8u40有一定帮助,但不是最终的解决方案。
Full 垃圾收集
G1里 full GC 使用的是与串行垃圾收集器相同的算法。当发生full GC时,就会执行对整个内存堆的全面压缩。这确保最大数量的空闲内存可以被系统使用。很重要的一点是 G1 的 full GC活动是单线程的,结果就是可能导致异常长的暂停时间。
当然,G1 的设计方式也希望使 full GC 不再是必需的。G1希望不用 full GC 就能满足应用的性能目标,然后通过不断地调优从而不再需要full GC。
并发周期
一个G1并发周期包含了几个阶段的活动:1初始标记、2并发根分区扫描、3并发标记、4重新标记以及5清除。
一个并发周期从初始标记开始,到清除阶段结束。除了清除阶段,所有这些阶段都是“标记存活对象图”的组成部分。
1初始标记阶段的目的是收集所有的GC根。根是对象图的起点。为了从应用线程中收集根引用,必须先暂停这些应用线程,所以初始标记阶段是 stop-the-world 方式的。在G1里,完成初始标记是年轻代GC暂停的一个组成部分,因为无论如何年轻代GC都必须收集所有根。
标记操作的同时还必须扫描和跟踪 survivor 分区里所有对象的引用。这也是2并发根分区扫描所要做的事。在这个阶段,所有 Java 线程都允许执行,所以不会发生应用暂停。唯一的限制就是在下一次 GC启动前必须先完成扫描;这样做的原因是一次新的GC 会产生一个新的存活对象集合,它们跟初始标记的存活对象是有区别的。
大部分标记工作是在3并发标记阶段完成的。多个线程协同标示存活对象图。所有Java线程都可以与并发标记线程同时运行,所以应用就不存在暂停,尽管会受到吞吐量下降的一些影响。
完成并发标记后就需要另一个 stop-the-world 方式的阶段来最终完成所有的标记工作。这个阶段被称为4重新标记阶段,通常它只是一个非常短暂的 stop-the-world 的暂停。
并发标记的最终阶段是5清除阶段。在这个阶段,找出来的那些没有任何存活对象的分区将被回收。正因为它们没有任何存活对象,这些分区也不会被包含在年轻代或混合GC中,它们会被添加到可用分区的队列里。
完成标记阶段之后,就能找出哪些对象是存活的,进而确定哪些分区要被包含在混合GC里。既然G1里混合GC是释放内存的基本手段,那么在G1用光可用分区之前完成标记阶段就显得至关重要,如果做不到的话,G1只能退回去发起一次full GC来释放内存,这虽然可靠却很慢。
确保标记阶段及时完成以避免full GC。