Skip to content

Navigation Menu

Sign in
Sign up

JVM调优工具&步骤

bedlamite edited this page Aug 8, 2023 · 6 revisions

调优调什么?

一般的话,有两个方面:内存方面、线程方面。

内存方面的话,会包括如堆内存各区域的分配占用、GC停顿次数和时间、内存泄漏等问题。

线程方面的话,会包括如死锁、CPU占用等问题。

如何调优?

主要分为三步:监控、分析、调整。

监控指的是监控JVM的状态信息,主要是堆内存占用、线程使用、代码隐患、IO等。

分析指的是分析堆dump文件,如使用MAT分析工具分析heap dump的情况,分析是代码导致的占用问题、内存分配不足导致的、线程一直占用不释放等问题。

调整指的是找出问题然后并优化代码,如果是代码造成的则优化代码,如果是内存分配问题则重新优化JVM参数,如果是线程占用导致的则优化其代码等。

调优的目标

GC时间足够小、GC次数足够小、将转移到老年代的对象数量降低到最小、减少Full GC的次数、减少Full GC的时间等。

常见的调优策略

减少创建对象的数量、减少使用全局变量和大对象、平衡新生代-老年代的内存占用大小、选择合适的GC收集器并设置合理的参数(根据业务实际情况而定)等。

JVM调优的思考

  1. 多数的Java应用是不需要在服务器上进行GC优化的。

  2. 多数能导致GC问题的Java应用,都不是因为参数设置错误,而是因为代码问题。

  3. 有条件的话,在应用上线前,应该考虑将机器的JVM参数设置到最合适的参数(最优解参数)。

  4. JVM优化是到最后不得已才采用的手段。并不是一上来就无脑JVM调优

  5. 在实际使用过程中,分析JVM情况并优化代码比优化JVM本身的次数要多得多。

调优经验

  1. 需要考虑到client模式和Server模式的选择。

    • VM的Client和Server是指JVM的两种不同的编译模式。总体来说,Client模式更适用于桌面应用程序,Server模式更适用于服务器应用程序。但是,随着JVM的不断改进,两种模式的性能差距已经不像以前那么明显了。
  2. 要想GC的时间小必须要一个更小的堆,而要保证GC次数足够少,又必须要保证一个更大的堆,这两个是有冲突的,只能取其平衡。

  3. 针对堆的设置,一般通过-Xms -Xmx设置其最小值、最大值。通常会把最小值、最大值设置为相同值(为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间)。

  4. 默认情况下新生代和老年代按照1:2比例分配堆内存。可以通过-XX:NewRatio来调整比例。也可以通过参数-XX:NewSize -XX:MaxNewSize来指定新生代内存大小。同样为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间。通常会将-XX:NewSize -XX:MaxNewSize设置为相同值。

    • 若当设置 -XX:NewRatio=3 时,表示新生代和老年代的比例为1:3。也就是说,如果老年代有3个单位的内存,那么新生代就有1个单位的内存。
  5. 根据业务实际需求场景,合理的划分新生代和老年代的内存大小。

  6. 如果应用可能会存在大量的临时对象或大对象,则新生代应该适当增大。如果引用可能会存在较多的持久对象,则老年代应该适当增大。在两者选择如何调整内存时,应优先考虑Full GC尽量少的原则,让老年代尽量缓存常用对象。(或者说修改老年代晋升阈值)

  7. 或者在观察应用一段时间后,看其在峰值老年代会占用多少内存,在不影响Full GC的前提下,可以根据实际情况增大新生代内存。但是应该给老年代至少预留1/3的增长空间。

  8. 线程堆栈的设置:默认情况下每个线程会开辟1M的堆栈(可以理解为虚拟机栈),用于存放栈帧(局部变量表、操作数栈、动态连接、返回地址等)。但对于大多数应用而言这个1M的默认值过大了,一般设置256K即可。在内存不变的情况下,减少每个线程的堆栈,可以产生更多的线程。

内存泄漏现象

  1. GC时间越来越长,且Full GC时间也延迟到好几秒。

  2. Full GC执行次数越来越频繁,最频繁的时候时隔不到1分钟就触发一次Full GC。

  3. 老年代的内存越来越大,并且每次Full GC后老奶奶带的内存没有被释放。

  4. 老年代内存被占满。

内存泄漏的解决方式:一般是根据GC前后情况对比,如抓取heap dump文件,同时根据对象引用情况分析,辅助查找泄漏点。

栈溢出的情况:会抛java.lang.StackOverflowError,一般是递归调用、或者循环调用导致的。

调优的步骤

调优的步骤的重点是调优的过程、方法以及思路。

