实战JAVA虚拟机 JVM故障诊断与性能优化(五)---->G1回收器
G1回收器(Garbage First Garbage Collector)
G1回收器拥有独特的垃圾回收器,和之前提到的回收器截然不同。从分代上看,G1依然属于分代垃圾回收器,它会区分年轻代和老年代,依然有edan区和survivor区,但从堆的结构上看,它并不要求整个eden区、年轻代或者老年代都连续。它使用分区算法。作为CMS长期替代方案,G1同时使用了全新的分区算法,特点如下:
并行性:G1在回收期间,可以由多个GC线程同时工作。
并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收期间完成阻塞应用程序。
分代GC:G1同时兼顾年轻代和老年代,对比其他回收器,他们或者工作在年轻代或者老年代。
空间整理:G1在回收过程中,会进行适当的移动,不想CMS只简单地标记,在若干次后必须进行空间碎片整理。而G1不同,它每次回收都会有效地复制对象,减少空间碎片。
预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对全局停顿也能得到较好的控制。
G1回收器进行分区,每次收集时候,只收集其中几个区域,可以控制停顿时间。
收集过程可能有4个阶段:
(1):新生代GC
(2):并发标记周期
(3):混合收集
(4):如果需要,可能进行Full GC
G1的新生代GC
新生代GC主要工作时回收eden区和survivor区,一旦eden区被占满,新生代GC就会启动。在回收后,多有的eden区都应该被清空,而survivor区会被清理一部分数据,但至少存一个survivor区,如下图。另一个重要的变化是老年代的区域增多,因为eden区和survivor区的对象可能晋升到老年代。
G1的并发标记周期
G1的并发阶段和CMS有点类似,它们都是为了降低一次停顿时间,而将可以和应用程序并发的部分单独提取出来执行。
并发标记分为以下几步:
初始标记:标记从根节点直接可达的对象。这个阶段会伴随一次新生代GC,它是会产生全局停顿的,应用程序线程在这个阶段必须停止。
根区域扫描:由于初始标记必然伴随一次新生代GC,所以在初始化标记后,eden会清空,并且存活的对象被转移survivor区,在这个阶段,会扫描有survivor区直接可达的老年代区域,斌标记这些直接可达的对象。但是跟区域扫描不能和新生代GC同时执行(因为跟区域扫描依赖survivor区的对象,而新生代GC会修改这个区域),因此如果恰巧此时需要进行新生代GC,GC需要等待跟区域扫描结束后才能进行,如果发生这种情况,这次新生代GC的时间就会延长。
并发标记:和CMS类似,并发标记将扫描整个堆的存活对象,并做好标记,这是一个并发的过程,并且这个过程可能被一次新生代GC打断。
重新标记:和CMS一样,重新标记也是会产生应用程序停顿的。在并发标记过程中,应用程序仍然在运行,因此标记结果可能进行修正,所以在此对上一次标记结果进行补充。在G1中,这个过程使用SATB(Snapshot-At-The-Beginning)算法完成,即G1会在标记之初为存活对象创建一个快照,这个快照有助于加速重新标记的速度。
独占清理:这个阶段会引起停顿。它将计算各个区域的存活对象和GC回收比例并进行排序,识别可供混合回收的区域。在这个阶段,还会更新记忆集(Remebered Set)。该阶段给出了需要被混合回收的区域进行标记,在混合回收阶段,需要这些信息。
并发清理阶段:识别并发清理完全空闲的区域。它是并发清理,不会引起停顿。并发标记周期结束后,又会有新的eden空间被使用,并发标记在执行之前和之后最大的不同是在该阶段后,系统增加了一些标记为G的区域。这些区域被标记,是因为他们内部的垃圾比例较高,因此希望在后续的混合GC中进行收集(注意在并发标记周期中并未正式收集这些区域)。这些将要被回收的区域会被G1记录在一个称为Collection Set(回收集)的集合中。
并发回收阶段的整体流程图:
混合回收
在并发标记周期中,虽然有部分对象被回收,但是总体上说,回收的比例是相当低的。但是在并发标记周期后,G1已经明确知道哪些区域含有比较多的垃圾对象,在混合回收阶段,就会专门针对这些区域进行回收。当然,G1会优先回收垃圾比例较高的区域,因为回收这些区域的性价比也会比较高,G1全称为垃圾优先的垃圾回收器,这里的垃圾优先(Garbage First)指的就是回收优先选取的垃圾比例较高的区域。
这个阶段叫混合回收,会执行正常的年轻代GC和被选取标记的老年代区域进行回收,同时处理年轻代和老年代。由于新生代GC的原因。eden区域会必然被清空,此外,被标记为G的垃圾比例较高的区域被清理。被清理区域中的存活对象会被移动其他区域,减少空间碎片。
混合GC会执行多次,直到回收了足够多的内存空间,混合GC以及G1整体示意图:
必要时的Full GC
和CMS类似,并发收集由于应用程序和GC线程交替工作,因此总是不能完全避免特别繁忙的场合会出现回收过程中内存不足的情况。当遇到这种情况时,G1也会转入一个Full GC进行回收
此外,如果在混合GC是发生空间不足或者在新生代GC时,survivor区和老年代无法容纳幸存对象,都会导致一次Full GC产生。
G1相关参数
-XX:+UseG1GC 打开GC1开关
-XX:MaxGCPauseMillis 指定最大停顿时间
-XX:ParallelGCThreads 设定并行回收时,GC工作线程数
-XX:InitiatingHeapOccupancyPercent 指定当整个堆使用率达到多少时,触发并发标记周期执行,默认值是45,InitiatingHeapOccupancyPercent一旦设置,
始终都不会被G1收集器修改,设置偏大,会导致Full GC,设置偏小,会使并发周期非常频繁,大量GC线程抢占CPU,
会导致应用程序的性能有所下降。
-XX:+DisableExplicitGC 禁止显示GC,使得System.gc()等价一个空函数。
-XX:+ExplicitGCInvokesConcurrent 开启CMS和G1并发执行
-XX:-ScavengeBeforeFullGC 去除Full GC之前的那次新生代GC。默认情况下ScavengeBeforeFullGC的值为true
-XX:MaxTenuringThreshold=15 参数MaxTenuringThreshold,控制新生代对象的最大年龄,默认情况等于15,也就是说最多经历15次GC后,就可以晋升到老年代。
在TLAB上分配对象
TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存,TLAB是一个线程专用的内存分配区域。为什么需要TLAB区域?为了加速对象的分配而生的,由于对象一般会分配在堆上,而堆是全局共享的。因此会出现竞争关系,从而降低了分配效率,考虑到对象分配几乎是Java常用的操作,因此Java虚拟机就使用了TLAB这种线程专属的区间来避免多线程冲突,提高了对象的分配效率。TLAB本身占用了edem区的空间。在TLAB启动的情况下,虚拟机会为每一个Java线程分配一块TLAB空间。
对象分配简要流程:
方法finalize()对垃圾回收的影响
finalize()会导致对象复活,执行的时间是没有保障的,一个糟糕的finalize()会严重影响GC的性能
函数finalize()是由FinalizerThread线程处理的。每一个即将被回收的并且包含finalize()方法的对象都会在正式回收前加入FinalizerThread的执行队列,该队列为java.lang.ref.ReferenceQueue引用队列,内部实现为链表结构,队列中每一项为java.lang.ref.Finalizer引用对象,本质为一个引用。
eg:当一个JDBCConnection被回收时,需要进行连接的关闭,在finalize方法中进行连接关闭。当然,由于其调用时间的不确定性,这不能单独性作为可靠的资源回收手段。