基于实战的JVM GC 调优

478 阅读6分钟

一:相应命令

命令
jstat -gcutil pid time n:pid time时间执行一次一共执行n次(每个区域占比)
jstat -gc pid time n :pid time时间执行一次一共执行n次(每个区域大小)
jstat -gccapacity PID:堆内存分析
jstat -gcnew PID:年轻代GC分析,这里的TT和MTT可以看到对象在年轻代存活的年龄和存活的最大年龄
jstat -gcnewcapacity PID:年轻代内存分析
jstat -gcold PID:老年代GC分析
jstat -gcoldcapacity PID:老年代内存分析
jstat -gcmetacapacity PID:元数据区内存分析
jmap -heap pid 堆初始化以及使用情况

1.0 jmap -heap pid

image.png

1.1 jstat -gcutil

image.png

  • S0: 新生代中Survivor space 0区已使用空间的百分比
  • S1: 新生代中Survivor space 1区已使用空间的百分比
  • E: 新生代已使用空间的百分比
  • O: 老年代已使用空间的百分比
  • M:元数据区使用比例
  • CCS:压缩使用比例
  • YGC: 从应用程序启动到当前,发生Yang GC 的次数
  • YGCT: 从应用程序启动到当前,Yang GC所用的时间【单位秒】
  • FGC: 从应用程序启动到当前,发生Full GC的次数
  • FGCT: 从应用程序启动到当前,Full GC所用的时间
  • GCT: 从应用程序启动到当前,用于垃圾回收的总时间【单位秒】

1.2 jstat -gc

image.png

  • S0C:这是From Survivor区的大小
  • S1C:这是To Survivor区的大小
  • S0U:这是From Survivor区当前使用的内存大小
  • S1U:这是To Survivor区当前使用的内存大小
  • EC:这是Eden区的大小
  • EU:这是Eden区当前使用的内存大小
  • OC:这是老年代的大小
  • OU:这是老年代当前使用的内存大小
  • MC:这是方法区(永久代、元数据区)的大小MU:这是方法区(永久代、元数据区)的当前使用的内存大小
  • YGC:这是系统运行迄今为止的Young GC次数
  • YGCT:这是Young GC的耗时
  • FGC:这是系统运行迄今为止的Full GC次数
  • FGCT:这是Full GC的耗时
  • GCT:这是所有GC的总耗时

1.3 查看相应Gc日志(需要在jvm中配置,输入服务器上执行文件可以用tail -f logname 观察)

image.png

上图就是对应的对内存日志,par new 就是新生代,对应下图的 Eden,From,To区域。一般占据堆空间的1/3。在新生代中,保存着大量的刚刚创建的对象,但是大部分的对象都是朝生夕死,所以在新生代中会频繁的进行MinorGC,进行垃圾回收。新生代又细分为三个区:Eden区、SurvivorFrom、ServivorTo区,三个区的默认比例为:8:1:1

  • Eden区:Java新创建的对象绝大部分会分配在Eden区(如果对象太大,则直接分配到老年代)。当Eden区内存不够的时候,就会触发MinorGC(新生代采用的是 [复制算法] ),对新生代进行一次垃圾回收。
  • SurvivorFrom区和To区:在GC开始的时候,对象只会存在于Eden区和名为From的Survivor区,To区是空的,一次MinorGc过后,Eden区和SurvivorFrom区存活的对象会移动到SurvivorTo区中,然后会清空Eden区和SurvivorFrom区,并对存活的对象的年龄+1,如果对象的年龄达到15,则直接分配到老年代。MinorGC完成后,SurvivorFrom区和SurvivorTo区的功能进行互换。下一次MinorGC时,会把SurvivorTo区和Eden区存活的对象放入SurvivorFrom区中,并计算对象存活的年龄。

1.3.1 解释

image.png

2665.813: [GC (Allocation Failure) 2665.813: [ParNew : 1799735K->190530K(1887488K), 0.1172587 secs] 2670607K->1061403K(5033216K), 0.1178282 secs] [Times: user=0.45 sys=0.00, real=0.12 secs] 解析: 2665.813 :系统运行以后过了多少秒发生了本次GC GC (Allocation Failure) :对象分配失败,此时就要触发一次Young GC ParNew : 1799735K->190530K(1887488K), 0.1172587 secs ParNew: 触发的是年轻代的Young GC,所以是用我们指定的ParNew垃圾回收器执行的 GC (1887488K): 年轻代可用空间是1887488K,也就是1843MB。Eden区是1638MB,两个Survivor中只有一个是可以放存活对象的,另外一个是必须一致保持空闲的,所以他考虑年轻代的可用空间,就是Eden+1个Survivor的大小,也就是1757MB。 1799735K->190530K: 意思就是对年轻代执行了一次GC,GC之前都使用了1799735KB了,但是GC之后只有190530KB的对象是存活下来 0.1178282 secs: 这个就是本次gc耗费的时间,看这里来说大概耗费了117.8ms,仅仅是回收1572MB的对象。

image.png

1.3.2 提升失败现象

什么是提升失败:在 Minor GC 过程中, Survivor Unused 可能不足以容纳 Eden 和另一个 Survivor 中的存活对象, 那么多余的将被移到老年代, 称为 过早提升(Premature Promotion)。 这会导致老年代中短期存活对象的增长, 可能会引发严重的性能问题。  再进一步, 如果老年代满了, Minor GC 后会进行 Full GC, 这将导致遍历整个堆, 称为 提升失败(Promotion Failure)。 失败日志如下图:

image.png

原因:Minor GC 时发现 Survivor 空间放不下,而老年代的空闲也不够

  • 新生代提升太快
  • 老年代碎片太多,放不下大对象提升(表现为老年代还有很多空间但是,出现了 promotion failed) 解决方式:
  • 新生代提升过快问题:(1)如果频率太快的话,说明空间不足,首先可以尝试调大新生代空间和晋升阈值。(2)如果内存有限,可以设置 CMS 垃圾收集在老年代占比达到多少时启动来减少问题发生频率(越早启动问题发生频率越低,但是会降低吞吐量,具体得多调整几次找到平衡点),参数如下:如果没有第二个参数,会随着 JVM 动态调节 CMS 启动时间 -XX:CMSInitiatingOccupancyFraction=68 (默认是 68) -XX:+UseCMSInitiatingOccupancyOnly
  • 老年代碎片严重问题:(1)如果频率太快或者 Full GC 后空间释放不多的话,说明空间不足,首先可以尝试调大老年代空间(2)如果内存不足,可以设置进行 n 次 CMS 后进行一次压缩式 Full GC,参数如下:

-XX:+UseCMSCompactAtFullCollection:允许在 Full GC 时,启用压缩式 GC -XX:CMSFullGCBeforeCompaction=n     在进行 n 次,CMS 后,进行一次压缩的 Full GC,用以减少 CMS 产生的碎片

另一条,是因为 Survivor Unused 不足,那么可以尝试调大 Survivor 来尝试下

二分配速率

分配速率代表固定时间内分配的内存量,通常情况下以MB/S为单位,分配速率高,其实并不是什么好事,对于这点我们稍后再做阐述。先来具体如何计算分配的速率。