JVM 调优

一、什么情况下GC会对程序产生影响

无论Minor GC/Young GC还是 Full GC,都会造成一定程度的程序卡顿,即Stop The World:JVM 因为执行 GC 线程,其他工作线程被挂起。它会在任何一种 GC 算法中发生,即使采用 ParNew、CMS 或者 G1 这些更先进的垃圾回收算法,也只是减少卡顿时间,并不能完全消除卡顿。当stop-the-world发生时,除 GC 所需的线程外,所有的线程都进入等待状态,直到 GC 任务完成。GC 优化常常就是减少 stop-the-world 的发生。

根据 GC 对程序产生影响的严重程度,从高到低包括以下四种情况:

1️⃣【Full GC 过于频繁】Full GC 通常是比较慢的,少则几百毫秒,多则几秒。正常情况 Full GC 每隔几个小时甚至几天才执行一次,对系统的影响还能接受。一旦 Full GC 频繁出现(比如几十分钟就会执行一次),它会导致工作线程频繁被停止,让系统看起来一直有卡顿现象,也会使得程序的整体性能变差。
2️⃣【Minor GC/Young GC 耗时过长】一般 Minor GC/Young GC 的总耗时在几十或者上百毫秒是比较正常的,即便会引起系统卡顿几毫秒或者几十毫秒,但这种情况几乎对用户无感知,对程序的影响可以忽略不计。如果耗时达到了 1 秒甚至几秒(都快赶上 Full GC 的耗时了),那卡顿时间就会增大,加上 Minor GC/Young GC 本身比较频繁,就会导致比较多的服务超时问题。
3️⃣【Full GC 耗时过长】Full GC 耗时增加,卡顿时间也会随之增加,尤其对于高并发服务,可能导致 Full GC 期间比较多的超时问题,可用性降低,这种也需要关注。
4️⃣【Minor GC/Young GC 过于频繁】即使 Minor GC/Young GC 不会引起服务超时,但是过于频繁也会降低服务的整体性能,对于高并发服务也是需要关注的。

其中,「Full GC 过于频繁」和「Minor GC/Young GC 耗时过长」,这两种情况属于比较典型的 GC 问题,大概率会对程序的服务质量产生影响。剩余两种情况的严重程度低一些,但是对于高并发或者高可用的程序也需要关注。

二、JVM性能调优方法和步骤

对 JVM 内存的系统级调优策略主要是减少 GC 的频率,尤其是 Full GC,从而减少 stop-the-world 的发生。

1️⃣监控 GC 的状态
使用各种 JVM 工具,查看当前日志,分析当前 JVM 参数设置,并且分析当前堆内存快照和 GC 日志,根据实际各区域的内存划分和 GC 执行时间,分析是否进行优化。

系统崩溃前的一些现象:

  1. 每次垃圾回收的时间越来越长,由之前的 10ms 延长到 50ms 左右,Full GC 的时间也由之前的 0.5s 延长到 4、5s。
  2. Full GC 的次数越来越多,最频繁时隔不到 1 分钟就进行一次 Full GC。
  3. 老年代的内存越来越大并且每次 Full GC 后老年代没有内存被释放。

之后系统会无法响应新的请求,逐渐逼近OutOfMemoryError的临界值,此时就需要分析 JVM 内存快照 dump。

2️⃣生成堆的 dump 文件
通过 JMX 的 MBean 生成当前的 Heap 信息,大小为一个 3G(整个堆的大小)的 hprof 文件,如果没有启动 JMX 可以通过 Java 的 jmap 命令来生成该文件。

3️⃣分析 dump 文件
打开这个 3G 的堆信息文件,显然一般的 Window 系统没有这么大的内存,必须借助高配置的几种 Linux 工具打开该文件:

①Visual VM
②IBM HeapAnalyzer
③JDK 自带的Hprof工具
④Mat(Eclipse专门的静态内存分析工具)推荐使用

说明:文件太大,建议使用Eclipse专门的静态内存分析工具Mat打开分析。

4️⃣分析结果,判断是否需要优化
如果各项参数设置合理,系统没有超时日志出现,GC 频率不高,GC 耗时不高,那么没有必要进行 GC 优化。如果 GC 时间超过 1-3 秒,或者频繁 GC,则必须优化。

注意:如果满足下面的指标,则一般不需要进行 GC 优化:

①Minor GC 执行时间不到 50ms。
②Minor GC 执行不频繁,约 10 秒一次。
③Full GC 执行时间不到 1s。
④Full GC 执行频率不算频繁,不低于 10 分钟 1 次。

