JVM GC 调优理论

参考资料:

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:
    • -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
    • 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的区别):
  • 通常老年代没有压缩(标记-清除算法,会有内存碎片,可以指定每次 Full GC 后进行内存碎片整理,详见 JVM GC 基础知识)
    • 潜在的内存碎片化
    • 仅 Full GC 进行内存压缩
  • 老年代 增大 20% ~ 30%
    • 因为碎片化/更繁琐的并发GC循环(和用户线程并发,需要为新生代 GC 预留内存)
  • 更长的新生代 GC 时间
    • 因为对象进入老年代更慢
  • 更好的最差延迟
    • 大部分工作都是和用户进程并发进行的
    • 更短的最差停顿时间
  • 更低的性能
    • CMS做的工作更多

老年代阈值优化:
  • 阈值决定对象在新生代待的时长
    • 最大老年代阈值(Max Tenuring Threshold, MTT),决定对象经历的新生代 gc 的次数
    • 老年代阈值自动优化(和 survivor 的剩余空间有关),不会超过 MTT
  • -XX:MaxTenuringThreshold=
    • 最小为1,不超过15,(JDK 5u6 之前,最大 31)
  • 更高的老年代阈值(MTT 调大,survivor 内存调大)-->进入老年代的对象更少
    • 新生代 GC 时间通常更长(复制的内存更多)
    • 新生代回收内存更多,整体效率更高
  • 更低的老年代阈值( MTT 调小,survivor 内存调小)-->进入老年代的对象更多
    • 新生代 GC 时间通常更短(复制的内存更少)
    • 老年代负载更大
    • 内存更可能碎片化(仅CMS会使内存碎片化,需尽量减少此情况)

幸存区大小优化:
  • 幸存区不应该溢出
  • -XX:TargetSurvivorRatio=
    • 目的幸存区(to)仅在新生代 GC 之后使用
    • 使用[50%,90%],少于突发负载情况
  • 可以通过 GC log 或 tenuring distribution (分代信息) 进行监视
  • 如果幸存区溢出
    • 调大幸存区比例,-XX:SurvivorRatio
    • 降低MTT(默认15,JVM 通常会自动调整TT,所以一般不会发生溢出)

CMS 阈值优化:
  • 决定何时开始并发 GC 循环(由于并发进行 GC,需要预留老年代空间给新生代 GC)
    • 基于老年代 GC 表现
  • -XX:CMSInitiatingOccupancyFraction=(默认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%

























你可能感兴趣的:(Java)