JVM调优总结

本篇从以下几个方面,对JVM调优进行总结

1. YoungGC 频繁

如果线上频繁YoungGC,应该如何解决呢?想有整体思路的话,不防先用反推法,先看原理。

1.1 触发时机

当 JVM 无法为新对象分配在新生代内存空间时总会触发 Young GC。比如 Eden 区占满时,新对象分配频率越高,Young GC 的频率就越高。

1.2 问题的原因

可以发现,当Eden区占满不足以分配新对象时,就会触发YoungGC。

1.3 问题排查

首先先用Jstat工具,观察GC变化,再结合业务逻辑查看是不是新生代设置小了,比如系统每秒产生60M的对象,但是新生代的Eden区只设置为600M,那么不到10秒Eden区就满了,那么在内存允许的范围内,应该加大Eden区的内存,尽量减少YoungGC的次数。

2. FullGC 频繁

2.1 触发时机

  • 老年代空间不足
  • 老年代空间分配担保机制
  • 元空间不够导致的多余full gc
  • 由于新生代的动态年龄判断,导致更多的对象进入老年代

2.2 问题的原因

核心点就一条,老年代的存储空间满了或者将要满了。

2.3 问题排查

根据实际经验,频繁FullGC一般是由于代码问题导致的大对象过多,FullGC之后还是清不掉,所以先用Jmap工具,打印出当时程序里的大对象,然后对照类名,查看代码,看看是不是有从数据库查询出来的一大堆对象,或者某个递归没有终止导致一直创建大对象。

3. OOM问题

3.1 堆内存不足

报错:

java.lang.OutOfMemoryError: Java heap space

原因:

  • 代码中可能存在大对象分配
  • 可能存在内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象

解决方法:

  • 检查是否存在大对象的分配,最有可能的是大数组分配
  • 通过jmap命令,把堆内存dump下来,使用mat工具分析一下,检查是否存在内存泄露的问题
  • 如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存
  • 还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性

3.2 方法栈溢出

报错:

java.lang.OutOfMemoryError : unable to create new native Thread

原因:
出现这种异常,基本上都是创建的了大量的线程导致的,以前碰到过一次,通过jstack出来一共8000多个线程

解决:

  • 通过 *-Xss *降低的每个线程栈大小的容量
  • 线程总数也受到系统空闲内存和操作系统的限制,检查是否该系统下有此限制:
    /proc/sys/kernel/pid_max
    /proc/sys/kernel/thread-max
    max_user_process(ulimit -u)
    /proc/sys/vm/max_map_count

3.3 永久代/元空间溢出

报错:

java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Metaspace

原因:

  • 在Java7之前,频繁的错误使用String.intern方法
  • 生成了大量的代理类,导致方法区被撑爆,无法卸载
  • 应用长时间运行,没有重启

解决方案:

  • 检查是否永久代空间或者元空间设置的过小
  • 检查代码中是否存在大量的反射操作
  • dump之后通过mat检查是否存在大量由于反射生成的代理类
  • 放大招,重启JVM

4. 大型系统JVM参数设置

4.1 思路

  • 分析业务系统的访问量,得出每小时访问量
  • 将访问量转化为对象数,查看每个对象的大小
  • 计算业务系统每秒产生的对象大小
  • 根据这些对象的特点确定新生代、老年代大小
  • 观察程序运行情况或压测时查看GC日志,查看YoungGC和FullGC的情况
  • 尽量让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在YoungGC的时候这些对象都会被回收,不会进到老年代从而导致FullGC。
  • 可以适当调整进入老年代的动态年龄判断
  • 遇到问题多翻阅那几条优化原则,对症下药

5. G1收集器优化建议

5.1 不要设置年轻代大小

显式的使用-Xmn设置年轻代的大小,会干预G1的默认行为。

  • G1就不会再考虑设定的暂停时间目标,所以本质上说,设定了年轻代大小就相当于禁用了目标暂停时间
  • G1就无法根据需要增大或者缩小年轻代的大小。既然大小固定了,就无法在大小上做任何改变了

5.2 响应时间指标

不要根据平均响应时间(ART)作为衡量标准去设定XX:MaxGCPauseMillis=选项,而是设定一个想在90%或者以上的时间都会满足这目标的值。也就是说90%的用户,都会在目标时间,甚至更短的时间内得到响应。记住设定的目标时间只是一个目标,不能保证永久都会满足这个目标。

5.3 MixedGC优化

  • -XX:InitiatingHeapOccupancyPercent 指定触发全局并发标记的老年代使用占比,默认值45%,也就是老年代占堆的比例超过45%
  • -XX:G1MixedGCLiveThresholdPercent 指定被纳入Cset的Region的存活空间占比阈值,不同版本默认值不同,有65%和85%。在全局并发标记阶段,如果一个Region的存活对象的空间占比低于此值,则会被纳入Cset。
  • -XX:G1HeapWastePercent 指定触发Mixed GC的堆垃圾占比,默认值5%,也就是在全局标记结束后能够统计出所有Cset内可被回收的垃圾占整堆的比例值,如果超过5%,那么就会触发之后的多轮Mixed GC,如果不超过,那么会在之后的某次Young GC中重新执行全局并发标记。可以尝试适当的调高此阈值,能够适当的降低Mixed GC的频率。
  • -XX:G1MixedGCCountTarget 指定一个周期内触发Mixed GC最大次数,默认值8。
  • -XX:G1OldCSetRegionThresholdPercent 指定每轮Mixed GC回收的Region最大比例,默认10%,也就是每轮Mixed GC附加的Cset的Region不超过全部Region的10%,最多10%,如果暂停时间短,那么可能会少于10%。

5.4 to-space overflow 和 to-space exhausted 问题

当在gc日志中出现to-space overflow或者to-space exhausted消息时,说明没有足够的内存来存储晋升对象或者survivor对象,gc日志示例如下:

924.897: [GC pause (G1 Evacuation Pause) (mixed) (to-space exhausted), 0.1957310 secs]

924.897: [GC pause (G1 Evacuation Pause) (mixed) (to-space overflow), 0.1957310 secs]

要缓解此问题,可以尝试以下调整:

  • 增加-XX:G1ReservePercent 选项的值(并相应地增加总堆),以增加“担保预留空间的大小”
  • 通过减小-XX:InitiatingHeapOccupancyPercent的值来更早地开始标记周期
  • 增加-XX:ConcGCThreads的值,以增加并行标记线程的数量

参考资料

  1. https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html

你可能感兴趣的:(JVM调优总结)