5️⃣调整 GC 类型和内存分配
如果内存分配过大或过小,或者采用的 GC 收集器比较慢,则应该优先调整这些参数,并且先找 1 台或几台机器进行测试,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。

6️⃣不断的分析和调整
通过不断的试验和试错,分析并找到最合适的参数,如果找到了最合适的参数,则将这些参数应用到所有服务器。
cms参数优化步流程

三、JVM调优参数参考

1️⃣针对 JVM 堆的设置,一般可以通过 -Xms -Xmx 限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,通常把最大、最小设置为相同的值。

2️⃣新生代和老年代将根据默认的比例(1:2)分配堆内存, 可以通过调整二者之间的比率来调整二者之间的大小,也可以针对回收代。

比如新生代,通过-XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止新生代的堆收缩,通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。

3️⃣新生代和老年代设置多大才算合理

  1. 更大的新生代必然导致更小的老年代,大的新生代会延长 Minor GC/Young GC 的周期,但每次时间会增加;小的老年代会导致更频繁的 Full GC。

  2. 更小的新生代必然导致更大老年代,小的新生代会导致 Minor GC/Young GC 很频繁,但每次时间会更短;大的老年代会减少 Full GC 的频率。

如何选择应该依赖应用程序对象生命周期的分布情况:如果应用存在大量的临时对象,应该选择更大的新生代;如果存在相对较多的持久对象,老年代应该适当增大。但很多应用都没有这样明显的特性。

在抉择时应该根据以下两点:

  1. 本着 Full GC 尽量少的原则,让老年代尽量缓存常用对象,JVM 的默认比例(1:2)也是这个道理 。
  2. 观察应用一段时间,看其在峰值时老年代会占多少内存,在不影响 Full GC 的前提下,根据实际情况加大新生代,比如可以把比例控制在(1:1)。但应该给年老代至少预留 1/3 的增长空间。

4️⃣在配置较好的机器上(比如多核、大内存),可以为老年代选择并行收集算法:-XX:+UseParallelOldGC

5️⃣线程堆栈的设置:每个线程默认会开启 1M 的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太大了,一般 256K 就够用。理论上,在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

四、排查Full GC问题的实践指南

1️⃣Full GC 的触发时机

当晋升到老年代的对象大于了老年代的剩余空间时,就会触发 Full GC。除此之外,还有以下四种情况也会触发:

  1. 老年代的内存使用率达到了一定阈值(可通过参数调整),直接触发 Full GC。

  2. 空间分配担保
    在发生 Minor GC/Young GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。

①如果大于,那这一次 Minor GC/Young GC 可以确保是安全的。
②如果小于,则虚拟机会先查看- XX:HandlePromotionFailure的设置值是否允许担保失败(Handle Promotion Failure):

  • 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行 Minor GC/Young GC,尽管此次 Minor GC/Young GC 有风险。
  • 如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次 Full GC。
  1. Metaspace 达到-XX:MetaspaceSize【元空间初始值,以字节为单位】的指定值时,也会触发 Full GC。同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize【元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。】(如果设置了)的情况下,适当提高该值。

  2. System.gc()或者Runtime.getRuntime().gc()被显式调用时,触发 Full GC。

2️⃣清楚排查问题时能使用哪些工具

  1. 公司的监控系统:大部分公司都会有,可全方位监控 JVM 的各项指标。

  2. JDK 的自带工具,包括 jmap、jstat 等常用命令:

  • 查看堆内存各区域的使用率以及GC情况
    jstat -gcutil -h20 pid 1000
  • 查看堆内存中的存活对象,并按空间排序
    jmap -histo pid | head -n20
  • dump堆内存文件
    jmap -dump:format=b,file=heap pid
  1. 可视化的堆内存分析工具:JVisualVM、MAT等

3️⃣排查指南

  1. 查看监控,以了解出现问题的时间点以及当前 Full GC 的频率(可对比正常情况看频率是否正常)。
  2. 了解该时间点之前有没有程序上线、基础组件升级等情况。
  3. 了解 JVM 的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后分析 JVM 参数设置是否合理。
  4. 再对可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用 gc 方法比较容易排查。
  5. 针对大对象或者长生命周期对象导致的 Full GC,可通过jmap -histo命令并结合 dump 堆内存文件作进一步分析,需要先定位到可疑对象。
  6. 通过可疑对象定位到具体代码再次分析,这时候要结合 GC 原理和 JVM 参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JFS_Study

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值