Java GC 调优

标签: java gc | 发表时间:2013-09-08 08:00 | 作者:
出处:http://darktea.github.com/

关于 Java GC 已经有很多好的文档了, 比如这些:

http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

http://blog.csdn.net/fenglibing/article/details/6321453

但是这里还是想再重点整理一下 Java GC 日志的格式, 可以作为实战时的备忘录.

同时也会再整理一下各种概念

一, JDK 6 提供的各种垃圾收集器

先整理一下各种垃圾收集器.

垃圾收集器:

  • 新生代收集器: Serial, ParNew, Parallel Scavenge (MaxGCPauseMillis vs. GCTimeRatio 响应时间 vs. 吞吐量)
  • 老生代收集器: Serial Old, Parallel Old, CMS

垃圾收集器搭配总结:

  • CMS 只能配 Serial 或 ParNew
  • Parallel Scavenge 只能配 Serial Old 或 Parallel Old
  • Serial 不能配 Parallel Old

组合起来有以下几种:

  1. Serial + Serial Old ( UseSerialGC): GC 线程在做事情时, 其他所有的用户线程都必须停止 (即 stop the world)
  2. Serial + CMS: 一般不会这样配合使用
  3. ParNew + CMS ( UseConcMarkSweepGC): 新生代的 GC 使用 ParNew, 有多个 GC 线程同时进行 Minor GC (主要是在多核的环境用多线程效果会好); 而老生代使用 CMS (CMS 后面会重点讲)
  4. ParNew + Serial Old ( UseParNewGC): 新生代用 ParNew 的时候, 也可以选择老生代不用 CMS, 而用 Serial Old (实际上, 这个组合也不太常用)
  5. Parallel Scavenge + Serial Old ( UseParallelGC): Parallel Scavenge 收集器的目的是达到一个可控制的吞吐率 (适用于各种计算任务); 这个组合中老生代仍旧使用 Serial Old
  6. Parallel Scavenge + Parallel Old ( UseParallelOldGC): 新生代使用 Parallel Scavenge, 而 Parallel Old 是老年代版本的 Parallel Scavenge

这里要注意一下 UseParallelGC vs. UseParallelOldGC, 如果没有调好配置, UseParallelOldGC 有可能比 UseParallelGC 的性能还要差 (参考: http://softwareopolis.blogspot.com/2012/12/jvm-tuning-useparalleloldgc.html)

总结下来, 有 3 种场景:

  • 客户端程序: 一般使用 UseSerialGC (Serial + Serial Old). 特别注意, 当一台机器上起多个 JVM, 每个 JVM 也可以采用这种 GC 组合
  • 吞吐率优先的服务端程序: UseParallelGC (或者 UseParallelOldGC)
  • 响应时间优先的服务端程序: UseConcMarkSweepGC

二, Java 性能调优步骤

预先设定调优目标 -> 部署方案 (单 JVM 部署 vs. 多 JVM 部署) -> 32位 or 64位 -> 优化内存占用 -> 优化响应时间 -> 优化吞吐率

优化需要重点考量的 3 个因素:

  • 内存占用
  • 响应时间
  • 吞吐率

调优过程中的 3 原则:

  • Minor GC 尽可能多的回收新生代对象
  • 只要条件允许, 尽可能开大 JVM 的堆大小
  • 优化中需要重点考量的 3 个因素, 只对其中的 2 个重点调优

三, 内存占用调优

  • 打开 GC 日志

  • Xms 和 Xmx 的值设置一致

  • Xmn 设置为新生代的值

  • 应用运行稳定以后, 观察老生代和永生代所占的空间大小, 即 Live Data Size

利用 Live Data Size 估算调优开始时的各项配置:

  • JVM 堆总大小: 3 - 4 倍 老生代的 Live Data Size
  • 永生代大小: 1.2 - 1.5 倍 永生代的 Live Data Size
  • 新生代大小: 1 - 1.5 倍老生代 Live Data Size
  • 老生代大小: (堆总大小 - 新生代大小), 即 2 -3 倍 老生代 Live Data Size

四, 响应时间调优

调优响应时间可能的方法:

  • 优化程序
  • 调整部署 (单 JVM 或者 多 JVM)
  • 减小对象分配, 降低 GC 频率

调优步骤:

监控 Minor GC 响应时间频率 -> 调整新生代大小:

  • Minor GC 频率高 -> 增大新生代
  • Minor GC 时间长 -> 减小新生代

监视 Full GC 响应时间频率 -> 调整老生代大小:

  • 老生代空闲空间多长时间被填满决定了 Full GC 的频率 (空闲空间 = 老生代总空间 - Live Data Size)
  • 填满空闲空间的速度由提升率决定
  • 例如, 每次 Minor GC 提升 20M, 200M 的空闲空间 10 次 Minor GC 后会被填满
  • 如果发现 Full GC 过于频繁, 增大老生代大小 (同时保持新生代大小不变)
  • 但增大老生代大小会增加 Full GC 的时间. 所以需要对老生代大小进行调优. 如果调不好, 可以考虑用 CMS 替代 PS

五, CMS

CMS 的细节可以参考: http://blog.griddynamics.com/2011/06/understanding-gc-pauses-in-jvm-hotspots_02.html

这里只着重整理一下调优相关的东东.

  • CMS 可以不做 compacting, 所以可能避免 stop-the-world 的 compacting 过程
  • 调优 CMS 的目的, 就是避免 stop-the-world 的 compacting 过程发生 (实际很难做到)
  • CMS 可以和应用程序线程并行的运行, 也就是在应用程序运行的同时回收老生代空间, 避免老生代用完
  • 但一旦老生代真的用完了, 就会发生恐怖的 stop-the-world 的 compacting GC
  • 从 PS 迁移到 CMS 时, 最好先把老生代的大小扩大 20% - 30%
  • 老生代不做 compacting, 所以老生代的内存碎片率特别关键. 有 2 个方法可以减轻内存碎片率: 1) 增大内存 2) 优化新生代到老生代的提升率

