G1收集器
G1(Garbage-Frist)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以及高概率满足GC停顿时间要求的同时,还具备高吞吐量性能的特性.
G1将Java堆划分为多个大小相等的独立区域(Region),JVM目标似乎不超过2048个Region(JVM源码里TARGET_REGION_NUMBER定义),实际可以超过该值,但是不推荐.
一般Region大小等于堆除以2048.例如堆大小为4096M,则Region大小为2M,当然也可以用参数-XX:G1HeapRegionSize
手动指定Region大小,但是推荐默认的计算方式.
G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合.
默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200M左右的内存,对应大概是100个Region.可以通过-XX:G1NewSizePercent
设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过-XX:G1MaxNewSizePercent
调整.年轻代中的Eden和Survivor对应的Region也跟之前一样,默认8:1:1,假设年轻代现有100个Region,Eden区对应80个,S0对应10个,S1对应10个.
一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是Region的区域功能可能会动态变化.
G1垃圾收集器对于对象什么时候转移到老年代跟CMS是一样的,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫 Humongous区,而不是让大对象直接进入老年代的Region中.在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放.
Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销.
Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收.
G1收集器一次GC(主要指Mixed GC)的运作过程大致分为以下几个步骤:
- 初始标记(initial mark,STW):暂停所有的其他线程,并记录下GC Roots直接能引用的对象,速度很快.
- 并发标记(Concurrent Marking):并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长,但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行.因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变.
- 最终标记(Remark,STW):最终标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记变动的那一部分对象的标注记录(主要是处理漏标问题),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短.
- 筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的 回收价值和成本进行排序,根据用户所期望的GC停顿STW时间(可以用参数
-XX:MaxGCPauseMillis
指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200ms,那么通过之前回收成本计算得知,可能回收800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内.这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率.不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个Region中存活的对象复制到另一个Region中,这种不会像CMS那样回收完成会有很多内存碎片需要整理一次,G1采用的复制算法回收几乎不会有太多的内存碎片.(注意:CMS回收阶段是跟用户线程一起并发执行的.G1因为内部实现太复杂暂时没实现并发回收).
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字的又来Garbage-First的由来),比如一个Region花200ms能回收10M的垃圾,另外一个Region花50ms就能回收20M的垃圾,在回收时间有限的情况下,G1当然会优先选择后面这个Region回收.这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内可以尽可能高的收集效率.
被视为JDK1.7以上版本Java虚拟机的一个重要进化特征.它具备以下特点:
- 并行与并发:G1能充分理由CPU,多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短STW停顿时间.部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行.
- 分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个堆,但是还是保留的分代的概念.
- 空间整合:与CMS的"标记-整理"算法不同,G1从整体来看是基于"标记-整理"算法实现的收集器;从局部来看是基于"标记-复制"算法实现的.
- 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立 可预测的停顿时间模型,能够让使用者明确指定在一个长度为M毫秒的时间内(通过参数
-XX:MaxGCPauseMillis
指定)内完成垃圾收集
毫无疑问,可以由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的一个最佳平衡.不过,这里设置的"期望值"必须是符合实际的,不能异想天开,毕竟G1是要冻结用户线程来复制对象的,这个停顿时间再怎么低也得有个限度.它默认的停顿时间是200ms,一般来说,回收阶段占到几十甚至近200ms都很正常,但如果我们把停顿时间调的特别低,比如设置为20ms,很可能出现的结果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积.很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间,但应用运行时间一长就不行了,最终占满堆引发Full GC反而降低性能,所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒都是比较合理的.
G1垃圾收集分类
- Young GC:Young GC并不是说现有的Eden区放慢了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数
-XX:MaxGCPauseMillis
设定的值,那么增加年轻代的Region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数-XX:MaxGCPauseMillis
设定的值,那么就会触发Young GC - Mixed GC:不是Full GC,老年代的堆占有率达到参数(
-XX:InitiatingHeapOccupancyPercent
)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定Old区垃圾收集的优先顺序)以及 大对象区,正常情况G1的垃圾收集是先做Mixed GC,主要使用复制算法,需要把各个Region中存活的对象拷贝到别的Region里去,拷贝过程中如果发现 没有足够的空的Region 能够承载拷贝对象就会触发一次Full GC - Full GC:停止系统程序,然后采用单线程进行标记,清理和压缩整理,好空闲出一批Region来供下一次Mixed GC使用,这个过程是非常耗时的.
G1收集器参数设置
-XX:+UseG1GC
:使用G1收集器
-XX:ParallelGCThreads
:指定GC工作的线程数量
-XX:G1HeapRegionSize
:指定分区大小(1M-32M,且必须是2的N次幂),默认将整堆划分为2048个分区
-XX:MaxGCPauseMillis
:目标暂停时间(默认200ms)
-XX:G1NewSizePercent
:新生代内存初始空间(默认整堆5%,值配置整数,默认就是百分比)
-XX:G1MaxNewSizePercent
:新生代内存最大空间
-XX:TargetSurvivorRatio
:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+2+年轻n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代.
-XX:MaxTenuringThreshold
:最大年龄阈值(默认15)
-XX:InitiatingHeapOccupancyPercent
:老年代占用空间达到整堆内存阈值(默认45%),则新生代和老年代的混合收集(Mixed GC),比如堆默认2048个Region,如果有接近1000个Region都是老年代的Region,则可能就要触发Mixed GC了
-XX:G1MixedGCLiveThresholdPercent
:Region中存活对象低于这个值才会回收该Region(默认85%),如果超过这个值,存活对象过多,回收的意义不大
-XX:G1MixedGCCountTarget
:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会儿,然后暂停回收,恢复系统运行,一会儿再开始回收,这样可以让系统不至于单次停顿时间过长.
-XX:G1HeapWastePercent
:GC过程中空出来的Region是否充足阈值(默认5%),在混合回收的时候,对于Region回收都是基于复制算法进行的,都是要把回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话,回收过程中就会不断空出来新的Region,一旦空闲出来的Region达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就要结束了.
G1垃圾收集器优化建议
假设参数-XX:MaxGCPauseMills
设置的值很大,导致系统运行很久才会做年轻代GC,年轻代可能都占用堆内存的60%了,此时才触发年轻代GC,那么存活下来的对象可能就会很多,此时就会导致Survivor区放不下那么多的对象就会进入老年代中,或者年轻代的GC过后,存活下来的对象过多,导致进入Survivor区域触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中.
假设参数-XX:MaxGCPauseMills
设置的值很小,随着系统的运行.最终占满堆引发Full GC反而降低性能.
G1垃圾收集器优化主要是要根据场景设计一个合理的-XX:MaxGCPauseMills
.
G1适用场景
- 50%以上的堆被存活对象占用
- 对象分配和晋升的速度变化非常大
- 垃圾回收时间特别长,超过1s
- 8G以上的堆内存(建议值)
- 停顿时间是500ms以内
每秒几十W并发的系统如何优化JVM
Kafka类似的支撑高并发消息系统大家并不陌生,对于Kafka来说,每秒处理几万甚至几十万的消息是很正常的,一般来说部署Kafka需要用大内存机器(比如64G),也就是说可以给年轻代分配三四十G的内存来支持高并发处理.这里就涉及到一个问题了,我们常说对于Eden区的Young GC是很快的,这种情况下它的内存还会很快么?很显然,不可能,因为内存太大,处理还是要花不少时间的,假设三四十G内存回收可能最快也要几秒钟,按Kafka这个并发量放满三四十G的Eden区可能也就一两分钟吧,那么就意味着整个系统每运行一两分钟就会因为Young GC卡顿几秒钟没法处理新消息,显然是不行的.那么对于这种情况如何优化?我们可以适用G1收集器,设置-XX:MaxGCPauseMills
为50ms,假设50ms能够回收三四G内存,然后50ms的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下一遍处理业务一遍收集垃圾.
G1天生就适合这中大内存机器的JVM运行,可以比较完美的解决大内存垃圾回收时间过长的问题.