jvm

Java基础: GC(一) 介绍

Posted by ZhouJ000 on April 23, 2019

Java基础: GC(一) 介绍
Java基础: GC(二) G1垃圾收集器
Java基础: GC(三) G1垃圾收集器优化
Java基础: GC(四) SA调试工具

Java性能优化03-JVM调优

跟踪参数

打印简单GC日志:-XX:+PrintGC

[GC (Allocation Failure)  2048K->728K(9728K), 0.0010036 secs]
[GC (Allocation Failure)  2776K->704K(9728K), 0.0057727 secs]

打印更详细的GC日志,且会在退出前打印堆的详细信息:-XX:+PrintGCDetails

// Minor GC:YoungGeneration(eden)剩余的空间不够了,满了的时候触发,将Eden和Survivor的对象复制到另外一块Survivor上
// YoungGen共有1536KB,从GC前1512KB到GC后488KB,减少了1024KB,堆总大小为5632KB,现在从1732KB到了708KB,正是减少了这1024KB
// GC耗时0.0004852秒,用户态耗时、内核态耗时、总耗时都为0.00秒
[GC (Allocation Failure) [PSYoungGen: 1512K->488K(1536K)] 1732K->708K(5632K), 0.0004852 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1512K->488K(1536K)] 1732K->724K(5632K), 0.0005110 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 1536K, used 508K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 2% used [0x00000000ffe00000,0x00000000ffe05378,0x00000000fff00000)
  from space 512K, 95% used [0x00000000fff80000,0x00000000ffffa020,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 4096K, used 236K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 5% used [0x00000000ffa00000,0x00000000ffa3b010,0x00000000ffe00000)
 Metaspace       used 3285K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 360K, capacity 388K, committed 512K, reserved 1048576K

// Full GC:其中JDK1.8以后是用的Metaspace
[Full GC (Ergonomics) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 3050K->3195K(4096K)] 3554K->3195K(5632K), [Metaspace: 3283K->3283K(1056768K)], 0.0351583 secs] [Times: user=0.06 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 1024K->0K(1536K)] [ParOldGen: 3195K->3781K(4096K)] 4219K->3781K(5632K), [Metaspace: 3283K->3283K(1056768K)], 0.0186923 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 

如果要更全面的堆信息,会分别表示GC前和后的堆信息:-XX:+PrintHeapAtGC

