Java GC 调优
序
关于 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
组合起来有以下几种:
- Serial + Serial Old ( UseSerialGC): GC 线程在做事情时, 其他所有的用户线程都必须停止 (即 stop the world)
- Serial + CMS: 一般不会这样配合使用
- ParNew + CMS ( UseConcMarkSweepGC): 新生代的 GC 使用 ParNew, 有多个 GC 线程同时进行 Minor GC (主要是在多核的环境用多线程效果会好); 而老生代使用 CMS (CMS 后面会重点讲)
- ParNew + Serial Old ( UseParNewGC): 新生代用 ParNew 的时候, 也可以选择老生代不用 CMS, 而用 Serial Old (实际上, 这个组合也不太常用)
- Parallel Scavenge + Serial Old ( UseParallelGC): Parallel Scavenge 收集器的目的是达到一个可控制的吞吐率 (适用于各种计算任务); 这个组合中老生代仍旧使用 Serial Old
- 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