JVM调优实战

关键词:JVM、OOM、CPU、Full GC

背景

  Java虚拟机(JVM)在Java技术体系中占据着核心地位。JVM调优能够显著提升应用的响应速度、吞吐量以及资源利用效率,从而保障系统的稳定高效运行。本章为JVM调优前置知识,主要介绍:什么时候需要调优?JVM如何监控和诊断性能、如何设置参数、常用的JVM参数等。

文章导读

什么时候需要调优?

  在进行JVM调优时,需要综合考虑JVM的内存管理、垃圾回收、线程管理等方面,通过合理的配置和参数调整,实现最佳的性能和资源利用效果。

通常出现以下情况就需要JVM调优:

  • 性能问题:例如应用程序的响应时间过长、吞吐量低、频繁发生垃圾回收等情况,可能需要进行JVM调优。
  • 内存问题:当应用程序经常发生内存溢出错误或持续占用过多的内存时,表明堆内存配置不合理或垃圾回收策略需要调整。
  • 并发问题:在高并发环境下,如果应用程序出现线程竞争、死锁或阻塞等问题,可以考虑通过调整线程池大小、线程栈大小等参数来改善并发性能。

JVM调优监控和诊断

那么,当生产遇到上述类似的情况时,我们如何发现问题?一般会通过系统监控来诊断问题。通常诊断的工具有两类:一种是jdk自带命令行工具;一种是借助第三方性能分析工具。

基础配置

  • 主机参数:4核8G
  • Oracle jdk版本:1.8.0_221

Java命令行工具

我们知道,jdk打娘胎出来在${JAVA_HOME/bin}下有很多命令行工具。

获取命令具体参数选项,可以通过command -help 获取。例如:

[root@localhost bin]# jps -help
usage: jps [-help]
       jps [-q] [-mlvV] [<hostid>]

Definitions:
    <hostid>:      <hostname>[:<port>]

下面我们针对一些常用的命令行工具逐一介绍。

- jps:查看正在运行的Java进程

通过jps命令获取Java进程的进程ID(PID)以及主类名称或JAR文件的完整路径名。

参数选项:

  • -l:输出主类或者JAR的完全路径名。例如,运行jps -l命令将列出所有Java进程及其对应的主类名称或JAR文件的完整路径名。
  • -v:输出JVM参数。列出每个Java进程的JVM参数信息。
  • -m:输出JVM启动时传递给main()方法的参数。
  • -V:(特定环境或版本可能支持)提供特定于该环境或版本的输出或功能。
  • <hostid>:指定要查询 Java 进程信息的远程主机。

测试用例:

[root@localhost ~]# jps -mlvV
10217 demo-jvm-1.0-SNAPSHOT.jar
10285 sun.tools.jps.Jps -mlvV -Dapplication.home=/usr/local/java/jdk1.8.0_221 -Xms8m

结果演示:

- jstat:查看JVM统计信息

jstat 是 Java 虚拟机(JVM)自带的监控工具,用于查看 HotSpot JVM 的性能统计信息。

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

参数选项:

基础参数:

  • -t:展示从虚拟机运行到现在的性能数据
  • -h<lines>:每隔lines行展示行头部信息
  • <vmid>:JVM进程的虚拟机标识符。
  • <interval>:两次统计信息收集之间的时间间隔(秒)。
  • <count>:收集统计信息的次数。

性能参数:

  • -class:显示类加载器的统计信息。
  • -compiler:显示即时编译器的统计信息。
  • -gc:显示垃圾收集的统计信息。
  • -gccapacity:显示各内存池的容量和使用情况。
  • -gccause:显示上一次或当前垃圾收集的原因及相关统计信息。
  • -gcnew:显示新生代的垃圾收集统计信息。
  • -gcnewcapacity:显示新生代内存池的容量和使用情况。
  • -gcold:显示老年代的垃圾收集统计信息。
  • -gcoldcapacity:显示老年代内存池的容量和使用情况。
  • -gcpermcapacity:显示永久代的容量和使用情况(Java 8 及以后版本可能不再适用)。
  • -printcompilation:输出被即时编译器编译的方法信息。

测试用例:

通过jstat工具监控指定Java进程(进程ID为10217)的垃圾收集(GC)统计信息,并限制输出结果的行数为2行。每1秒收集一次数据,总共收集3次。

[root@localhost ~]# jstat -gc -h 2 10217 1s 3

结果演示:

- jmap:导出内存映像文件&内存使用情况

jmap 用于生成堆内存映射或堆转储文件(heap dump)。这些文件可以用于后续的堆分析,帮助开发者诊断内存泄漏、内存溢出等问题。

jmap [option] <pid>

其中,<pid> 是要分析的 Java 进程的进程 ID。

