深入学习JVM:(5) G1垃圾收集器

·  阅读 761

一. 前言

前段时间跳了个槽, 有点儿忙不过来, 所有很长时间没有做技术总结了. 今天接着把落下的知识点补上.

二. G1垃圾收集器简介

G1垃圾收集器是一款革新的产品, 不过也是从CMS收集器身上获得的灵感. 它摒弃了物理分代的概念, 什么是分代? 就是前面文章说的年轻代和老年代了. 那什么又是物理分代呢? 就是真真正正的把一块内存分成两块来使用了, 一个年轻代, 默认占整个堆内存的1/3. 另一个老年代, 默认占整个堆内存的2/3. 更具体的可以查看在下前面的文章.

回到正题, 前面我说G1垃圾收集器摒弃了物理分代的概念, 其实这话在不同的人的眼里理解起来是有偏差的. 所以下边儿还是先看看图:

G1垃圾收集器.png

刚开始看到这个图可能会有点儿蒙, 没关系, 在下来解释一下:

  • 图的最上方呢, 就是我们传统的垃圾收集器的内存图了, 比如之前说过的CMS垃圾收集器等, 大概就是那个样子, 内存被分为了年轻代和老年代.

  • 而下面的图, 就是G1垃圾收集器内存划分的样子. 它被划分成了一个一个的小格子, 这样的一个小格子又称之为一个region. 如果使用G1收集器, 在默认情况下内存会被划分成2048个region, 假设我们的堆内存是2个G, 那么每个region的大小就是1M.

  • 那图上的那么多种类型不同的格子又分别代表什么呢?

之前说的物理分代每个人的理解不同, 就是这里来的. 虽然说是没有分代, 但还是有分代的概念在里边儿. 上方的图中共有五种类型的格子, 分别是: eden区, 幸存区, 老年代, 大对象区(humongous)以及未使用的内存.

  • 前三种如果大家了解过前面的知识的话, 应该会很熟悉, 因为它们在分代的概念中也存在. 那这个大对象区是怎么回事呢? 那是G1收集器将大对象独立出来了, 不再像之前物理分代的时候一样放在老年代中了, 而是单独使用一个分区存放, 这可能是为了节约老年代的空间, 避免频繁触发full gc. 在G1收集器中, 一个对象的大小超过一个region的50%, 就会被认为是大对象. 以上面的例子来说, 一个region大小为1M, 那么一个对象如果占用内存达到512KB, 那么就会被放到大对象区了.

  • 年轻代的堆内存初始占整个堆内存的5%, 会随着系统的运行不断增多, 最多不会超过堆内存的60%.

  • 而对象的分配原则基本和前面几篇文章说过的一样:

  1. 新new出来的对象优先在eden区分配;
  2. 大对象放在大对象区(例外);
  3. young gc后将幸存的对象放到survivor区;
  4. 对象分代年龄达到15(默认)进入老年代
  5. 至于后两条: 对象动态年龄判断和老年代分配担保机制...我的猜测是在eden区增长到最大之后才会生效. 不过具体在下没有找到答案, 如果有知道的小伙伴儿欢迎留言.
  • 另外, G1的内存分配不会永远不变. 我的意思是...比如说有一块内存之前是eden区, 但是随着垃圾的收集, 它会变回未使用状态, 接着它可能又被用来作为幸存区, 或者老年代等等区域.

  • 而至于收集算法...大家可能随便就想到了, 这么多region, 不正是做复制算法的好条件吗?

三. G1垃圾收集器垃圾收集流程

G1的收集流程其实和CMS的差不多, 且先看图:

G1收集流程.png

看过上一篇文章的同学, 应该能看的出这和CMS收集器的收集流程很相似. 灰色箭头代表垃圾收集线程, 白色箭头代表应用程序线程. 下面来详细说说这些流程吧:

  • 第一阶段: 初始标记, 和CMS收集器一样...和图上说的也一样.

  • 第二阶段: 并发标记, 和CMS收集器一样...和图上说的还是一样.

  • 第三阶段: 最终标记, 除了换了个名字, 和CMS仍然一样.

  • 第四阶段: 筛选回收, 和CMS...额, 不一样, 极大的不一样. 这个阶段是G1收集器的骄傲. 为什么说G1收集很强大, 就是因为这一步, 所有的改变很大程度上都是为了这一步, 挥剑千百招, 只为这一刺. 来听我解释:

  • 其实文章开头就应该说明这个特性: G1收集器可以控制垃圾收集的时间, 你配置多少毫秒, 它一次垃圾收集所消耗的时间就是多少毫秒. 当然你肯定不能欺人太甚...给人家1毫秒...