一般信息如堆使用率、CPU占用率、GC暂停时间是非常重要的三个指标。对于Java应用而言,GC暂停时间是最值得关注的指标。

  1. 查看CPU的占用率。

  2. 查看内存的分配和使用量。

  3. 查看垃圾回收的频率、暂停时间、垃圾回收区域等。

  4. 文件IO、网络Socket IO、远程IO阻塞等。

JVM常见线上问题的排查方法&案例

在处理 Java 应用程序的线上问题时,常会遇到系统运行缓慢,所有页面都出现延迟卡顿的现象,如果能排除掉网络本身的问题,那我们该如何对这类问题进行分析定位呢?

大多数情况下,最终排查发现,大概率是自己写的Java代码有问题。比如最常见的开发问题就是代码中的循环或定时任务写得不严谨,导致某个操作一直在不停的执行,最终耗尽资源,其实这种问题完全是可以避免的。

另外,在排查问题之前,如果出现的问题导致线上系统不可用,那么首先需要做的,是导出我们需要的系统信息,比如堆转储文件,然后重启系统(或回退到上一个稳定版本等),尽快保证系统的可用性。

其实排查的思路也很简单,那就是搜集尽可能多的报错日志,然后根据这些报错信息,缩小问题代码范围,找到具体是哪几行代码写得不妥,再做针对性的优化即可。

CPU飙高

思路一

  1. 先找到占用CPU高的进程。

    • 执行top或者top -c命令,显示进程运行信息列表,再键入大写P,列表会按照CPU使用率降序排列。

    • 补充:需要注意的是,如果Java程序部署在Docker中,则需要先进入Docker容器,然后再容器内执行top命令:

    sudo docker exec -it [containerId] bash
    • 记录一下CPU占用过高的Java进程PID。后续命令会用到。
  2. 再找到占用CPU高的线程。

    • 一般情况下一个进程中会有多个线程,执行top -Hp PID,会显示进程PID的线程列表,假如大写P后,列表会按CPU使用率降序排列。然后记录一下其中一个线程的PID。
  3. 最后找到占用CPU高的线程对应的业务代码。

    • 需要将线程的PID转为十六进制,因为jstack打印的线程栈信息中的线程PID是十六进制显示的。转换方式: printf "%x\n" PID

    • 假设线程PID转为十六进制后是e。最后通过 jstack 检索进程(进程 PID = 6)中最耗 CPU 资源的线程(线程 PID = e)的线程栈信息,执行 jstack 命令:

    jstack 6 | grep "0xe" -C5 --color
    
    • 这样就定位到了问题引起的具体业务代码处了。但如果只通过 jdk 生成的线程名称,以及只包含 jdk 代码的线程栈信息,是无法定位到业务代码的。因此,给线程取一个与业务处理相关的名称,对快速定位问题尤为重要

如果发现信息中包含文本「Full GC (System.gc())」,说明代码或第三方依赖包中有显式的 System.gc() 调用。

如果发现信息中有类似Found one Java-level deadlock文本,说明代码产生死锁。

......

思路二

  1. 生成head dump(堆转储文件)。它是一个二进制文件,保存了某一时刻 JVM 堆内存中的对象使用情况。与之相似的还有 thread dump,用于记录 CPU 信息。

    第一种方式:可以使用jmap命令生成堆dump。jmap -dump:live,format=b,file=heap.bin PID,然后通过dump分析工具分析此二进制文件。

    第二种方式:但使用 jmap 命令有一点不好的地方是,内存溢出是某个时间点发生的事情,跟执行 jmap 命令的时候获取到的转储文件相比,存在时间差问题。所以另一种方式是设置-XX:+HeapDumpOnOutOfMemoryError虚拟机参数后,它就会在程序发生内存溢出的时候,自动生成一个二进制的堆转储文件,这个转储文件的后缀为 hprof,而且这个文件几乎是在发生内存溢出的同时生成的,故而会更准确些。这个文件的存储路径也可以通过参数-XX:HeapDumpPath指定:(如果不指定,则默认在当前工作目录下生成 java_pid.hprof 文件)。并且需要保证文件路径存在。

    建议提前添加 -XX:+HeapDumpOnOutOfMemoryError 参数,而不是用 jmap。有几点要注意,内存分析可能需要比对多个堆转储文件,除了在发生内存溢出时自动生成的堆转储文件外,我们在获取其他时间点的堆转储文件时一定要找准时机,避免得到一个没用的快照。另外,每次执行堆转储,都会对 JVM 进行「冻结」,所以在生产环境中不能执行太多次的 dump 操作,一旦系统缓慢或者卡死,麻烦就大了。(快照中也可能含有机密信息,例如密码等,这时候如果企业有安全限制,还需先获得在生产服务器上执行堆转储的权限)

  2. 分析heap dump文件。

    分析堆内存转储文件主要用到命令 jhat,jhat是 JDK 自带的用于分析 heap dump 文件的工具。使用如下命令可以将堆转储文件的分析结果以 HTML 网页的形式进行展示:(如果系统有可视化界面,也推荐用工具MAT来分析,该工具既可以安装到 Eclipse 上使用,也可以独立运行使用,解压之后双击 MemoryAnalyzer.exe 即可)

    jhat heap.hprof -J-Xmx8192m

    参数 -J-Xmx8192m 用来设置命令使用的内存大小,当然首先要保证执行命令的物理机器有足够大的内存,因为堆转储文件往往很大,不加这个参数的话有可能会报内存溢出异常,导致分析失败。

    执行成功后,就可以访问了,默认端口7000。

    那么该如何从HTML中获取想要的有价值的信息呢?需要拉到网页最底部,找到 Other Queries,然后选择 Show instance counts for all classes (excluding platform),找除了 JDK 本身之外实例数最多的类。

    看看排名最前的几个类,就可以找出可能会分配大量对象的代码,基本就能定位到问题代码的位置了。

内存溢出

当Java 应用消耗大量内存,导致出现异常:java.lang.OutOfMemoryError: GC overhead limit exceeded。一般导致的原因有:

  1. JVM内存过小。

  2. 产生的垃圾过多,无法回收。(对象、线程创建太多,且一直未释放;IO操作后未关闭流,资源无法释放;消费者速度慢,生产者速度快,任务队列中的对象不断堆积)

  3. 代码有漏洞。

大都数情况下,通过配置JVM启动参数增大堆内存并不能从根本上解决这个问题,只能延迟错误发生的事件。要解决内存溢出问题需要搞清楚两点:哪些对象占用了大量内存?这些对象是在代码中哪部分分配的?

而解决办法就是找到占用内存大的地方,然后优化相关代码。怎么找?依然是通过堆转储文件去找。

而除了直接分析堆转储文件外,还有一些值得学习的命令可以辅助我们排查问题,有时甚至只用这些命令就能定位到问题代码的位置。

  • jmap -histo

如果想知道什么对象消耗内存最大,那么可以执行命令:

jmap -histo:live PID | head -n 100

结果会以表格的方式显示存活对象的信息,并按对象所占的 bytes 大小进行降序排列。如果发现某类对象占用内存很大,比如达到了几个 G,那大概率有问题。

  • pstree

如果想知道创建了多少线程,可以执行命令:

pstree -p <pid> | wc -l

结果会显示进程内的线程数,还有每个线程的线程栈内存。

  • jmap -heap

如果想查看堆(新生代、老年代)内存分配大小及使用情况,可以执行命令:

jmap -heap pid

可用来确认是不是整体内存分配太小。

  • jstat -gc

如果想查看实时的新生代、老年代内存使用情况及 GC 情况,可以执行命令:

jstat -gc 6 1000

其中,6 为进程ID,1000 为数据刷新间隔的毫秒数,执行结果会显示各个区内存使用情况及 GC 的情况,结果列表表头含义:

  • EC:Eden 区容量;

  • EU:Eden 区已使用量;

  • OC:Old 区容量;

  • OU:Old 区已使用量;

  • YGC:YongGC 次数(当新生代被填满时执行一次,回收速度快);

  • YGCT:YongGC 耗时;

  • FGC:FullGC 次数(当老年代被填满时执行一次,回收时应用程序会整个停下来直到回收完成);

  • FGCT:FullGC 耗时。

极个别功能运行缓慢

除了前面一旦出现就很严重的两种情况外,还有三种情况,也会导致系统功能运行缓慢,但它的影响范围没那么大,出现问题后不会导致整个系统都不可用,最多只会让一个具体的功能变得很慢。跟前面的情况不同的是,如果出现这三种问题,通过查看 CPU 和系统内存情况是无法排查出问题原因的,因为它们只是局部的阻塞,CPU 和系统内存使用率都不高,所以需要用一些其他的手段来排查。

代码中有阻塞性的操作

代码阻塞使用不当,会导致调用相关功能时比较耗时。比较典型的例子是,访问某个接口经常需要 2~3s 才能返回,而且是不定时出现。

定位这种问题的思路如下:首先找到该接口,通过压测工具不断加大访问力度,如果该接口中有某个位置比较耗时,由于访问的频率非常高,那么大多数的线程最终都将阻塞于该阻塞点,这样通过多个线程的堆栈日志,就可以定位到接口中耗时的代码位置。

某个线程进入 WAITING 状态

定位这种问题的思路如下:通过 grep 在 jstack 日志中找出所有处于 TIMED_WAITING 状态的线程,将其导出到某个文件中,如 log1.log,等待几分钟之后,再次对 jstack 日志进行 grep,将其导出到另一个文件,如 log2.log。重复上述操作,待导出 4、5 个文件之后,再对导出的文件进行对比,找出在这几个文件中一直都存在着的用户线程,基本上就能确定是哪里出问题了, 因为正常的请求线程是不会在 30s 之后还是处于等待状态的。

死锁

jstack PID
# 拉到末尾:Found 1 deadlock.

jstack 自带检查死锁的功能,会在日志底部打印出代码中存在哪些死锁,以及每个死锁的线程堆栈信息,我们可以根据这些很容易定位到发生死锁的代码位置。

堆内存诊断工具

  • jps:查看当前系统中有哪些 Java 进程。

    jps [-q] [-mlvV] [hostid]
    jps [-help]
    • 命令参数说明:

      • -q:不显示主类名称、jar文件名以及传递给主方法的参数,只显示本地虚拟机的唯一ID。

      • -m:显示Java虚拟机启动时传递给主方法的参数。

      • -l:显示主类的完整包名,如果运行的是jar文件,也会显示jar文件的完整路径。

      • -v:显示Java虚拟机启动时传递的JVM参数。

      • -V:不显示主类名称、jar文件名以及传递给主方法的参数,只显示本地虚拟机的唯一ID。

      • hostid:指定的远程主机,可以是ip地址和域名, 也可以指定具体协议,端口。若不指定,默认显示本机的Java虚拟机的进程信息。

      • -help:显示jps命令的帮助信息。

  • jmap:查看堆内存某一时刻的占用情况。(不要在生产环境中频繁使用jmap命令,因为它会导致Java进程暂停一段时间,影响业务运行。)

    jmap [option] <pid>
    jmap -help
    • 命令参数说明option:

      • -heap:显示堆的概要信息。如堆的配置信息、GC算法、堆内存使用信息等。

      • -histo[:live]:显示堆中对象的统计信息。包括对象数量、占用大小、全类名。若指定了live则表示只计算活动的对象。

      • -clstats:显示类加载器的统计信息。

      • -finalizerinfo:显示正等候回收的对象的信息。

      • -dump:[live,],format=b,file=<file>:生成某一时刻堆的快照。若指定live则表示只转储堆中活动对象。默认所有对象。

      • -F:可以在无法连接Java进程时强制执行,但可能会导致进程暂停。

  • jconsole:图形界面,多功能检测工具(支持检测线程、CPU..),可以连续检测。

  • jvirsualvm:图形化界面,和jconsole功能类似,但jvirsualvm能够抓取堆内存的快照。

  • jinfo:用于查看Java进程运行的JVM参数及系统属性。(可以动态修改部分参数)

    jinfo [option] <pid>
    jinfo -help
    • 命令参数说明option:

      • -flag <name>:要打印已命名的VM标志的值。

      • -flag [+|-]<name>:开启或关闭对应名称的参数。

      • -flag <name>=<value>:修改指定参数的值。

      • -flags:查看JVM非默认的参数值。

      • -sysprops:输出当前JVM运行时的系统属性。

  • jstat:实时查看堆内存各区域的使用量、加载类的数量。

    jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
    jstat -help|-options
    • Options选项:一般使用 -gcutil 查看gc情况

      • -compiler:查看HotSpot中即时编译器编译情况的统计

      • -gc:查看JVM中堆的垃圾收集情况的统计

      • -gccapacity:显示各个代的容量以及使用情况

      • -gccause:查看垃圾收集的统计情况(这个和-gcutil选项一样),如果有发生垃圾收集,它还会显示最后一次及当前正在发生垃圾收集的原因

      • -gcmetacapacity:显示关于metaspace大小的统计信息。

      • -gcnew:查看新生代垃圾收集的情况,new对象的信息

      • -gcnewcapacity:用于查看新生代的存储容量情况,new对象的信息及其占用量

      • -gcold:用于查看老生代及持久代发生GC的情况,old对象的信息

      • -gcoldcapacity:用于查看老生代的容量,old对象的信息及其占用量

      • -gcutil:查看新生代、老生代及持代垃圾收集的情况

      • -printcompilation:当前VM执行的信息

    • -t:在打印列加上Timestamp列,用于显示系统运行时间。

    • -h:指定输出多少行。

    • vmid:pid。

    • interval:打印间隔时间,单位秒或者毫秒。

    • count:打印次数,默认无数次。

  • jstack:显示目标Java进程中线程之间的栈轨迹、线程状态、锁状况等信息,还可以检测死锁。

    jstack [-l] <pid>
    jstack -help
    • -F:可以在无法连接Java进程时强制执行,但可能会导致进程暂停。

    • -l:显示关于锁的附加信息。

    • -m:打印虚拟机栈、本地方法栈的栈信息,一般应用排查不需要使用。

  • 其它命令...

AltStyle によって変換されたページ (->オリジナル) /