当前位置: Java > JVM高频面试题 > 26.JVM如何调优

本来这个地方只是想列几个常用的JVM参数就完了,毕竟大多数项目并不需要JVM调优,千万别项目中出了问题就想JVM调优,这绝对是本末倒置的做法。不过虽然项目中大概率用不到,但是面试会问啊,所以还是简单介绍下。

正常的情况下项目出问题或者系统优化应该先从应用层面开始,然后往底层过度,首先查查是不是代码写的太垃圾了,逻辑上有没有可优化的地方等等,然后再看看数据库有没有可优化的地方,在然后可以考虑JVM层面的优化,最后,可以考虑底层操作系统层面的优化。前两者基本就解决了大部分问题。

JVM调优的目标

大多数时候,JVM的调优最主要的目标是停顿时间吞吐量,停顿时间一般是由垃圾收集引起的,可以简单理解为系统对请求的响应速度,吞吐量是指用户程序运行时间占用户程序运行时间和垃圾收集总时间的比值,可以简单理解为系统在特定时间内的最大工作量。还有一个指标是内存占用,程序正常运行所需要的内存,自然是越小越好。

JVM优化得目标就是系统以较小的内存资源获得较低的停顿时间和较高的吞吐量,这当然不现实了,又想马儿跑,还想马儿不吃草,所以还是要根据具体场景具体分析。

JVM调优策略

JVM调优的具体策略基本都是围绕着两部分展开的,内存垃圾回收器

  • 调整内存大小,如果内存太小,那么GC会非常频繁的,停顿时间自然就长了
  • 调整新生代和老年代的比重等,比如IO密集型的,就可以把新生代加大些,因为大多数对象在新生代就会消亡
  • 调整晋升老年代的年龄,比如老年代频繁GC,可以考虑增大老年代的比重,也可以提高晋升老年代的年龄
  • 大对象分配的标准,因为大对象是直接进入老年代的,如果大对象的标准不合适,也会出现问题
  • 选择合适的垃圾回收器及垃圾回收器的各种参数,比如如果是CPU是单核的,就选Serial就可以了等等

上面这只是简单举例,类似的情况还有很多,简单来说就是围绕着内存和垃圾回收器具体情况具体分析

JVM调优参数

前面说了调优策略主要是围绕着内存和垃圾回收器,那么可调的参数大多也是围绕这两个。

内存部分参数
参数含义
-Xms初始堆大小
-Xmx最大堆大小
-Xmn新生代大小
-Xss设置每个线程的堆栈大小
-XX:NewRatio设置新生代与年老代的比值
-XX:SurvivorRatio设置新生代中Eden区与Survivor区的大小比值
-XX:PermSize/-XX:MetaspaceSize初始化持久代/元空间大小
-XX:MaxPermSize/-XX:MaxMetaspaceSize设置持久代/元空间最大值
-XX:MaxTenuringThreshold设置进入老年代的年龄
jv内部参数
垃圾回收器相关参数
参数含义
-XX:+UseG1GC使用G1垃圾回收器
-XX:ParallelGCThreads并行收集器的线程数
-XX:GCTimeRatio设置垃圾回收时间占程序运行时间的百分比
-XX:MaxGCPauseMillis设置目标停顿时间
垃圾回收参数

相关参数还有很多,这里就不一一列举了

JVM的调优步骤

JVM调优肯定不是乱调的,也应先确定瓶颈及调优目标,如下:

  • 分析GC日志及通过虚拟机监控的命令查看系统运行情况,找出哪里出了问题
  • 确定调优的目标
  • 确定调优策略及调整相关参数,这是个不断对比分析和调整的过程,很难一步到位的

JVM调优案例

前面简单介绍了JVM调优的方法和策略,下面从网上找了一些调优的案例,大家可以参考一下

1.数据分析平台系统频繁 Full GC

平台主要对用户在 App 中行为进行定时分析统计,并支持报表导出,使用 CMS GC 算法。

数据分析师在使用中发现系统页面打开经常卡顿,通过 jstat 命令发现系统每次 Young GC 后大约有 10% 的存活对象进入老年代。

原来是因为 Survivor 区空间设置过小,每次 Young GC 后存活对象在 Survivor 区域放不下,提前进入老年代。

通过调大 Survivor 区,使得 Survivor 区可以容纳 Young GC 后存活对象,对象在 Survivor 区经历多次 Young GC 达到年龄阈值才进入老年代。

调整之后每次 Young GC 后进入老年代的存活对象稳定运行时仅几百 Kb,Full GC 频率大大降低。

2.业务对接网关 OOM

网关主要消费 Kafka 数据,进行数据处理计算然后转发到另外的 Kafka 队列,系统运行几个小时候出现 OOM,重启系统几个小时之后又 OOM。

通过 jmap 导出堆内存,在 eclipse MAT 工具分析才找出原因:代码中将某个业务 Kafka 的 topic 数据进行日志异步打印,该业务数据量较大,大量对象堆积在内存中等待被打印,导致 OOM。

3. 鉴权系统频繁长时间 Full GC

系统对外提供各种账号鉴权服务,使用时发现系统经常服务不可用,通过 Zabbix 的监控平台监控发现系统频繁发生长时间 Full GC,且触发时老年代的堆内存通常并没有占满,发现原来是业务代码中调用了 System.gc()。

上述三个案例来源于博客:https://juejin.cn/post/6844903953415536654#heading-61

