本篇从以下几个方面,对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的值,以增加并行标记线程的数量
网友评论