1. PrintTenuringDistribution

先介绍一下对象 age 的概念. JVM 中的一个对象新被创建时 age 是 0; 之后每次 Minor GC 后, 这个对象如果还在新生代中, 这个对象的 age 数加一.

通过一个调优例子来说明 PrintTenuringDistribution 的用法

打开 JVM 的参数:

-XX:+PrintTenuringDistribution

这样 gc.log 会有 Tenuring 相关的信息. 例如:

Desired survivor size 8388608 bytes, new threshold 1 (max 15)
- age 1: 16690480 bytes, 16690480 total

  • threshold 1 表示对象在新生代中存活的阈值是 1. 也就是说, 一旦一个对象的 age 大于 1, 这个对象会晋升到老年代.
  • age 1: 16690480 bytes: 表示在新生代中年纪是 1 的对象大小共有 16690480 bytes
  • Desired survivor size 8388608 bytes 表示当前时刻, 需要的目标 survivor 空间为 8388608 bytes

简单点说, 从这个输出, 我们发现, 当前时刻 survivor 目标空间只有 8388608 bytes, 而小于实际占用的 16690480 bytes 的空间. 因此, 我们需要扩充 survivor 空间的大小.

需要把 survivor 空间扩大到多大呢?

需要 16690480 bytes 的空间来装入所有 age 为 1 的对象. 而 CMS 会用一个比率来估算需要分配多少 survivor 目标空间 (这个比率的默认值是 50%). 所以, survivor 空间需要扩充到: 16690480 / 50% = 33,380,960 bytes

结论: 把 survivor 空间的大小扩容到 33,380,960 bytes (大约 32M) 是合适的

这里再详细说明一下前面提到的 CMS 用来估算 survivor 目标空间占用的那个比率.

这个比率默认值是 50 (代表50%). 可以通过设置 JVM 的参数来配置, 例如:

-XX:TargetSurvivorRatio=90

不过一般来说, 极少情况需要配置这个值, 默认的 50 就 OK 了.

总之, 我们通过打开 PrintTenuringDistribution 获取更多的 GC 信息来优化对象从新生代到老生代的提升率, 以及优化 Minor GC 的响应时间.

2. 避免老生代的 stop-the-world

要避免老生代的 stop-the-world 就是要保证 CMS 回收的内存的速度比从新生代晋升到老生代的速度快. 这样才能保证老生代不被填满而造成 stop-the-world.

要保证 CMS 回收的速度, 要靠两个因素来保证:

  • 老生代越大, 发生 stop-the-world 的概率越低
  • CMS 开始的时机越早, 发生 stop-the-world 的概率越低

对第一个因素, 我们尽量给老生代分配更多的空间就行了.

下面详细说说第二个因素.

GC 参数中有一个:

-XX:CMSInitiatingOccupancyFraction

默认值是68. 其含义是, 每次当老生代的总空间的 68% 被占用的时候, 就进行 CMS.

这个值调得越小, CMS 就会越早发生, 发生 stop-the-world 的概率就会更小;

这个值调得越大, CMS 就会越晚发生, 发生 stop-the-world 的概率就会更大.

所以我们可以通过调节这个值来调节 CMS 发生的时机, 避免 stop-the-world 的发生. 但是也不能调的太小了, 太小了的话, 会触发不必要的 CMS, 降低了吞吐率

另外需要注意, -XX:CMSInitiatingOccupancyFraction 要和 -XX:+UseCMSInitiatingOccupancyOnly 参数一起使用才会生效

六, 吞吐率

在响应时间调好以后, 可以考虑调吞吐率. 这里主要讲一下 CMS 吞吐率的调优方法:

  • 增加新生代的大小
  • 增加老生代的大小
  • 调整 eden 和 survivor 的比率
  • 调整 CMSInitiatingOccupancyFraction 参数

注意: 以上 4 个调整方法需要和响应时间的调整做 trade off

