Fork me on GitHub

JVM调优工具及优化原则

一些调优工具

jmap

jmap可以查看内存信息、对象实例个数和占用内存大小。

比如下面命令可以查看堆内存中存活的对象实例个数和占用内存大小

1
jmap  -histo:live pid > ./log.txt

image-20220701105039703

还可以通过命令dump JVM的内存

1
jmap -dump:format=b,file=CMSdemo.hprof pid

jstack

jstack可以查看线程堆栈,比如命令:

1
jstack -e pid

jstack能自动输出存在线程死锁的详细信息。(线程状态,互相等待的线程,等待的对象等)
image-20220701105054869

另外在机器CPU飙高之后,可以使用jstack来排查占用cpu时间最多的线程。

  1. top -p pid:此命令是显示pid对应java进程的CPU统计情况。
  2. 按H输出当前进程的线程情况(也可以直接top -H -p pid)
  3. 找到内存和CPU占用最高的线程tid,转为16进制(在jstack线程堆栈中线程id都是16进制的)
  4. 执行jstack -e pid |grep -A 10 16进制线程id即可查看对应的线程堆栈。

当然用Arthas更方便的排查CPU利用率高的线程。

Jinfo

查看正在运行的Java程序的扩展参数。

比如查看jvm参数,可以用命令:

1
jinfo -flags pid

image-20220701105111453

可以用命令,查看jvm的系统参数

1
info -sysprops 75453

jstat

jstast可以看JVM堆内存各部分的使用量,以及加载类的数量。

垃圾回收对象统计(最常用)

1
jstat -gc pid 2000 100 -- 代表输出进程=pid的java堆gc信息,每2000ms打印一次,打印100次

image-20220701105119527

  • S0C:Survivor0的大小,单位kb
  • S1C:S1区的大小
  • S0U:S0的使用大小
  • S1U:S1的使用大小
  • EC:Eden区的大小
  • EU:Eden使用大小
  • OC:Old区当前空间大小
  • OU:Old区使用空间大小
  • CCSC:压缩指针类当前空间大小
  • CCSU:压缩指针类空间使用大小
  • YGC:年轻代GC次数
  • YGCT:年轻代GC消耗时间,单位S
  • FGC:FullGC次数
  • FGCT:FullGC时间
  • GCT:垃圾回收消耗的总时间

堆内存统计

下面的命令可以查看堆内存统计

1
jstat -gccapacity pid

image-20220701105136683

新生代垃圾回收统计

1
jstat -gcnew 75453 1000 10

image-20220701105152448

新生代内存空间

1
jstat -gcnewcapacity 75453 1000 10

image-20220701105203998

老年代内存统计

1
jstat -gcoldcapacity 75453 1000 10

image-20220701105212831

老年代垃圾回收统计

1
jstat -gcold 75453 1000 10

image-20220701105226962

元空间统计

1
jstat -gcmetacapacity 75453  1000 10

image-20220701105248283

JVM运行情况预估

jstat -gc pid命令可以观察到堆内存使用情况和GC情况,则可以通过jstast观测的结果来了解和预估JVM的运行情况。

年轻代对象增长的速率

观察EU(Eden区的使用)来估算每秒eden大概新增多少对象,可以根据负载去调整观察频率。注意系统的高峰期和日长期,在不同时间去观测对象增长速率。

YoungGC触发频率和每次耗时

知道了年轻代对象增长速率,再根据Eden区的大小就可以知道YoungGC的触发频率,还可以根据YGC / YGCT 来计算出YoungGC每次的耗时。

每次YoungGC后有多少对象存活进入到老年代

如果知道了YoungGC没过多久触发了一次,比如1s触发一次,可以用命令
jstat -gc pid 1000 10 来打印最近10次的内存和GC情况,观察SU和OU的增长,因为每次YoungGC之后,存活对象会移动到Survivor区或者晋升到老年代中,所以可以看到每次多少对象进入到老年代。

FullGC触发频率和每次耗时

