参考资料:
Step_by_Step_GC_Tuning_in_the_HotSpot_Java_Virtual_Machine :Java One 大会演讲PPT(相当于下面官方文档的简化版,本笔记的主要来源)
Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning:Oracle Jdk6 调优(推荐看下面的 JDK8 版本,内容更详实)
Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide:Oracle jdk8 调优(增加了G1的内容)
简介:
GC 表现的3个属性:
- 性能
- 延迟
- 内存占用
GC 调优的3个原则:
- 内存越多越好
- 每次gc回收的越多越好(往往意味着 GC 性能高)
- 针对 GC 表现的3个属性中的2个进行调优
方法:
基础:
- 测量 GC 表现的3个属性
- 测量实际负载下的实际情况(关注线上的gc日志)
- 对应用程序/第三方库,框架/jdk,jvm/硬件/操作系统有足够了解
gc日志:
- 实际线上日志
- 将应用级别和jvm级别的时间进行关联
- 参数:
-
- -XX:+PrintFCTimeStamps -XX:PrintGCDetails -Xloggc:<file name>
- -XX:+PrintFcDateStamps (from JDK6u4)
- -XX:+PrintHeapAtGC (更多细节信息,更多输出的代价)
JVM选择:
- 32-bit
- 64-bit
-
- -XX:+UseCompressedOops 压缩普通指针,heap最大26G,CPU换内存
- 非压缩,heap unlimited
- 32-bit --> 64-bit
-
- heap内存额外增加20%左右
- 性能损失20%左右(详细查找的资料如下)
-
- 64bit JVM相比32bit JVM,在大量的内存访问的情况下,其性能损失更少,AMD64和EM64T平台在64位模式下运行时,Java虚拟机得到了一些额外的寄存器,它可以用来生成更有效的原生指令序列。
- 性能上,在SPARC 处理器上,当一个java应用程序从32bit 平台移植到64bit平台的64bit JVM会用大约 10-20%的性能损失,而在AMD64和 EM64T平台上,其性能损失的范围在0-15%.
GC选择:
- 第一原则
-
- 低延迟比性能重要-->使用CMS
-
- 例外情况:heap<1GB-->ParallelOldGC可能能够满足低延迟要求
- 延迟没有性能重要-->使用ParallelOldGC
- 方法:
-
- 从ParallelOldGC开始,并观测GC表现
- 如果需要的话,转而使用CMS
总体方法:
- 观测
- 优化
- 重复以上2个步骤
方法-->内存占用:
计算存活数据大小(LDS,Live Data Size):
- 确保稳定状态有Full GC
- 使用工具:
-
- JConsole/VisualVM,连上JVM后,点击"Perform GC"
- jmap,jmap -histo:live <pid>
- System.gc(),仅用于测试环境
- 从 GC log 能够获得:
-
- LDS的近似值(Full GC 后的内存占用)
- 最大永久代的近似值(Full GC 后的永久代大小)
- Full GC 导致的最大延迟
初始 heap 配置:
- 了解LDS之后,可以对heap设置一个有理由的大小
-
- 第一原则
-
- heap 大小为3x~4x LDS
- -XX:PermSize 和 -XX:MaxPermSize 设置为 1.2x~1.5x 最大永久代的近似值
- 设置分代大小
-
- 第一原则
-
- 新生代 1x~1.5x LDS
- 老年代 2x~3x LDS
- 举例来说,新生代应该是heap的1/4~1/3
heap size 不能说明一切
- 监听进程的内存占用:prstat/top/Task Manager
- heap size 不一定是最大的贡献者:本地库(c/c++),I/O buffers,线程的方法栈等
- 要考虑 heap size 以外的内存占用,并适当调整内存策略
内存占用:总结
- 应用可能无法在给定的内存的情况下运行
- 可能需要应用级别的修改
-
- 使用更有效的数据结构
- 减少输入的大小
- 等等
- 如果 heap size < 1.5 LDS
-
- 太紧了,GC没有呼吸空间
- GC会很频繁,并摧毁应用程序
方法-->延迟:
延迟需求:
- 停顿时间多久?
-
- GC平均停顿时间目标
- GC最大停顿时间目标
- 如何忍受频繁的妨碍(GC)
- 停顿频率
-
- GC停顿频率目标(近似于应用程序的停顿频率目标)
- 重要性通常低于停顿时间目标
优化新生代:
- 监视新生代GC次数
-
- GC引起的延迟的最频繁的源头
- 时长和频率并重
- 如果新生代GC时间过长-->减小新生代大小
-
- 通常能够减小新生代GC时间
- 新生代GC更频繁
- 如果新生代GC太频繁-->增大新生代大小
-
- 通常会加长新生代GC时间
- 新生代GC性能最好的情况-->新生代占堆的80%,但是这样的设计不合理
- 优化新生代大小方法
-
- 保持老年代大小不变
- 增大新生代大小(例如增大1G)
- 老年代不应该小于1.5 LDS
- 太小的新生代将会产生负面效果
-
- 非常频繁的新生代GC
- 通常,新生代的大小不小于 heap 的 10%
最坏的场景-> Full GC:
- 如果 Full GC 太频繁
-
- 在新生代大小不变的情况下,调大老年代大小
- 如果 Full GC 太久
-
- 在 ParallelOldGC 的情况下没有办法优化,调整老年代大小没有明显效果
- 切换到ParNew+CMS或G1进行尝试
Full GC 频率:
- 得到 Full GC 频率的办法:
-
- 查看 Full GC 的日志
- 计算平均每秒新生代数据进入老年代的数量,并不是很准
- 老年代和LDS的比值,并不是很准
延迟:总结:
- 新生代 GC 过长是很有可能的
- 此种情况很难调优,应该是机器CPU不够为主要原因
- 尝试:
-
- 优化应用程序,减少中间存活对象
- 使用单个内存更小的JVM集群代替单个 heap size 过大的 JVM,深入JVM 这本书有提到,使用nginx代理做JVM集群(tomcat?),技术要求高
GC 决策:
- 新生代和老年代 GC 时间,频率都OK,使用默认的Parallel/ParallelOld
- GC 时间 OK(应该是CPU性能足够),单次 GC 过长或GC频率过高?这种情况应该调大老年代吧,使用CMS
- 新生代 GC 时间过长,考虑应用级别优化(提升CPU性能应该有帮助)
方法-->ParallelOldGC 调优:
ParallelOldGC 需求:
- 性能目标 :应用层测量(压测)
ParallelOldGC 优化:
- 动态适应大小
-
- -XX:+/-UseAdaptiveSizePolicy(默认开启)
- 主要是为了增强易用性(不需要主动优化)
- 如果需要追求最后的10%性能,可以关闭
-
- 应用的表现很稳定(压力稳定),不需要
- 对表现复杂的应用更有帮助
- 更高的性能-->增加新生代/永久代大小
-
- 典型地,永久代对性能的影响更大
- 主要内存占用和延迟
- -XX:+UseNUMA:需要硬件开启对VM的支持,且此功能仅支持ParallelOldGC
-
- 提升非统一内存架构机器性能
ParallelOldGC总结:
- 无法达到期望的性能是可能的
-
- ParallelOldGC的性能是所有GC中最好的(不包含G1)
- 为了进一步优化,需要对应用层进行修正
- ParallelOldGC占CPU时间的天花板应该 < 5%,通常 < 1%
方法-->CMS 调优:
CMS需求:
- GC停顿时间目标
- GC停顿频率目标
- 性能目标
迁移到CMS(和ParallelOldGC的区别):
- 通常老年代没有压缩(标记-清除算法,会有内存碎片,可以指定每<n>次 Full GC 后进行内存碎片整理,详见 JVM GC 基础知识)
-
- 潜在的内存碎片化
- 仅 Full GC 进行内存压缩
- 老年代 增大 20% ~ 30%
-
- 因为碎片化/更繁琐的并发GC循环(和用户线程并发,需要为新生代 GC 预留内存)
- 更长的新生代 GC 时间
-
- 因为对象进入老年代更慢
- 更好的最差延迟
-
- 大部分工作都是和用户进程并发进行的
- 更短的最差停顿时间
- 更低的性能
-
- CMS做的工作更多
老年代阈值优化:
- 阈值决定对象在新生代待的时长
-
- 最大老年代阈值(Max Tenuring Threshold, MTT),决定对象经历的新生代 gc 的次数
- 老年代阈值自动优化(和 survivor 的剩余空间有关),不会超过 MTT
- -XX:MaxTenuringThreshold=<n>
-
- <n>最小为1,不超过15,(JDK 5u6 之前,最大 31)
- 更高的老年代阈值(MTT 调大,survivor 内存调大)-->进入老年代的对象更少
-
- 新生代 GC 时间通常更长(复制的内存更多)
- 新生代回收内存更多,整体效率更高
- 更低的老年代阈值( MTT 调小,survivor 内存调小)-->进入老年代的对象更多
-
- 新生代 GC 时间通常更短(复制的内存更少)
- 老年代负载更大
- 内存更可能碎片化(仅CMS会使内存碎片化,需尽量减少此情况)
幸存区大小优化:
- 幸存区不应该溢出
- -XX:TargetSurvivorRatio=<percent>
-
- 目的幸存区(to)仅在新生代 GC 之后使用
- 使用[50%,90%],少于突发负载情况
- 可以通过 GC log 或 tenuring distribution (分代信息) 进行监视
- 如果幸存区溢出
-
- 调大幸存区比例,-XX:SurvivorRatio
- 降低MTT(默认15,JVM 通常会自动调整TT,所以一般不会发生溢出)
CMS 阈值优化:
- 决定何时开始并发 GC 循环(由于并发进行 GC,需要预留老年代空间给新生代 GC)
-
- 基于老年代 GC 表现
- -XX:CMSInitiatingOccupancyFraction=<percent>(默认90)
-
- 阈值应该超过LDS
-
- 至少1.5倍LDS(否则回收的垃圾太少,且 CMS GC 极易触发,导致 CMS GC 频率过高)
- 举例:老年代 2G, LDS 1G,合理的阈值应该是1.5 G,百分百应该在 75 左右
- CMS GC 过早,没有必要
- CMS GC过迟,不能及时完成,会引起 Full GC
- 根据老年代的增长速度确定百分比
CMS 停顿优化:
- CMS 每次 GC 周期会停顿 2 次
- 初始标记
-
- 不能影响这个阶段
- 这个阶段通常很短
- 重新标记
-
- 极大依赖于对象的变化率
- -XX:+CMSScavengeBeforeRemark:remark 之前强制使用新生代 GC 减少 remark 工作量
- -XX:+ParallelRefProcEnabled:如果程序有大量的引用或 final 对象,此参数对 CMS 有帮助
如果经历了 Full GC:
- 并发 GC 循环中断
-
- GC log 中查找 "concurrent mode failure"
- 降低 CMS 阈值
- 永久代满了
-
- 通过 GC log 监视永久代
- CMS GC 中启用用永久代 GC
-
- CMS 默认不回收永久代
- -XX:+CMSClassUnloadingEnabled
- -XX:+CMSPermGenSweepingEnabled:(JDK6u6 之前版本不支持)
- System.gc()引起 Full GC
-
- 查看 GC log,查找"Full GC (System)"
- -XX:+DisableExplicitGC:禁止程序手动 GC
- -XX:+ExplicitGCInvokesConcurrent,-XX:+ExplicitGCInvokesConcurrentAndUloadsClasses:遇到相关代码时,使用 CMS GC
- 不确定怎么做
-
- 一些应用程序/框架(例如 RMI)依赖频繁 GC 来强制引用处理
- 禁用显式 GC 对应用程序的性能有重大影响
CMS 总结:
- 相较 ParallelOldGC, CMS 需要额外的资源
-
- 额外的并发瓶颈,更大的堆,等
- 导致一些临界的应用程序超出边界
- CMS 总体优化建议
-
- 通过将对象放入老年代来降低新生代 GC 时间,长期来看会产生相反效果(性能更低)
-
- 老年代压力更大,内存碎片化,触发Full GC
- 新生代 GC 时间更久,总体而言表现更好
- 如果 ParallelOldGC 的新生代过久,CMS 不能做的更好
- CMS 的总的时间应该小于 10%