{Heap before GC invocations=1 (full 0):
 PSYoungGen      total 1536K, used 1024K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 100% used [0x00000000ffe00000,0x00000000fff00000,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 4096K, used 0K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 0% used [0x00000000ffa00000,0x00000000ffa00000,0x00000000ffe00000)
 Metaspace       used 2577K, capacity 4480K, committed 4480K, reserved 1056768K
  class space    used 283K, capacity 384K, committed 384K, reserved 1048576K
Heap after GC invocations=1 (full 0):
 PSYoungGen      total 1536K, used 488K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 4096K, used 136K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 3% used [0x00000000ffa00000,0x00000000ffa22000,0x00000000ffe00000)
 Metaspace       used 2577K, capacity 4480K, committed 4480K, reserved 1056768K
  class space    used 283K, capacity 384K, committed 384K, reserved 1048576K
}

如果需要需要知道应用程序的执行时间和停顿时间,在之前基础上加上:-XX:+PrintGCApplicationConcurrentTime-XX:+PrintGCApplicationStoppedTime

[GC (Allocation Failure) [PSYoungGen: 1024K->504K(1536K)] 1024K->608K(5632K), 0.0010120 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Total time for which application threads were stopped: 0.0011399 seconds, Stopping threads took: 0.0000190 seconds
Application time: 0.0039253 seconds

如果还需要跟踪系统内软引用、弱引用、虚引用和Finallize队列,在之前基础上再加上:-XX:+PrintReferenceGC

[GC (Allocation Failure) [SoftReference, 0 refs, 0.0000090 secs][WeakReference, 10 refs, 0.0000047 secs][FinalReference, 31 refs, 0.0000093 secs][PhantomReference, 0 refs, 0 refs, 0.0000037 secs][JNI Weak Reference, 0.0000028 secs][PSYoungGen: 1024K->488K(1536K)] 1024K->600K(5632K), 0.0007993 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [SoftReference, 0 refs, 0.0000096 secs][WeakReference, 2 refs, 0.0000037 secs][FinalReference, 31 refs, 0.0000078 secs][PhantomReference, 0 refs, 0 refs, 0.0000047 secs][JNI Weak Reference, 0.0000019 secs][PSYoungGen: 1512K->488K(1536K)] 1624K->656K(5632K), 0.0009654 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

如果需要查看在survivor里面有效的对象的岁数:-XX:+PrintTenuringDistribution

Desired survivor size 524288 bytes, new threshold 7 (max 15)

当然,输出在控制台的GC日志可以记录到log文件中:-Xloggc:logpath

有时候可能需要知道系统加载了哪些类,这时可以使用:-verbose:class跟踪类的加载和卸载。或者单独使用:-XX:+TraceClassLoading跟踪类的加载;单独使用:-XX:+TraceClassUnloading跟踪类的卸载。如果加上:-XX:+PrintClassHistogram还可以在运行时在控制台使用Ctrl+Break组合键显示Full thread dump和当前类信息图

打印出传递给虚拟机的显式参数和隐式参数:-XX:+PrintCommandLineFlags

-XX:InitialHeapSize=5242880 -XX:MaxHeapSize=5242880 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 

除此之外可以使用:-XX:+PrintFlagsFinal打印出所有的系统参数,里面有每一个参数和当前取值,有兴趣可以深入研究

术语

最早,串行GC、并行GC和并发标记清除(CMS)GC这3种垃圾收集器基本覆盖了GC最重要的3种使用场景:内存占用空间以及并发开销最小化、应用吞吐量最大化、GC相关中断时间最小化

串行:即这是个单线程的垃圾收集运算

并行:即这是个多线程的垃圾收集运算。对于Hotspot来说,几乎所有多线程GC操作都由JVM的内部线程处理;而对于G1垃圾收集器,G1中的某些后台垃圾收集工作能够由应用线程来承担

stop-the-world(STW):即在一个垃圾收集事件中,所有的Java应用线程全部被暂停。那么一个stop-the-world垃圾收集器就意味着当它执行垃圾收集操作时会停掉所有Java应用线程,因为这时系统才不会产生新的垃圾,这个在停顿时间,整个应用被卡死没有响应的停顿被称为STW

并发:在Java应用执行过程中,垃圾收集活动也能同时进行

Minor GC

Java虚拟机中内置了两个即时编译器,分别为Client和Server,即C1和C2。C1编译器将字节码编译为本地代码,进行简单可靠的优化,C2编译器则会启动一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。C1和C2都是编译一些热点代码(多次调用的方法或者多次执行的循环体),因此在编译前,首先要进行热点探测,HotSpot虚拟机中使用的是基于计数器的热点探测方法。这两种编译器模式(C1、C2)使用不同的JIT编译技术,它们可以为同一Java方法生成不同的机器代码,在JDK1.7之后有一种分层编译的新特性(C1+C2:-XX:+TieredCompilation),该功能启用后,在启动时使用C1编译器模式以提供更好的启动性能。当应用程序被适当预热后(随着时间推移,执行频率较高的代码),C2编译器模式将接管以提供更积极的优化。在JDK1.8中是默认启用的。因此当启用JIT编译优化后,常用的优化技术为标量替换、栈上分配,在此前提下:

  • 新对象尝试栈上分配,不行再尝试TLAB分配,不行则考虑是否直接绕过eden区在年老代分配空间(-XX:PretenureSizeThreshold设置大对象直接进入年老代的阈值),不行则最后考虑在eden申请空间
  • 向eden申请空间创建新对象,eden没有足够的空间,因此触发Minor GC
  • minor gc将eden区from survivor区域的存活对象进行处理
    • 其中年龄达到阈值的对象,则直接晋升到年老代
    • 若要拷贝的对象太大,那么不会拷贝到to survivor,而是直接进入年老代
    • 若to survivor区域空间不够或者复制过程中出现不够,则发生survivor溢出,直接进入年老代
    • 当to survivor区域空间充足时,则存活对象拷贝到to survivor区域
  • 此时eden区以及from survivor区域的剩余对象为垃圾对象,直接抹掉回收,释放的空间成为新的可分配的空间
  • minor gc之后,若eden空间足够,则新对象在eden分配空间;若eden空间仍然不够,则新对象直接在年老代分配空间

判断触及性

垃圾回收的基本思想是考察每个对象的可触及性,即从根节点开始是否可以访问到这个对象,如果可以,则说明对象正在被使用,如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说这个对象就需要被回收了。但事实上,一个无法触及的对象有可能在某一条件下”复活”自己,这时对它回收就是不合理的,因此一个对象的可触及性包含以下3种状态:
1、可触及的:从根节点开始,可以达到这个对象
2、可复活的:对象的所有引用都被释放,但是对象有可能在finalize()函数中复活
3、不可触及的:在可复活状态下,对象的finalize()函数调用后没有复活,那么就进入不可触及状态

在Java中还提供了4个级别的引用:强引用、软引用、弱引用、虚引用。强引用就是一般使用的引用类型,是可触及的,不会被回收。相对的剩下3种引用的对象是软可触及、弱可触及、虚可触及的,在一定条件下都是可以被回收的:
1、强引用:可以直接访问目标对象,所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常也不会回收强引用所指向的对象,因此强引用可能会导致内存泄露
2、软引用:比强引用弱一点,一个对象只持有软引用时,平时GC不一定会回收软引用对象,在堆空间不足时会被回收,并加入到一个注册的引用队列,所以软引用对象不会引起内存溢出。使用SoftReference类实现
3、弱引用:比软引用再弱一点,在系统GC时只要发现弱引用,都会回收对象,但是由于垃圾回收器的线程通常优先级很低,因此并不一定能很快发现持有弱引用的对象,这样弱引用能存在较长时间,一旦弱引用被回收,便会加入到一个注册的引用队列。使用WeakReference类实现
总结:软引用、弱引用都非常适合保存一些可有可无的缓存数据,当系统内存不足时便会被回收,不会导致内存溢出,而系统内存充足时又能存在相当长的时间,起到加速系统的作用
4、虚引用:所有引用类型中最弱的一个,持有虚引用的对象和没有引用几乎一样,随时可能被垃圾回收器回收,当试图通过虚引用的get()方法取得强引用时,总是会失败,而且虚引用必须和引用队列一起使用,它的作用就是在于跟踪垃圾回收过程,因为它被回收对象后会加入引用队列,可以通知应用程序对象的回收情况,比如跟踪对象的回收时间。使用PhantomReference实现

垃圾收集器

串行垃圾收集器

串行垃圾收集器是一种单线程的stop-the-world的收集器,单线程意味着垃圾收集器实现的复杂度更低,以及需要非常少的外部运行时数据结构,其内存占用空间大小(footprint)也是所有Hotspot垃圾收集器中最低的,但是引发的长暂停会更明显,因为所有垃圾收集工作都在一个线程中完成,激活参数为:-XX:UseSerialGC

并行垃圾收集器

并行垃圾收集器是一种并行stop-the-world的收集器,也就是每发生一次垃圾收集,它会停掉所有应用的线程并用多个线程执行垃圾回收工作。因此垃圾回收工作可以不接受任何中断非常高效地完成,对相关应用来说,这通常也是最小化垃圾收集工作开销时间的最好方式。然而某些个别情况下,因垃圾回收而导致应用中断可能会很长时间

在并行垃圾收集器中,年轻代和老年代的回收都是并行的,而且会使stop-the-world,其中老年代的回收还会同时进行压缩动作。压缩会将邻近的对象移动到一起,以消除它们之间被浪费的空间,形成一个最理想的堆布局,然而压缩也会花费较长时间,这通常与Java堆大小和老年代中存活对象的数量和大小有关

最初只有年轻代使用并行STW收集器,老年代使用单线程STW收集器,激活参数为:-XX:+UseParallelGC。随着堆大小与硬件的发展,可以使用多线程的年轻代收集器和多线程的老年代收集器,并行垃圾收集器获得增强,这使并行收集器降低了收集和压缩堆的时间开销,激活参数为:+XX:+UseParallelOldGC,即同时激活并行的年轻代与老年代收集器。这在JDK1.7以后成为默认垃圾收集器和并行垃圾收集器的标准操作模式

并发标记清除(CMS)垃圾收集器

随着越来越多应用需求一个垃圾收集器,它能够比串行或并行垃圾收集器有更短的最坏情况的中断,就算牺牲一些应用吞吐量来消除或极大地减少漫长的GC中断数量也是可接受的,这样CMS垃圾收集器被开发了出来

在CMS垃圾收集器中,年轻代的垃圾收集与并行垃圾收集很类似,都是并行且STW的,也就是在年轻代的垃圾收集过程中所有的Java应用线程都会被暂停,而垃圾收集工作是以多线程方式执行的。因此并行垃圾收集器与CMS垃圾收集器的主要区别就是在老年代的收集上。CMS收集器的老年代收集活动试图避免应用线程的长时间中断,这样CMS老年代收集器在应用线程执行的同时做了大部分工作(垃圾收集与应用线程同时工作),除了少量相对较短的GC同步暂停。通常来说,CMS在绝大多数情况下是并发的,老年代收集的某些阶段会暂停应用程序,比如初始标记、重新标记阶段。在CMS最初的实现中,初始标记和重新标记阶段是单线程的,但是现在都是多线程的了。激活多线程的初始标记和重新标记的Hotspot命令参数分别为:-XX:CMSParallelInitialMarkEnabled-XX:CMSParallelRemarkEnabled,当通过参数XX:+UseConcurrentMarkSweepGC激活CMS垃圾收集器时会缺省自动激活这两个选项。有可能在一个老年代并发收集正在进行的时候,又发生了一个年轻代收集,一旦发生这种情况,老年代并发收集会被年轻代收集所中断,直到后者结束之后立刻恢复执行。CMS GC的缺省年轻代收集器被称为ParNew收集器

总体讲,CSM可以分为这几个步骤:初始标记(STW)、并发标记、并发预清理、重标记(STW)、并发清理、重置。在并发过程中,如果有足够的可用硬件线程,那么CMS线程的执行成本不会对Java应用线程的性能产生太大影响,但是如果硬件线程被饱和或高度利用,那么CMS线程就会与Java应用线程竞争CPU周期。使用CMS垃圾收集器的一个挑战就是要在应用消耗完Java的可用堆空间之前完成并发收集工作,因此对于CMS很棘手的一个问题就是要找到一个合适的时机来启动这个并发工作。这种并发方式往往导致一个结果,就是处理同一个应用CMS GC会比并行GC多占用10%~20%的Java堆空间,这也是为了缩短垃圾收集暂停时间所付出的代价;CMS垃圾收集器的另一个挑战是如何处理老年代中的空间碎片,即老年代中对象间的空间碎片太小,以至于无法容纳从年轻代晋升上来的对象,因为CMS的并发收集循环中并不执行压缩,哪怕是增量或局部压缩,因此一旦无法找到可用空间,就会使CMS回过来使用串行GC,触发一次FULL GC,导致一个漫长的暂停,伴随着CMS碎片另一个很不幸的挑战就是上述的问题完全无法预测,同样是老年代碎片,某些应用可能没有经历过一次FULL GC,有些可能时不时就要进行一次。因此对CMS垃圾收集器做些调整,对应用做些优化改动,避免生成大尺寸对象,会有助于延缓空间碎片的产生

图解 CMS 垃圾回收机制

上述收集器总结

上面提到的3种垃圾收集器都有几个共同的问题,一个就是所有老年代收集器的大部分操作都必须扫描整个老年代空间,比如标记、清除、压缩,这意味着执行工作的时间将随着Java堆空间的变化而线性增加或减少。另一个问题是因为年轻代和老年代是独立的连续内存块,所以要先决定年轻代和年老代放在虚拟地址空间的什么位置

Garbage First垃圾收集器

G1垃圾收集器将堆拆分成一系列的分区,这样在一个时间段内,大部分的垃圾收集器就只是在一个分区内执行,而不是在整个堆或整个老年代。在G1中,年轻代就是一系列的内存分区,这样就不再要求年轻代是一个连续的内存块;类似的老年代也是一系列的内存分区,那么就不需要在JVM运行时考虑哪些分区是老年代,哪些是年轻代。事实上,G1通常的运行状态是映射G1分区的虚拟内存随着时间的推移在不同的代之间前后切换,一个G1分区最初被指定为年轻代,经过一次年轻代的回收后,整个年轻代分区就被划入到未被使用的分区中,那它就可以被使用在别的地方了,可以使用“可用分区”来定义这些未被使用且可以被G1使用的分区

G1年轻代的收集方式是并行STW的,前面说过,并行STW回收时将暂停所有Java应用线程,而垃圾回收的工作也将通过多个线程来分担,与其他Hotspot垃圾收集器一样,一旦发生一次年轻代收集,整个年轻代都会被回收。而G1的老年代垃圾收集方式就与其他Hotspot垃圾收集方式有极大的不同了,G1老年代的收集不会为了释放老年代的空间而要求对整个老年代做回收,相反,在任一时刻只有一部分的老年代分区会被回收,并且这部分老年代分区将与一次年轻代收集一起被收集

混合(mixed)垃圾收集器就是用来描述这种一部分老年代分区与年轻代垃圾收集结合在一起进行的收集。因此,混合GC就是将要被回收的年轻代与年老代分区的组合

与CMS GC类似,当遇到一些极端情况,诸如老年代空间被消耗完了,会有一个安全措施来收集和压缩整个老年代。撇开这个安全模式下的收集,一个G1老年代的收集是由一系列阶段组成,某些是并行STW的,某些是并行并发的。当超过Java堆的占用阈值,G1就会启动一次老年代收集,这个G1中的堆占用阈值,是根据老年代占用空间与整个Java堆空间相比较得出的。而CMS GC触发老年代收集所用的占比阈值只是相对于老年代空间本身而言的。在G1中,一旦打到或超过内存堆的占用阈值,一次并发STW方式的初始标记阶段就会被安排执行。初始标记阶段会跟着下一次年轻代收集同时进行,一旦初始阶段标记结束,就会触发一个并发多线程的标记阶段,标记老年代中所有的存活对象。当并发标记阶段结束,并行STW的重新标记阶段就会启动,标记那些因为在标记阶段同时执行的应用线程导致产生错过的对象。到重新标记阶段结束,G1就拥有了老年代分区的完整信息。如果碰巧老年代分区里一个存活对象也没有,那么在下一个阶段,清除阶段,不用做额外垃圾收集工作就可以被回收利用。同样也是在重新标记阶段结束,G1能识别出最适合回收的老年代分区集合(Cset)。选择那些分区被包含在一个Cset中,是基于有多少空间可以被释放以及G1暂停时间目标。在完成Cset识别后,G1就在接下来的几次年轻代垃圾收集中,处理年轻代分区,还有一部分的老年代分区也将被回收,这就是前面提到的混合GC类型

不管是年轻代还是老年代,G1会把每个收集过的垃圾的分区中的存活对象转移到一个可用分区中,一旦存活对象被转移掉,你这个年轻代分区(可能还有老年代分区)就会被回收为可用分区。将各老年代分区中存活的对象转移到可用分区会带来一个很棒的结果:在虚拟地址空间里每个转移对象都是前后相连的,对象和对象之间没有碎片化的空余空间。而像CMS、并行以及串行垃圾收集器都需要一个full gc来压缩老年代,这个压缩动作需要扫描整个老年代空间。因为G1以每个分区为基础做垃圾收集操作,因此它适用于大尺寸的Java堆,垃圾收集工作的数量可以被限制在一个小范围的分区集合内,哪怕Java堆的尺寸相当大

G1暂停时间的最大来源是年轻代收集和混合收集,所以G1的设计目标之一就是允许用户设置GC暂停时间目标。G1会尝试通过调整Java堆尺寸大小的方式来满足设定的暂停时间目标。它根据暂停时间目标自动调整年轻代的尺寸和总Java堆尺寸,暂停时间目标越短,年轻代空间就越小,总的堆空间就越大,使得老年代空间就相对越大。G1的设计目标就是把必要的调整限定在设置最大的Java堆空间和指定GC暂停时间目标上。另外G1还被设计为可以通过一个内部的启发式算法来做自我调整

综上所述,对于大的Java堆来说,通过将Java堆拆分成一个个分区,G1会比其他垃圾收集器有更好的综合表现。在局部压缩的帮助下,G1解决了Java堆碎片,它的绝大多数工作都通过多线程的方式完成

G1设计

G1将Java堆分成多个分区,每个分区的大小可以根据堆的尺寸而改变,但必须是2的幂,同时最小1MB,最大为32MB,由此得出可以为1、2、4、8、16和32MB。所有分区的大小都是一样的,在JVM运行的过程中它们的尺寸不会发生变化。分区尺寸是基于Java堆的初始值和最大值的平均数进行计算的,这样对于一个平均堆尺寸就会有2000个左右的分区。比如-Xmx16g -Xms16g时G1就会选择16GB/2000=8MB的分区尺寸;而如果堆的初始和最大值相差很远或者这个堆尺寸很大,那就可能会产生远超过2000个分区;类似的堆内存很小,分区数量会远小于2000个

每个分区都有一个关联的已记忆集合(RSet,用于记录跟踪分区外指向分区内的引用),这样就避免了对整个堆的扫描,使得每个分区的GC更加独立。RSet总体的尺寸有限,但也不容忽视,因此分区数量会对Hotspot的内存空间占用有直接影响。RSet总体的尺寸严重依赖应用的行为,RSet最少时大概会占用1%左右的堆空间,最多可能会达到20%

一个特定的分区一次只能用于一个目的,一旦这个分区被包含进一次收集,它就会被彻底转移,同时被释放为一个可用分区。G1有多种类型的分区:可用分区是当前未被使用的,eden(新生代)分区组成了年轻代的eden空间,survivor(存活代)分区组成了年轻代的survivor空间,所有eden分区和survivor分区的总集合就是年轻代。eden分区或survivor分区的数量随着一次次的垃圾收集发生改变,包括年轻代收集、混合收集或full收集;老年代分区由绝大部分老年代组成,最后通常认为巨型分区是老年代的一个组成部分,它用来容纳那些大小达到或超过一个分区50%空间的对象,(在JDK 8u40前,巨型分区是作为老年代的一部分被收集的,而在JDK 8u40后,某些巨型分区是作为年轻代的一部分被收集的)。实际上,一个分区可以用于任何目的,也就是没有必要把内存堆划分为相邻的年轻代段和年老代段。G1的启发式算法会估算年轻代需要多少个分区,以及按照指定的GC暂停时间计算目前还有多少分区要被回收

一旦应用开始生产对象,G1就选中一个可用分区并将它指定为eden分区,然后从中取出内存块交给Java线程。当这个分区满了之后,另一个未被使用的分区会再被指定为eden空间,这个操作一直持续下去直到达到eden分区的上限数量,就会触发一次年轻代垃圾收集。一次年轻代垃圾收集会回收所有年轻代分区,包括eden分区和survivor分区。这些分区里的所有存活对象会被转移到另一个新的survivor分区或者老年代分区。在当前转移的目标分区满了之后,就会将新的可用分区标记为survivor分区或老年代分区,继续转移操作。当在一次GC后,老年代的空间占用达到或超过了堆空间的占用门槛(通过-XX:InitiatingHeapOccupancyPercent设置,默认为45%),G1就会启动一次老年代收集。当标记阶段显示某些老年代分区中没有任何存活对象时,G1会提前将它们回收,这些分区被加入到可用分区集合中。那些包含存活对象的老年代分区则被安排到将来的混合收集中。G1使用多个并发标记线程,为了尽量避免从应用线程中偷取太多CPU,标记线程的工作往往是爆发式的,它们在一个给定时间段里拼命工作,然后暂停一段时间,让Java线程得以执行

巨型(Humongous)对象

G1对大尺寸对象(巨型对象)分配会做特殊处理,即大小达到甚至超过一个分区50%空间的对象,这个尺寸包括Java对象头。当发生巨型对象分配时,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就能满足应用的性能目标,然后通过不断地调优从而不需要full gc

并发周期

一个G1并发周期包含了几个阶段的活动:初始标记、并发根分区扫描、并发标记、重新标记、清除,一个并发周期从初始标记开始,到清除阶段结束。除了清除阶段,所有这些阶段都是“标记存活对象图”的组成部分

初始标记阶段的目的是收集所有的GC根。根是对象图的起点,为了从应用线程中收集根引用,必须先暂停这些应用线程,所有初始标记阶段是STW方式的。在G1里,完成初始标记是年轻代GC暂停的一个组成部分,因为无论如何年轻代GC都必须收集所有根

标记操作的同时还必须扫描和跟踪survivor分区里所有对象的引用。这也是并发根分区扫描要做的事情,在这个阶段,所有Java线程都允许执行,所以不会发生应用暂停,唯一的限制就是在下一次GC启动必须先完成扫描。这样做的原因是一次新的GC会产生一个新的存活对象集合,它们跟初始标记的存活对象是有区别的

大部分标记工作是在并发标记阶段完成的,多个线程协同标识存活对象图,所有Java线程可以与并发标记线程同时运行,虽然不存在应用暂停,但是会收到吞吐量下降的影响

完成并发标记后就需要另一个STW方式的阶段来完成最终的所有标记工作,这个阶段被称为重新标记阶段,通常它只是一个很短暂的STW暂停

并发标记的最终阶段是清除阶段,这个阶段中,找出来的那些没有任何存活对象的分区将被回收,正因为它们没有任何存活对象,这些分区也不会被包含在年轻代或混合GC中,它们会被加入到可用分区队列中。完成标记阶段后,就能找出哪些对象是存活的,进而确定哪些分区要被包含在混合GC中。既然G1的混合GC是释放内存的基本手段,那么在G1用完可用分区之前完成标记阶段就显得至关重要,如果做不到的话,G1只能退回去发起一次full gc来释放内存,这虽然可靠但是很慢,因此确保标记阶段及时完成以避免full gc需要进行调优

堆空间调整

G1里的Java堆尺寸通常是分区尺寸的整数倍,出去这个限制,G1和其他Hotspot垃圾收集器一样,可以在-Xms-Xmx之间动态扩大或缩小堆的大小。其中基于以下几个理由,G1可能会增加堆的尺寸:
1、在一次full gc中,基于堆尺寸的计算结果会调整堆的空间
2、当发生年轻代收集或混合收集,G1会计算执行GC所花费的时间以及执行Java应用花费的时间,根据-XX:GCTimeRatio,如果将太多时间花费在垃圾收集上,Java堆尺寸就会增加,这个情况下增加尺寸,背后的想法就是允许GC减少发生频率,这样与花费在应用上的时间相比,花费在GC上的时间可以随之降低,G1中期缺省值为9,其他所有Hotspot垃圾收集器缺省为99,其中GCTimeRatio值越大,Java堆尺寸的增长就会更加积极,因此它们在增加堆尺寸的策略上更加激进,它们的目标是相对于执行应用开销,用于GC的时间越少越好
3、如果一个对象分配失败(甚至是做了一次GC后),G1会尝试通过增加堆尺寸来满足对象的分配,而不是马上去做一次full gc
4、如果一个巨型对象分配无法找到足够的连续空间来容纳,G1会尝试扩展Java堆来获取更多的可用分区,而不是做一次full gc
5、当GC需要一个新的分区来转移对象时,G1更倾向于通过增加Java堆空间来获得一个新的分区,而不是通过返回GC失败并开始做一次full gc来找到一个可用分区

扩展:
G1垃圾收集器
深入理解 Java G1 垃圾收集器