4.网站流量浏览量暴增后,网站反应页面响很慢。

1、问题推测:在测试环境测速度比较快,但是一到生产就变慢,所以推测可能是因为垃圾收集导致的业务线程停顿。

2、定位:为了确认推测的正确性,在线上通过jstat -gc 指令 看到JVM进行GC 次数频率非常高,GC所占用的时间非常长,所以基本推断就是因为GC频率非常高,所以导致业务线程经常停顿,从而造成网页反应很慢。

3、解决方案:因为网页访问量很高,所以对象创建速度非常快,导致堆内存容易填满从而频繁GC,所以这里问题在于新生代内存太小,所以这里可以增加JVM内存就行了,所以初步从原来的2G内存增加到16G内存。

4、第二个问题:增加内存后的确平常的请求比较快了,但是又出现了另外一个问题,就是不定期的会间断性的卡顿,而且单次卡顿的时间要比之前要长很多。

5、问题推测:练习到是之前的优化加大了内存,所以推测可能是因为内存加大了,从而导致单次GC的时间变长从而导致间接性的卡顿。

6、定位:还是通过jstat -gc 指令 查看到 的确FGC次数并不是很高,但是花费在FGC上的时间是非常高的,根据GC日志 查看到单次FGC的时间有达到几十秒的。

7、解决方案: 因为JVM默认使用的是PS+PO的组合,PS+PO垃圾标记和收集阶段都是STW,所以内存加大了之后,需要进行垃圾回收的时间就变长了,所以这里要想避免单次GC时间过长,所以需要更换并发类的收集器,因为当前的JDK版本为1.7,所以最后选择G1垃圾收集器,根据之前垃圾收集情况设置了一个预期的停顿的时间,上线后网站再也没有了卡顿问题。

5.后台导出数据引发的OOM

问题描述:公司的后台系统,偶发性的引发OOM异常,堆内存溢出。

1、因为是偶发性的,所以第一次简单的认为就是堆内存不足导致,所以单方面的加大了堆内存从4G调整到8G。

2、但是问题依然没有解决,只能从堆内存信息下手,通过开启了-XX:+HeapDumpOnOutOfMemoryError参数 获得堆内存的dump文件。

3、VisualVM 对 堆dump文件进行分析,通过VisualVM查看到占用内存最大的对象是String对象,本来想跟踪着String对象找到其引用的地方,但dump文件太大,跟踪进去的时候总是卡死,而String对象占用比较多也比较正常,最开始也没有认定就是这里的问题,于是就从线程信息里面找突破点。

4、通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,发现有个引起我注意的方法,导出订单信息。

5、因为订单信息导出这个方法可能会有几万的数据量,首先要从数据库里面查询出来订单信息,然后把订单信息生成excel,这个过程会产生大量的String对象。

6、为了验证自己的猜想,于是准备登录后台去测试下,结果在测试的过程中发现到处订单的按钮前端居然没有做点击后按钮置灰交互事件,结果按钮可以一直点,因为导出订单数据本来就非常慢,使用的人员可能发现点击后很久后页面都没反应,结果就一直点,结果就大量的请求进入到后台,堆内存产生了大量的订单对象和EXCEL对象,而且方法执行非常慢,导致这一段时间内这些对象都无法被回收,所以最终导致内存溢出。

7、知道了问题就容易解决了,最终没有调整任何JVM参数,只是在前端的导出订单按钮上加上了置灰状态,等后端响应之后按钮才可以进行点击,然后减少了查询订单信息的非必要字段来减少生成对象的体积,然后问题就解决了。

6.CPU经常100% 问题定位思路。

问题分析:CPU高一定是某个程序长期占用了CPU资源。

1、所以先需要找出那个进行占用CPU高。

 top  列出系统各个进程的资源占用情况。

2、然后根据找到对应进行里哪个线程占用CPU高。

 top -Hp 进程ID   列出对应进程里面的线程占用资源情况

3、找到对应线程ID后,再打印出对应线程的堆栈信息

 printf "%x\n"  PID    把线程ID转换为16进制。
jstack PID 打印出进程的所有线程信息,从打印出来的线程信息中找到上一步转换为16进制的线程ID对应的线程信息。

4、最后根据线程的堆栈信息定位到具体业务方法,从代码逻辑中找到问题所在。

 查看是否有线程长时间的watting 或blocked
如果线程长期处于watting状态下, 关注watting on xxxxxx,说明线程在等待这把锁,然后根据锁的地址找到持有锁的线程。
7.内存飚高问题定位思路。

分析: 内存飚高如果是发生在java进程上,一般是因为创建了大量对象所导致,持续飚高说明垃圾回收跟不上对象创建的速度,或者内存泄露导致对象无法回收。

1、先观察垃圾回收的情况

 jstat -gc PID 1000 查看GC次数,时间等信息,每隔一秒打印一次。
 jmap -histo PID | head -20   查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存。

如果每次GC次数频繁,而且每次回收的内存空间也正常,那说明是因为对象创建速度快导致内存一直占用很高;如果每次回收的内存非常少,那么很可能是因为内存泄露导致内存一直无法被回收。

2、导出堆内存文件快照

 jmap -dump:live,format=b,file=/home/myheapdump.hprof PID  dump堆内存信息到文件。

3、使用visualVM对dump文件进行离线分析,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体问题。

上述四个案例来源于博客:https://zhuanlan.zhihu.com/p/269597178


点击面试手册,获取本站面试手册PDF完整版