JVM GC 调参 AND 调优

分代收集

Java 中在一小段代码中大量的对象被快速地创建和丢弃,这种操作是非常普遍的。

垃圾收集器设计时特别考虑要处理大量(有时候是大多数)临时对象,这是分代设计的初衷之一。

  • 新生代
    • 对象首先在新生代中分配。新生代填满时,垃圾收集器会暂停所有的应用线程,回收新生代空间。不再使用的对象会被回收,仍然在使用的对象会被移动到其他地方。这种操作被称为 Minor GC,又称 Young GC。
    • 采用这种设计有两个性能上的优势
      • 新生代仅是堆的一部分,与处理整个堆相比,处理新生代的速度要更快,也就意味着应用线程停顿的时间会更短
      • 新生代分为 Eden 区 和两个 Survivor 空间,新生代的对象起初都分配在 Eden 区,垃圾收集时,Eden 区的对象要么被回收,要么就被移动到老年代,要么被移动到 Survivor 空间(两个 Survivor 空间会来回倒换不能被回收的对象,当超过一定的倒换次数,才会被挪到老年代),这样相当于给新生代做了一次压缩整理
  • 老年代
    • 新生代回收之后的依然存活的对象不断的挪到老年代,老年代就会被填满。JVM 就需要对整个堆进行垃圾回收,这个过程被成为 FullGC。

GC 算法

  1. Serial 垃圾收集器
    • 使用单线程清理堆的内容。
    • 无论是 Minor GC 还是 Full GC,清理堆空间时,所有线程都会被暂停。
    • 进行 Full GC 时,还会对老年代空间的对象进行压缩整理。
    • 通过 -XX:+UseSerialGC 标志可以启用 Serial 收集器
  2. Parallel 垃圾收集器,也叫 Throughput 收集器
    • 多线程回收新生代和老年代,Minor GC 的速度要比 Serial 收集器快的多。
    • Minor GC 和 Full GC 的时候,会暂停所有线程,Full GC 也会对老年代进行压缩整理
    • 通过 -XX:+UserParallelGC -XX:UseParallelOldGC 启用 Parallel 收集器
  3. CMS 收集器
    • CMS 设计初衷是为了降低 Full GC 周期中的长时间停顿。
    • Minor GC 会暂停所有线程,并以多线程的方式进行垃圾回收。
    • 使用若干个后台线程定期的对老年代空间进行扫描,通过 标记,清除等操作及时回收其中不再使用的对象。这种算法使 CMS 成为一个低延迟的收集器,应用线程仅在 Minor GC 以及后台扫描老年代时发生极其短暂的停顿,比起 Parallel 停顿总时长要短的多
    • 额外付出的时更高的 CPU 使用,CMS 要求必须要有足够的 CPU 资源用于运行后台的垃圾收集线程
    • 回收线程不进行任何压缩整理的工作,这意味着堆会逐渐变得碎片化。如果 CMS 的后台线程无法获得任务所需的 CPU 资源,或者堆变得过度碎片化以至于无法找到连续空间分配对象,CMS 就会蜕化到 Serial 收集器的行为:暂停所有应用线程,单线程回收,整理老年代空间。之后有恢复到并发运行,再次启动后台线程(直到下一次堆变得过度碎片化)
    • 通过 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 启用 CMS 收集器
  4. G1 垃圾收集器
    • G1 设计初衷是为了尽量缩短处理超大堆(大于 4GB)时产生的停顿。
    • G1 算法将堆划分为若干个区域(Region),不过它依然属于分代收集器。利用多线程的方式来收集新生代和老年代
    • G1 算法属于 Concurrent 收集器。老年代的垃圾收集工作由后台线程完成,不需要暂停应用线程
    • G1 将老年代划分成不同的区域(Region),通过将对象从一个区域复制到另一个区域,完成对象的清理工作,也同时实现了堆的压缩整理,所以 G1 收集的堆不太容易发生碎片化
    • 同 CMS 一样,避免 Full GC 的代价就是消耗额外的 CPU 周期
    • 通过 -XX:+UseG1 启用 G1 收集器

GC 算法选择

  • 如果应用线程可以最大程度的利用 CPU,使用 Parallel 通常能获得更好的性能
  • 如果应用线程没有充分利用 CPU,则使用 Concurrent 往往能获得更好的性能
  • 选择 Concurrent ,如果堆较小,推荐使用 CMS ,G1 的设计使得它能够在不同的分区(Region)处理堆,因此 G1 比 CMS 更易于处理超大堆的情况

GC 调优基础

  • 调整堆的大小

    • 不要将堆的容量设置得比机器的物理内存还大,通常情况下,至少要预留 1G 的内存空间
    • 尽量通过调整 GC 算法进行调优,而不是调整堆的大小来改善程序性能
  • 代空间的调整

    • 整个堆的范围内,不同代的大小划分是由新生代所占用的空间控制的
    • -XX:NewRatio=N 设置新生代与老年代的空间占用比率
    • -XX:NewSize=N 设置新生代空间的初始大小
    • -XX:MaxNewSize=N 设置新生代空间的最大值
    • -XmnN 将 newSize 和 MaxNewSize 设定为同一个值的快捷方法
  • 永久代和元空间的调整

    • Jdk7 中的永久代和 jdk8 中的元空间保存着类的元数据
    • 永久代或元空间耗尽也会触发 Full GC,这时老的元数据会被丢弃回收
  • 控制 GC 并发线程的数量

    • 几乎所有的垃圾收集算法的回收线程数都依据机器上的 CPU 数据算出
      N 为 CPU 的数目,每个 CPU 最多同时运行 8 个线程

      $$ParallelGCThreads = 8 + ((N - 8) * 5 / 8)$$

  • JVM 自适应调整

    • JVM 默认会自适应的调整新生代和老年代的百分比
    • 对于已经精细化调优过的堆,使用 -XX:-UseAdaptiveSizePolicy 可以在全局范围内关闭自适应调整
  • 垃圾回收工具

    • -verbose:gc 或 -XX:+PrintGC 这两个标志中的任意一个都能创建基本的 GC 日志
    • 使用 -XX:PrintGCDetails 可以创建更详细的 GC 日志
    • 使用 -XX:+PrintGCTimeStamps 或 -XX:+PrintGCDateStamps 可以更精确的判断几次 GC 操作之间的时间
    • 使用 jstat -gccause PID 1000 10 可以监控应用程序的垃圾回收过程

参考资料

Java 性能指南