参数选项:

  • -heap:打印出堆内存的概要信息,包括各代(新生代、老年代)的使用情况、GC 配置等。

  • -histo:打印堆内存的直方图,列出每个类的实例数量和总字节大小。帮助识别哪些类占用了大量内存。

  • -dump:<dump-options>:生成堆转储文件。<dump-options> 可以是 live(仅转储活动的对象)、format=b(二进制格式)、file=<filename>(指定转储文件的名称)。

例如,要生成一个名为 heapdump.hprof 的堆转储文件,可以使用以下命令:

jmap -dump:format=b,file=heapdump.hprof <pid>
  • -finalizerinfo:打印出正在等待 Finalizer 线程执行的对象信息。

  • -clstats:打印类加载器的统计信息,包括加载的类数量、卸载的类数量等。

  • -printcompilation:打印出即时编译器编译的代码信息。

注意: 生成堆转储文件可能会对正在运行的 JVM 产生性能影响,特别是在堆内存很大的情况下。因此,建议在系统负载较低或处于可接受的范围内时执行此操作。

- jinfo:实时查看和修改JVM配置参数

jinfo 用于实时查看和修改运行中的 Java 进程的 JVM 配置参数。

jinfo [option] <pid>

参数选项:

  • -flag <name>        打印指定名称的 VM 标志的值
  • -flag [+|-]<name>   启用或禁用指定名称的 VM 标志
  • -flag <name>=<value>将指定名称的 VM 标志设置为给定值
  • -Flags             打印 VM 标志
  • -sysprops          打印 Java 系统属性
  • <no option>        同时打印以上两者
  • -h-help      打印此帮助信息

注意jinfo 在 Java 9 及更高版本中才支持参数动态修改,并且并非所有 VM 标志都支持动态修改。修改某些标志可能需要 JVM 重启才能生效。

测试用例:

例如:查看这个进程的 JVM 标志和系统属性,例如:修改其中一个标志(比如 -Xmx,即最大堆内存大小)的值。

  • 查看所有 JVM 标志和系统属性
jinfo 10217
  • 查看特定 JVM 标志的值
jinfo -flag MaxHeapSize 10217

这个命令会输出进程 10217MaxHeapSize(最大堆内存大小)标志的当前值。

  • 修改特定 JVM 标志的值

    请注意,不是所有的 JVM 标志都可以在运行时修改。而且,修改某些标志可能需要 JVM 重启。下面的命令尝试将最大堆内存大小设置为 2GB:

jinfo -flag MaxHeapSize=2g 10217
  • 查看所有 JVM 标志
jinfo -Flags 10217
  • 查看所有 Java 系统属性
jinfo -sysprops 10217

演示效果:

- jstack:打印JVM中线程快照

jstack(JVM Stack Trace):用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)。

线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合。可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。就可以用jstack显示各个线程调用的堆栈情况。

jstack <pid>

参数选项:

  • -F:强制生成线程堆栈。当 jstack <pid> 没有响应(进程挂起)时使用。
  • -m:如果调用到本地方法的话,可以显示C/C++的堆栈
  • -l:打印关于锁的额外信息。

测试用例:

jstack 10217

演示效果:

第三方JVM监控及诊断工具

  使用命令行工具或组合能帮您获取目标Java应用性能相关的基础信息,但它们无法获取方法级别的分析数据,如方法间的调用关系、各方法的调用次数和调用时间等(这对定位应用性能瓶颈至关重要)。而且结果展示不够直观。

你可以如此可爱

  为此,JDK提供了一些内存泄漏的分析工具,如jconsole,jvisualvm等,用于辅助开发人员定位问题,但是这些工具很多时候并不足以满足快速定位的需求。

jvisualvm

  为方便测试,我们在window下使用Visual VM [${JAVA_HOME}/bin/jvisualvm.exe] 来演示效果(功能性详细信息,请自行发掘)

比如:我们在程序中写一个最简单OOM的例子。

public static void main(String[] args) {
    List<Object> list = new ArrayList<>();
    while (true) {
        list.add(new Object());
    }
}

运行一段时间,点击运行jvisualvm.exe,显示内存使用情况

后台运行情况:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

尽管直观地看到CPU、线程数、对空间的直观变化。但是这些信息有时候并不能满足我们的分析和定位需求。因此,下边介绍2个重常用的第三方工具。

eclipse MAT

MAT: MAT(Memory Analyzer Tool): 基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。

官网下载地址:https://eclipse.dev/mat/downloads.php

为演示效果,设置JVM参数:

java -jar -Xms8m -Xmx8m -XX:+PrintGC -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./ demo-jvm-1.0-SNAPSHOT.jar 

运行一段时间,出现OOM异常,并生成了转存文件:java_pid11469.hprof。

本地用mat工具导入并打开刚刚的转存文件,如下图:

那么,如何辅助我们发现OOM具体发生在哪里呢?

事实上,这和后台进程显示是一致的。

从上面的报告中,MAT工具使我们看出有内存泄漏的情况,并能够得知内存泄漏具体发生位置。

阿里 Arthas

Arthas:Alibaba开源的Java诊断工具。

官方使用教程:https://arthas.aliyun.com/

功能特性:

  • 支持在线排查,无需重启
  • 动态跟踪Java代码
  • 实时监控JVM状态

当你遇到以下类似问题时,Arthas可以帮助你解决(根据学习资料整理):

  1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
  2. 修改的代码是否生效?难道没commit?或者分支搞错了?
  3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
  4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
  5. 如何以全局视角来查看系统的运行状况?
  6. 有什么办法可以监控到JVM的实时运行状态?
  7. 怎么快速定位应用的热点,生成火焰图?

相关诊断指令:https://arthas.aliyun.com/doc/commands.html

用户案例:https://github.com/alibaba/arthas/issues?q=label%3Auser-case

  具体使用和异常处理,请在官方文档提示下学习和测试。也可以再实际生产中遇到相关问题,能快速定位。

如何设置JVM调优参数?

JVM参数类型

  1. 标准参数选项

通常用于显示版本信息或帮助文档。例如:

  • -version:输出JVM的版本信息。
  1. -X参数选项

通常用于调试、性能调优或诊断。例如:

  • -Xms<size>:设置JVM初始堆大小。
  1. -XX参数选项

用于调整JVM的内部行为、性能优化、诊断等。

  • -XX:+PrintGCDetails:输出详细的垃圾回收日志。

常用的JVM参数有哪些?

参考文章:https://articles.zsxq.com/id_3w4u6d0xqhfd.html

1. 设置堆、栈、方法区等内存大小

  • -Xmx4g: 设置进程占用的最大堆空间大小为4GB,超出后会导致OutOfMemoryError。
  • -Xms2g: 设置初始化堆空间大小为2GB。
  • -Xmn1g: 设置年轻代大小为1GB,官方推荐配置为整个堆的3/8。
  • -XX:NewRatio=n: 设置年轻代和老年代空间大小的比值。
  • -Xss512k: 设置每个线程占用的内存大小为512KB。
  • -XX:SurvivorRatio=n: 设置年轻代中Eden区与Survivor区的比值,例如n=4时,Eden和Survivor的比值为4:2。
  • -XX:MetaspaceSize=512m: 设置元空间(Metaspace)的初始大小为512MB。
  • -XX:MaxMetaspaceSize=512m: 设置元空间(Metaspace)增长的上限,防止无限制地使用本地内存。
  • -XX:MinMetaspaceFreeRatio=N: 设置Metaspace GC后空闲空间的最小比例,控制Metaspace的增长速度。
  • -XX:MaxMetaspaceFreeRatio=N: 设置Metaspace GC后空闲空间的最大比例,控制Metaspace的释放。
  • -XX:MaxMetaspaceExpansion=N: 设置Metaspace增长时的最大幅度。

设置垃圾收集器

  • -XX:+UseSerialGC: 设置使用串行收集器。
  • -XX:+UseParallelGC: 设置使用并行收集器。
  • -XX:+UseParalledlOldGC: 设置使用并行年老代收集器。
  • -XX:+UseConcMarkSweepGC: 设置使用并发收集器。
  • -XX:ParallelGCThreads=n: 设置并行收集器使用的线程数。
  • -XX:MaxGCPauseMillis=n: 设置并行收集的最大暂停时间。
  • -XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比,1/(1+n)。
  • -XX:+DisableExplicitGC: 禁止外部调用System.gc()
  • -XX:MaxTenuringThreshold: 设置年轻代对象复制到老年代前的最大复制次数。

垃圾回收信息统计

  • -XX:+PrintGC: 打印垃圾回收信息。
  • -XX:+PrintGCDetails: 打印详细的垃圾回收信息。
  • -XX:+PrintGCTimeStamps: 打印每次垃圾回收前程序未中断的执行时间。
  • -Xloggc:filename: 把GC日志存入指定文件。
  • -XX:+PrintGCApplicationStoppedTime: 打印垃圾回收期间程序暂停的时间。
  • -XX:+PrintGCApplicationConcurrentTime: 打印每次垃圾回收前程序未中断的执行时间。
  • -XX:+PrintHeapAtGC: 打印GC前后的详细堆栈信息。
  • -XX:+HeapDumpOnOutOfMemoryError: 在OutOfMemoryError时生成堆转储。
  • -XX:HeapDumpPath=/dump: 设置堆转储文件的路径。

如何设置JVM运行参数?

  通常应用一般部署在Linux系统中。设置JVM运行参数通常涉及在启动Java应用程序时通过命令行传递参数。这些参数可以直接附加在java命令后面,或者通过环境变量来设置。

命令行设置

例如,要设置初始堆大小为512MB和最大堆大小为1024MB,可以使用命令行:

java -jar -Xms512m -Xmx1024m App.jar

其中,-Xms-Xmx 分别是设置初始堆大小和最大堆大小的参数,App 是你的Java应用程序的类名。

环境变量设置

可以通过设置JAVA_OPTS环境变量来做到这一点:

export JAVA_OPTS="-Xms512m -Xmx1024m"
java $JAVA_OPTS App

或者,如果你使用Tomcat应用服务器,可以在tomcat/bin/catalina.sh启动脚本中设置CATALINA_OPTS或相应的环境变量。

参数选择依据

当选择JVM参数时,可以考虑以下几点:

  1. 内存需求:根据应用程序的需求,设置-Xms(初始堆大小)和-Xmx(最大堆大小)参数。

  2. 垃圾收集器:使用-XX:+UseConcMarkSweepGC-XX:+UseParallelGC-XX:+UseG1GC等参数来选择适合的应用程序的垃圾收集器。

  3. 性能调优:使用-XX:+PrintGC-XX:+PrintGCDetails等参数来打印垃圾收集日志,帮助了解应用程序的性能,并进行调优。

  4. 诊断:使用-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/path/to/dump等参数来在发生OutOfMemoryError时生成堆转储文件,便于后续分析。

  5. 线程数:如果你的应用程序是多线程的,考虑使用-XX:ParallelGCThreads-XX:ConcGCThreads来设置并行和并发垃圾收集器的线程数。

  6. 预测试:在生产环境部署之前,模拟业务场景和硬件设备,在测试环境中验证所选参数的效果,确保它们能够满足应用程序的需求。

  7. 官方文档和社区:当然,可以参考Oracle官方文档和Java社区的建议,了解参数的最佳实践和使用场景。

  因此,合适的JVM参数取决于应用程序的特定需求和环境。系统参数的设置也是根据实际情况逐渐调整,而不是一蹴而就的。最佳做法是不断监视应用程序的性能,并根据需要进行调整。

总结

  本文主要对:JVM调优的背景、产生原因、监控和诊断工具、常用JVM参数进行了介绍。限于篇幅,我们将在下一章具体介绍生产环境常见的JVM调优案例。通过实操的方式,结合代码和工具详细介绍如何发现问题、分析问题、解决问题。敬请期待......

最后推荐一下我的知识星球:Java突击队】,里面干货非常多,可以帮你系统性的提升技术,目前好评如潮,非常值得一看。

星球中包含13大优质专栏:

每一个专栏的干货都非常多。

最近商城项目中更新了:支付宝支付、分库分表、分片算法优化、手写动态定时任务、数据脱敏、动态workId、hanlp敏感词校验,手写分布式ID生成器、分布式限流、手写Mybatis插件、两级缓存提升性能、MQ消息通信、ES商品搜索、OSS服务对接、失败自动重试机制、接口幂等性处理、WebSocket消息推送、百万数据excel导出、用户异地登录检测、freemarker模版邮件发送、代码生成工具、重复请求自动拦截、自定义金额校验注解等等一系列功能。

扫描下方二维码即可加入星球:



相关推荐

  • 英伟达又向开源迈了一步「GitHub 热点速览」
  • 如何使用Proxy实现JavaScript中的观察者模式
  • 推荐一款开源、强大的云存储工具,堪称神器!
  • 时序表示学习的综述!
  • [开源]智能生产管理系统,是一个集成化、智能化的企业级应用软件
  • 公司新来一个同事,把 BigDecimal 运用的炉火纯青!
  • OPC UA点位到底是个啥???
  • 18.1K Star稀奇炫酷!!!全栈 Web 应用,纯 Python 编写
  • ICML2024: 华中科大发现大模型具有自我认知
  • 中科院张家俊团队最新综述,谈大模型研究的新领域:多模型协作
  • IMO数学竞赛第5题是何方神圣?大模型全军覆没了…
  • 专访 Luma AI 首席科学家:我们更相信多模态的 Scaling Law
  • 如何在小红书做出爆款?先发够1000条笔记
  • 苹果小模型来了
  • AI驱动下的新能源材料研究、发现与 NVIDIA Modulus 加速材料计算|在线研讨会预告
  • 大模型风向变了,OpenAI苹果掉头布阵
  • AI产品沉思录:浏览器插件
  • 如果不赚钱,AI 的繁荣可能会消失,就像淘金热消失一样
  • 职场中那些做到中层才明白的道理:1.解决问题而不是干活;2.顺着人性做事;3.最好的沟通:勤汇报
  • Spring Boot集成xjar快速入门Demo