七, 其他一些可以考虑调整的 JVM 参数:

  • -XX:PermSize
  • -XX:MaxPermSize
  • -XX:+PrintGCDetails
  • -XX:+PrintGCDateStamps
  • 打开或关闭 DisableExplicitGC
  • -XX:+PrintTenuringDistribution
  • -XX:+CMSParallelRemarkEnabled
  • -XX:CMSMaxAbortablePrecleanTime
  • -XX:ParallelGCThreads=
  • -XX:+UseNUMA
  • -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
  • -XX:+CMSScavengeBeforeRemark

另外, 需要特别注意 -XX:+ParallelRefProcEnabled 这个参数的使用, 在有些 JDK 版本, 设置了这个参数会导致程序 hang 住 (具体参考: http://www.codelve.com/archives/145 )

八, Reference

  • http://hllvm.group.iteye.com/group/topic/27945
  • << Java Performance >> Charlie Hunt, Binu John

相关 [java gc] 推荐:

Java GC 调优

- - Darktea
关于 Java GC 已经有很多好的文档了, 比如这些:. 但是这里还是想再重点整理一下 Java GC 日志的格式, 可以作为实战时的备忘录.. 同时也会再整理一下各种概念. 一, JDK 6 提供的各种垃圾收集器. 先整理一下各种垃圾收集器.. 新生代收集器: Serial, ParNew, Parallel Scavenge (MaxGCPauseMillis vs.

Java GC日志查看

- - Java - 编程语言 - ITeye博客
Java中的GC有哪几种类型. 虚拟机运行在Client模式的默认值,打开此开关参数后,. 使用Serial+Serial Old收集器组合进行垃圾收集. 打开此开关参数后,使用ParNew+Serial Old收集器组合进行垃圾收集. 打开此开关参数后,使用ParNew+CMS+Serial Old收集器组合进行垃圾收集.

面向GC的Java编程

- - 并发编程网 - ifeve.com
Java程序员在编码过程中通常不需要考虑内存问题,JVM经过高度优化的GC机制大部分情况下都能够很好地处理堆(Heap)的清理问题. 以至于许多Java程序员认为,我只需要关心何时创建对象,而回收对象,就交给GC来做吧. 甚至有人说,如果在编程过程中频繁考虑内存问题,是一种退化,这些事情应该交给编译器,交给虚拟机来解决.

Java GC 调试手记

- - 非技术 - ITeye博客
本文记录GC调试的一次实验过程和结果. 问题1:为什么要调试GC参数. 在32核处理器的系统上,10%的GC时间导致75%的吞吐量损失. 所以在大型系统上,调试GC是以小博大的不错选择. 问题2:怎么样调试GC?. 调试GC,有 三个主要的参数:. 选择合适的GC Collector. 整个JVM Heap堆的大小.

探秘Java虚拟机 gc的监控

- - 编程语言 - ITeye博客
1、Java虚拟机运行时的数据区. 2、常用的内存区域调节参数. -Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制. -Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制.

Java GC的工作原理详解

- - Java - 编程语言 - ITeye博客
JVM学习笔记之JVM内存管理和JVM垃圾回收的概念,JVM内存结构由堆、栈、本地方法栈、方法区等部分组成,另外JVM分别对新生代和旧生代采用不同的垃圾回收机制. 首先来看一下JVM内存结构,它是由堆、栈、本地方法栈、方法区等部分组成,结构图如下所示. JVM学习笔记 JVM内存管理和JVM垃圾回收.

Java垃圾回收GC概览

- - 掘金后端
    这部分的内容可以说是重中之重了,有过C/C++开发工作的人应该知道内存管理的重要性和难度. 虽然Java自己实现了内存管理,不用开发人员去操心,但其内存管理还是有不足之处,常常也会出内存泄漏和内存溢出等问题. 当我们进行这些问题排查的时候,没有掌握相关的JVM内存管理知识,那就是盲目,没有方向,掌握这部分知识在解决问题的时候才能有所依据.

成为Java GC专家(5)—Java性能调优原则

- - ImportNew
这是“ 成为Java GC专家”系列的第五篇文章. 在第一篇 深入浅出Java垃圾回收机制中,我们已经学习了不同的GC算法流程、GC的工作原理、新生代(Young Generation)和老年代(Old Generation)的概念. 你应该了解了JDK7中5种GC类型以及各种类型对应用程序的影响.

编写内存效率的java代码-面向GC

- - 非技术 - ITeye博客
Java程序员在编码过程中通常不需要考虑内存问题,JVM经过高度优化的GC机制大部分情况下都能够很好地处理堆(Heap)的清理问题. 以至于许多Java程序员认为,我只需要关心何时创建对象,而回收对象,就交给GC来做吧. 甚至有人说,如果在编程过程中频繁考虑内存问题,是一种退化,这些事情应该交给编译器,交给虚拟机来解决.

从实际案例聊聊Java应用的GC优化

- - 美团点评技术团队
当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化. 但GC算法复杂,影响GC性能的参数众多,且参数调整又依赖于应用各自的特点,这些因素很大程度上增加了GC优化的难度. 即便如此,GC调优也不是无章可循,仍然有一些通用的思考方法. 本篇会介绍这些通用的GC优化策略和相关实践案例,主要包括如下内容:.