深入探讨 Java JVM 垃圾收集器

本贴最后更新于 1982 天前,其中的信息可能已经沧海桑田

Java GC 收集器介绍

  目前在用的 JDK 版本基本是 6、7 和 8,通过 java -XX:+PrintCommandLineFlags -version 命令,可以查看当前 jvm 默认的命令行,里面有默认的 gc 收集器,6、7、8 默认的 gc 回收器都是
-XX:+UseParallelGC,这个参数代表是分代收集,young 区是用的 parallel scavenge,old 区是用的 serial old。注意:-XX:+UseParallelOldGC,才是开启 parallel scavenge+parallel old 的收集器组合。
  基于分代收集的原则,目前一共有 7 个可用的垃圾回收器,分别对应 young 区和 old 区,对应关系如下图所示:
JVMpng
  下面一一介绍这几种垃圾收集器。

Young 区收集器

  Serial:最原始,最古老的收集器,单线程工作,有着“Stop The World”的“美誉”,也就是说这个垃圾收集器在进行垃圾回收操作的时候,会将所有用户线程全部暂停,这个就是最开始很多人不喜欢 java 的原因。不过这个垃圾回收器以及变成历史,几乎没有再用的地方(比较适合单核的服务器,现在去哪里找呢?)。其工作模式如下所示:
serialpng
  ParNew:Serial 收集器的多线程版本,工作原理跟 Serial 差不多,由单线程变成了多线程。如下图所示:
ParNewpng
注意:目前只有 Serial 和 ParNew 两个新生代垃圾收集器能跟老年代垃圾收集器 CMS 组合使用,因此多线程版本的 ParNew+CMS(+Serial Old)是一个常用的组合。

  Parallel Scavenge:多线程并行收集器,跟另一个多线程收集器 ParNew 的设计理念不用,Parallel Svavenge 主要目的是控制 JVM 的吞吐量(吞吐量=用户代码执行时间/(用户代码执行时间 +GC 耗时)),可以通过配置-XX:MaxGCPauseMillis 来控制最大的单次 GC 耗时,配置-XX:GCTimeRatio 来直接设置吞吐量大小(GCTimeRatio 默认值是 99,即允许 JVM 运行时间的 1% 为垃圾回收的耗时,这个不是绝对,只是尽最大可能去保障这个值)。Parallel Scavenge 有个 JVM 参数值得注意,-XX:+UseAdaptiveSizePolicy,可以让虚拟机参数**-Xmn 新生代大小、EdenS0S1、晋升老年代年龄(-XX:PretenureSizeThreshold**)由系统自动根据运行情况来调节,也就是 GC 自适应的调节策略。Parallel Scavenge 是 JDK6、7、8 默认的新生代垃圾收集器(JDK9 默认 G1)。

Old 区收集器

  Serial Old:是新生代收集器 Serial 的老年代版本,工作原理跟 Serial 一样,目前是 JDK6、7、8 默认的老年代垃圾收集器。目前默认与新生代收集器 Parallel Scavenge 共同使用。因为 Serial Old 是采用标记-整理算法来回收内存,因此 Serial Old 还是 CMS 收集器的备用收集器。
  Parallel Old:Parallel Scavenge 的老年代版本,基于多线程和标记-整理算法实现,原理跟 Parallel Scavenge 一样,也是以 JVM 吞吐量控制为主的垃圾收集器,可以结合 Parallel Scavenge 共同使用。Parallel Scavenge+Parallel Old 组合适合注重 JVM 吞吐量、以及 CPU 资源敏感的场景。
  CMS:英文全名叫 Concurrent Mark Sweep,中文翻译叫并发标记清除,设计这款垃圾回收器的最主要目的是让垃圾回收的暂停时间最短,比较适合对响应速度有要求的场景,所以这个是牺牲吞吐量,换取最小暂停时间的垃圾收集器。这款收集器有个弊端,就是基于标记-清除为垃圾收集算法来实现的,在执行多次 GC 过后,会产生大量不连续的内存碎片,有可能导致对象无法分配,这时候就需要基于标记-整理算法实现的 Serial Old 垃圾收集器再执行一次 Full GC,将内存碎片整理一下。可以通过-XX:+UseCMSCompactAtFullCollection 开关参数,让 CMS 在执行完 Full GC 后进行内存整理,-XX:CMSFullGCsBeforeCompaction 参数来控制执行多少次 full gc 后,进行一次带内存压缩的 gc。
  CMS 工作过程目前一共分为四个阶段:
  初始标记:CMS initial mark,仅仅标识一下 GC root 能关联到的对象,速度非常快。
  并发标记:CMS concurrent mark,GC root tracing 的一个过程,标记出哪些对象 root 不可达,即为需要回收的对象,非常耗时。
  重新标记:CMS remark,由于并发标记是跟用户线程同时执行,重新标记是为了确保用户线程执行的时候没有改变并发标记的那些对象的状态,这里只需要判断用户线程当时改动过的小部分对象即可,速度很快。
  并发清除:CMS concurrent sweep,清除标记的垃圾对象,过程非常耗时。
  重置状态:CMS concurrent reset,并发重置状态等待下次 CMS 的触发。
  由于非常耗时的两个阶段,并发标记和并发清除,都是跟用户线程同时执行的,因此整个 gc 过程中,暂停时间是非常短的,但是,这里牺牲了 JVM 的吞吐量换来的,对于 CPU 敏感的系统来说,CMS 可能并不合适。CMS 的工作原理图如下所示:
CMSpng

G1 垃圾收集器

  G1 垃圾收集器是 JDK7 的时候正式发布的,基于标记-整理算法实现的垃圾收集器,能够很精确的控制垃圾回收的暂停时间。这个垃圾收集器是分块收集,跟之前的分代收集有所不同,没有老年代、新生代之分,G1 把堆内存分成 N 个大小固定的块,每次根据允许的垃圾回收时间,优先回收垃圾最多的块。JDK9 以后,G1 成为默认的垃圾收集器。
  之前做过一个 POC,分析了一下 GC 相关的参数和性能,压测的数据可以跟大家分享一下。两次压测都是在 JDK1.8 64bit 的环境下压测的,堆内存都是为 80G。

JVM 默认垃圾收集器 parallel scavenge+serial old

pssopng
psso1png
psso2png

G1 垃圾收集器

g1png
g12png
g13png
  从上面的图中可以看到,分代收集和分块收集的不同之处。分代收集里面,old 区的内存比较大,full gc 耗时比较长,其中 max full gc time 达到了 280+ms,可以想象一下一个高并发实时应用,突然暂停 280ms 是啥影响,但是分代收集的 young gc(mirror gc,新生代 gc)耗时很短,而且分代收集主要的 gc 还是消耗在 young gc 上面。G1 是属于分块收集,可以看到 gc 分析结果中,并没有 young gc 和 full gc 之分,分块收集并不像分代收集划分得那么明确,每个块都差不多,不会有太大的波动(gc 时间的方差比分代收集小多了),没有分代收集的 full gc 那么大的峰值,但是也没有 young gc 那么短的暂停,但是从数据中可以看到,两者的 JVM 吞吐量是相当的,G1 并没有增加 cpu 对业务线程的执行压力。

JVM 启动参数之垃圾回收相关参数

参数 描述
-XX:+UseSerialGC JVM 在 client 模式下的默认值,开启此开关后,JVM 将使用 Serial+Serial Old 的组合进行垃圾回收
-XX:+UseParNewGC 开启此开关后,采用 ParNew+Serial Old 的组合进行垃圾回收
-XX:SurvivorRatio 分代收集中,新生代的 Eden 与 survivor 区域的比例,默认为 8,即 8:1:1
-XX:PretenureSizeThreshold 直接晋升到老年代的对象大小,超过此大小的对象,直接分配到老年代,默认 0,即不管多大都先分配在 eden
-XX:MaxTenureThreshold 晋升到老年代的对象年龄,如果一个对象在新生代,一次回收没有收掉,那么年龄 +1,默认 15(最大 15),年龄超过这个数,则对象进入老年代

  Parallel GC 相关参数

参数 描述
-XX:+UseParallelGC JVM 在 Server 模式下的默认值,将采用 Parallel Scavenge+Serial Old 组合进行垃圾回收
-XX:+UseParallelOldGC 使用 Parallel Scavenge+Parallel Old 组合进行垃圾回收,适合对 JVM 吞吐量有要求,cpu 敏感的场景
-XX:+UseAdaptiveSizePolicy 动态调整各个 JVM 各个区域的大小,以及对象进入老年代的年龄
-XX:ParallelGCThreads 并行 GC 时候的线程数
-XX:GCTimeRatio JVM 吞吐量设置,默认 99
-XX:MaxGCPauseMillis GC 的最大暂停时间

  CMS 相关参数

参数 描述
-XX:+UseConcMarkSweepGC 采用 ParNew+CMS+Serial Old 的组合进行垃圾回收,Serial Old 作为 CMS 备用,在发生 Concurrent Mode Failure 失败后使用
-XX:CMSInitiatingOccupancyFraction 设置 CMS 在老年代多少空间被使用后出发垃圾回收,默认是 68%
-XX:+UseCMSCompactAtFullCollection 在 CMS 垃圾回收后,是否进行一次碎片整理
-XX:CMSFullGCsBeforeCompaction 在 CMS 垃圾回收若干次后,再进行一次碎片整理

这里有一片关于 JVM GC 的文章,写得非常好,还有一些实战经验,值得一看 https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html

  • GC
    17 引用 • 45 回帖
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 2 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3167 引用 • 8207 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...