FullGC的触发频率可以根据每次YoungGC多少对象进入老年代和老年代的大小来大致估算下。(当然老年代有很多参数,比如CMS垃圾回收器中触发CMS回收的阈值;比如元空间不足、手动调用System.gc()也都会触发FullGC)

FullGC的每次耗时可以根据FGCT / FGC来计算得出。

整体优化思路

优化思路主要是根据JVM内存分配和对象流转策略的几个点来的,主要是去优化FullGC的频率。同时要对JVM的内存有一个划分:
image-20220701105257526

优化的原则比如:

  • 选择合适的垃圾回收器
    • CPU单核:Serial可能是最好的选择,单线程进行回收
    • CPU多核:关注吞吐量,那么要选择Parallal Scavenge和Parallel Old的组合
    • CPU多核:关注用户停顿时间,内存不大时可以选择CMS+ParNew
    • CPU多核:关注用户停顿时间,内存大于6G时可以选择G1。
  • 增加内存大小

    • 可能是最有效的减少GC频次的方法,但要注意副作用
      • 内存变大,意味着每次GC都要清除更多的对象,要关注GC时间,且选择停顿时间低的垃圾回收器,比如CMS。
      • 在GC之后回收的很少,且占用率逐步上升,可能是存在内存泄漏,这里要进行排查,不能无脑加内存。
  • 设置合理的内存区域大小和分代晋升年龄

    • 如果Eden区太小,可能会频发触发MinorGC,大多数对象都是在年轻代被GC掉,Eden区域不要太小。
    • 如果Survivor太小,可能会提早晋升到老年代。
    • 分代晋升年龄太小,也会导致本应在年轻代被GC的对象晋升到老年代。
  • 根据对象的动态年龄判定来优化
    对象的动态年龄判定是指的年轻代的对象不一定要在GC分代年龄到达设置阈值才晋升到老年代,如果Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于此年龄的对象可以直接老年代,不需要等待达到晋升老年代的阈值。

这里尽量减少YoungGC之后对象在Survivor区占用的比例,可以适当调大Survivor区的大小,避免生命周期短的对象对象提前晋升到老年代,来增加老年代的压力和FullGC的频率。

  • 大对象直接进入到老年代
    新生代采用复制算法来GC,所以大于阈值的对象会为了避免复制则直接晋升到老年代中。应该尽量避免大对象的产生,因为大多数都是要在年轻代GC掉的对象,进入老年代可能会增加FullGC的频率。(比如未淘汰的缓存对象、未分页查询的SQL结果对象等)

  • 老年代空间分配担保机制
    可能出现的现象:频繁FullGC、FullGC次数比YoungGC次数都多

在MinorGC之前,只要老年代的连续空间小于新生代对象总和或历次晋升的对象平均大小,就会进行一次FullGC。这时如果可预测有生命周期长的对象,那么可以设置较小的晋升阈值或者较小的晋升年龄提前进入老年代;如果对象都是快速被GC的,可以设置较大的老年代空间。都是为了避免因为空间担保机制频繁FullGC。

  • 元空间不足触发FullGC
    可能出现的现象:启动过程频繁fullGC,启动时间变长。

如果不设置元空间的JVM相关参数,那么MetaSpace默认大小21m,在启动过程中会不断FullGC,(自动扩大元空间大小,再次触发FullGC的一个过程)

  • CMS垃圾收集器的concurrent mode failure
    如果在CMS GC的并发标记或者并发清理阶段,再次因为大对象晋升、老年代空间担保机制等原因触发了FullGC,那么会触发Concurrent mode failure,会退化为单线程的Serial Old进行GC,效率低且整个系统会STW。

在确定了大体原因之后,如果要定位有问题的代码,可以使用jmap去看存活对象或者导出内存快照,也可以jstack看占用CPU比较高的线程(一般不断的创建对象代表线程的活跃)区定位具体的问题代码。

-------------本文结束感谢您的阅读-------------

本文标题:JVM调优工具及优化原则

文章作者:夸克

发布时间:2019年05月30日 - 00:05

最后更新:2022年07月01日 - 10:07

原始链接:https://zhanglijun1217.github.io/2019/05/30/JVM调优工具及优化原则/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。