好了, 我还是接着说第四阶段吧, G1收集器在内存中维护了一个优先列表, 记录了回收每个region所需要付出的时间代价并排序. 假如说region A共可以回收垃圾10M, 需要花费20毫秒时间, 而region B共可以回收垃圾50M, 却只需要花费10毫秒时间, 那么region B的排序肯定在region A的前面.

这个算法相当之复杂, 而有了这个排序, G1就可以做到, 你设置多少的回收时间, 它则只回收多少毫秒的垃圾. 比方说设置一次回收时间为200毫秒, G1就会去列表中查找优先级较高且回收时间加起来正好200毫秒左右的那些region回收.

四. G1垃圾收集分类

G1收集器的垃圾回收分类不再像之前的垃圾收集器一般只有young gc和full gc两种. 而是除了上述两种, 还新增了一种叫做mixed gc的种类, 且full gc也不再是之前所说的full gc:

  • young gc: 当年轻代占满之后, G1不会马上执行young gc, 而是检查年轻代是否达到了设置的最大值(默认占用堆内存的60%). 如果没有达到, 则增加新的region, 否则执行young gc.
  • mixed gc: 这个是G1新增的gc分类, 当老年代占用达到45%(默认)时执行, 会回收年轻代, 大对象区, 以及部分老年代(根据回收所需要的时间决定). 主要是使用复制算法将非垃圾对象拷贝至另一个region, 但是收集线程和用户线程是并行的, 所以可能会出现拷贝时已经没有空闲的region可用了. 这种情况十分糟糕, 因为G1接下来会触发full gc.
  • full gc: 和之前说过的其它垃圾收集器不同, G1的full gc和CMS收集器并发失败的情况类似: 停止所有用户线程, 开启单线程对region进行标记清除和压缩整理, 使得能够空出来一些region区域供下一次mixed gc使用.

五. G1垃圾收集器常用参数

有意而为之: 前面没有提到过任何G1收集器的参数, 主要是为了方便看概念, 概念懂了...反正参数这种东西也没人无聊到去记. 而且那么多参数也不一定记得住, 大家都是查文档, 然后复制, 所以单独列在这里:

  • -XX:+UseG1GC: 使用G1收集器

  • -XX:ParallelGCThreads: 指定GC工作的线程数量

  • -XX:G1HeapRegionSize: 指定分区大小(1MB~32MB,且必须是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%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有1024个region,如果有接近500个region都是老年代的region,则可能就要触发MixedGC了

  • -XX:G1MixedGCLiveThresholdPercent(默认85%): region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。

  • -XX:G1MixedGCCountTarget: 在一次回收过程中指定做几次筛选回收(默认8次),在最后的筛选回收阶段可以回收一会儿,然后暂停回收,恢复系统运行,过一会儿再开始回收,这样可以让系统不至于单次停顿时间过长。

  • -XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

  • 以上内容摘自图灵学院

六. 什么场景下适合使用G1垃圾收集器

有人可能会想说, 哎? 这个G1收集器看上去很强大啊, 为啥现在jdk8主流使用的还是parNew + CMS收集器呢? 垃圾收集器的选择是看业务场景的, 虽然越来越多的垃圾器不断面世, 但几乎没有说有哪一个能够适用所有场景. 那什么场景下使用G1呢?

  • 一是内存足够大, 一般是8G以上内存才会推荐使用G1收集器. 这意味着年轻代会占到4-5G左右, 这时, 其它之前说过的收集器在执行young gc的时候, 也会比较耗时.
  • 二是堆中需要存活的对象较多, 可能占到了堆内存的50%左右. 将会很容易触发full gc.
  • 第三, 垃圾收集时间过长

总而言之, 小内存使用ParNew + CMS, 大内存使用G1. 不过没有绝对, 得根据业务情况分析. 如果系统不是高并发类型的系统, 也用不着调优, 全部使用Jvm默认的参数就ok了.

好了, 今天的总结就到这儿, 依然希望有小伙伴儿能指正我写的不对的地方.

分类:
后端
标签: