G1垃圾回收器
G1 Garbage Collector 垃圾回收器是可以同时回收新生代和老年代的对象,不需要两个垃圾回收器配合起来运作,他一个人就可以搞定所有垃圾回收。它最大的特点就是把Java堆内存拆分多个大小相等的Region。允许我们设置一个垃圾回收的预期停顿时间。在这个时间内,垃圾回收导致的停顿时间不能超过多久,G1全权给你负责,保证达到这个目标。
G1的Region 每个Region的大小是多大呢?
默认情况下,自动计算和设置,我们可以给整个堆设置大小,-Xms -Xmx 来设置堆的大小,然后JVM启动的时候一旦发现你使用的是G1垃圾回收器可以使用(-XX:UseG1gc)
G1 如何做到对垃圾回收导致的系统停顿可控?
追踪每个Region里的回收价值,搞清楚每个region里面的对象有多少是垃圾,如果对这个Region进行垃圾回收需要耗费多长时间,可以回收多少垃圾。
所以G1可以做到让你来设定垃圾回收对系统的影响,他自己通过吧内存拆分为大量大小一致Region,以及追踪每个Region中可以回收的对象大小和预估时间,最后在垃圾回收的时候,尽量吧垃圾回收对系统造成的影响控制在指定范围之内,同时在有限的时间回收更多的垃圾对象。
在G1对应的内存模型中,Region随时会属于新生代也会属于老年代,所以没有所谓的新生代给多少内存,老年代给多少内存,实际上新生代和老年代个字的内存区域是不停变动的,有G1自动控制
G1垃圾回收的过程
- 首先会触发一个“初始标记”的操作,这个过程是需要Stop the World的,仅仅只是标记一下GC roots 直接饮用的对象,这个过程比较快
- 接着会进入并发标记的阶段,这个阶段会允许新系统程序运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象,这个阶段是可以跟系统并发运行,所以对系统的影响并不大,而且JVM 会对并发标记阶段对象做出一些修改记录,比如那些对象失去了引用,哪些对象被新建了。
- 下一阶段是最终标记阶段,会进入Stop the World,系统程序是禁止运行的,但是会根据并发标记阶段记录的哪些对象修改,最终标记一下哪些对象存活,有哪些是垃圾对象
- 最后一个阶段教师混合回收,这个阶段会计算每个Region 中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。接着停止系统程序,然后全力以赴的尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在指定范围之内。
- 老年代在对内存占比达到45%的时候回触发混合回收
G1垃圾回收的参数
老年代的Region占据了堆的45%之后,会触发一个混合回收的过程,也就是Mixed GC(混合回收), 分为了好几个阶段,最后一个阶段就是执行混合回收,从新生代和老年代力回收一个Region,但最后一个阶段混合回收的时候,其实会停止程序运行,所以说G1 允许执行多次混合回收。
- -XX:G1MixedGCCountTarget 参数指定在一次混合回收中,最后一个阶段执行几次混合回收,默认是8次。
- -XX:G1HeapWastePercent 默认5%,在混合回收的时候,对Region回收都是基于复制算法进行,都是要把回收的Region 里的存活对象放入其他Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理,这样的话在回收过程就不断空出新的Region,一旦空闲出来的Region数量达到堆的5%,就会立刻停止垃圾回收。而且G1整体是基于赋值算法进行Region垃圾回收,不会出现内存碎片的问题,不需要像CMS那样标记-清理之后,在进行内存碎片的整理。
- -XX:G1MiexdGCLiveThresholdPercent 默认值是85%,意思就是确定要回收Region的时候必须是存活对象低于85%的才进行回收,否则要是一个Region的存活对象多余85%,回收成本是比较高的。
回收失败的Full GC
如果在进行混合回收的时候(Mixed),无论是年轻代还是老年代都基于赋值算法进行回收,都要把各个Region的存活对象拷贝到别的Region里面去,如果此时没有空闲的Region可以承载自己的存活对象就会触发一次失败,一旦失败就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩,空闲出来一批Region,这个过程是极慢的,
G1 垃圾回收器的默认布局
假设我们对机器分配 4g的堆内存,其中新生代默认初始占比5%,最大占比60%,每个java线程栈内存为1M,永久带内存256-Xms4096M -Xmx4096M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseG1GC
- -XX:G1NewSizePercent 该参数用来设置新生代初始化占比的,不用设置,维持默认百分之5%即可
- -XX:G1MaxNewSizePercent 该参数用来设置新生代最大占比,维持默认60%即可
此时的堆内存共4G,那么此时会除以2048,计算出每个Region的大小,此时每个Region的大小就是2M,新生代刚开始就占5%= 100个Region,有200MB的内存空间。
GC的停顿时间如何设置
在G1中垃圾回收器有一个至关重要的参数会影响到GC的表现 -XX:MaxGCPauseMills, 他默认值是200毫秒,默认设置的是希望每一次出发GC的时候导致系统停顿时间(Stop the world)不要超过200毫秒,避免系统长时间GC卡死。
多长时间会触发新生代GC
系统运行起来之后,会不停的在新生代Eden区域内分配对象,按照之前的推算是每秒分配3M的对象,当eden区域不足时,就会触发新生代gc,到底什么时候eden区域会内存不够呢?之前说过了-XX:G1MaxNewSizePercent 参数限定了新生代最多的就是占用堆内存的60%,但是G1并非直到新生代占据60%之后,无法再分配更多的Region了,再触发新生代gc。他会给新生代增加一些Region,然后让系统继续运行着在新生代region中分配对象好了,这样就不用过于频繁的触发新生代gc了,然后系统继续运行,一直到300个region都沾满了,此时通过计算发现回收着300个region大概需要200ms,那么可能这个时候就会触发一次新生代gc了。G1里是很动态灵活的,它会根据你设定的gc停顿的时间给你的新生代不停分配更多的Region,然后到一定程度,感觉差不多了,就会触发新生代gc,保证新生代gc的时候导致的系统停顿时间在你预设范围内。
其实上米那就是一个示例,G1到底会分配多少个Region给新生代,多久触发一次新生代gc,每次耗费多长时间,这些都是不能够确定的,G1垃圾回收器它会根据你预设的GC停顿时间,给新生代分配一些Region,然后到一定程度就会触发gc,并且吧gc控制在预设范围内,尽量避免一次性回收过多的region导致gc停顿时间超出预期。
新生代GC如何优化
比如对于g1而言,我们首先应该给整个jvm的堆区域足够的内存,我们给jvm超过5g的内存,其中堆内存有4g,合理的设置-XX:MaxGCPauseMills(最大停顿时间),如果参数设置较小,就可能每次gc停顿特别短,此时G1一旦发现你对几十个region占满了就会立即触发新生代gc然后gc特别频繁,虽然每次gc时间很短。如果参数设置过大,那么G1会允许你不停的在新生代理分配新的对象,然后积累了哼多对象,再一次性回收几百个Region,此时可能一次GC停顿时间就会达到几百毫秒,但是GC的频率横笛,比如30分钟才触发一次新生代GC,每次停顿500毫秒。根据系统雅策工具 gc日志,内存分析结合考虑。
Mixed GC 如何优化
对于Mixed gc的优化,当老年代在堆内占比超过45%就会触发,新生代进入老年代有几个条件,1.新生代gc过后存活对象太多没法放入Survivor区域,2.对象的年龄太大 3.动态年龄判断规则。
其中动态年龄判断规则和新生代gc过后存活对象无法放入Survivor区域,这两个条件可以让哼多多想快速进入老年代,一旦老年代频繁达到占用堆内存45% 阈值就会频繁触发Mixed gc(混合gc),Mixed gc 优化是避免对象过快的进入老年代,避免频繁的触发Mixed GC,
总结
- 基于JVM运行的系统最怕什么?
系统卡顿,每一次新生代塞满之后,都要停止程序运行,进行垃圾回收
- 新生代gc到底多久一次,对系统影响大不大。
其实不大,新生代gc几乎没有什么好调优的,因为他的运行逻辑非常简单,就是Eden一旦满了,无法放新对象了就会触发一次gc,一般来说新生代调优就只需要给系统分配足够的内存即可,核心点还是在于堆内存的分配、新生代的内存分配内存。新生代采用的复制算法效率极高,因为新生代里存活的对象很少。只要迅速标记这些少量的存活对象,移动动Survivor去,然后会收到其他的垃圾对象即可,速度很快。
- 什么时候新生代的gc,对系统影响很大
当你的系统部署在大内存的机器上的时候。比如32核64g的机器,此时你分配给系统的内存有几十个G,新生代的Eden区可能是30-40g 内存。比如kafka elasticsearch之类的大数据相关系统,此时如果你的系统复杂非常高,对于大数据系统恒有可能是每秒几百万的请求到达kafka、elsticsearch上去,那没可能导致你的eden区几十g内存平凡塞满要出发垃圾回收,假设1分钟会塞满,执行垃圾回收大概需要1s,此时系统每过几分钟就要卡顿几秒钟,有的请求一旦卡死几秒钟就会超时报错。
- 如何解决大内存机器的新生代GC 过慢的问题?
使用G1垃圾回收,针对G1垃圾回收器,可以设置一个期望的每次GC的停顿时间,那么G1基于Region内存划分原理,可以在运行一段时间之后,比如就针对2G内存的Region 进行垃圾回收,此时就仅仅停顿20ms,然后回收掉2G的内存大小空间,腾出来的部分内存,接着还可以继续让子系统运行,G1天生就是和这种大内存的JVM运行,可以解决大内存垃圾回收时间过长的问题。
- 要命的频繁老年代gc问题
对象进入老年代的几个条件,年龄太大了、动态规则判断,新生代gc后活动对象太多无法放入Survivor中,通常如果你的新生代年年中的survivor区域内存过小,就会导致第二个和第三个条件频繁发生,然后导致大量对象快速进入老年代,进而频繁触发老年代的gc,老年代gc通常来说都很消耗时间,无论是CMS垃圾回收器还是G1垃圾回收器,比如CMS垃圾回收器就要经历初始标记、并发标记、重新标记、并发清理、碎片整理几个环节。过程非常复杂,G1同样也是如此。所以一旦jvm内存分配不合理,就会导致老年代频繁gc。
- JVM性能优化到底在优化什么
系统真正的最大问题,就是内存分配、参数设置不合理,导致频繁进入老年代,频繁触发老年代gc,导致系统卡死。
用G1来优化大内存机器的minor gc (young gc) 性能
所以当对这个系统的一个优化,就是采用G1垃圾回收器来对应大内存的Young GC(minor gc)过慢的问题,对于G1设置一个预期的停顿时间,100ms,让G1保证每次YoungGC最多停顿100ms,避免影响终端用户的使用,G1会自动控制好每次Young GC 的时候就回收一部分R region,确保GC停顿时间控制在100ms以内。也许young GC 停顿的频率会更高些,但是每次停顿影响就不打了。
