JVM调优总结

作者: 逍遥白亦 | 来源:发表于2021-03-23 22:05 被阅读0次

    本篇从以下几个方面,对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=<N>选项,而是设定一个想在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调优总结

        本文链接:https://www.haomeiwen.com/subject/rluqhltx.html