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 获取。例如:

ini 复制代码
[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 进程信息的远程主机。

测试用例:

ini 复制代码
[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 的性能统计信息。

xml 复制代码
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次。

csharp 复制代码
[root@localhost ~]# jstat -gc -h 2 10217 1s 3

结果演示:

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


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

css 复制代码
jmap [option] <pid>

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

参数选项:

  • -heap:打印出堆内存的概要信息,包括各代(新生代、老年代)的使用情况、GC 配置等。
  • -histo:打印堆内存的直方图,列出每个类的实例数量和总字节大小。帮助识别哪些类占用了大量内存。
  • -dump:<dump-options>:生成堆转储文件。<dump-options> 可以是 live(仅转储活动的对象)、format=b(二进制格式)、file=<filename>(指定转储文件的名称)。

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

ini 复制代码
jmap -dump:format=b,file=heapdump.hprof <pid>
  • -finalizerinfo:打印出正在等待 Finalizer 线程执行的对象信息。
  • -clstats:打印类加载器的统计信息,包括加载的类数量、卸载的类数量等。
  • -printcompilation:打印出即时编译器编译的代码信息。

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

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


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

css 复制代码
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:

ini 复制代码
jinfo -flag MaxHeapSize=2g 10217
  • 查看所有 JVM 标志

    jinfo -Flags 10217

  • 查看所有 Java 系统属性

    jinfo -sysprops 10217

演示效果:

- jstack:打印JVM中线程快照


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

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

xml 复制代码
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的例子。

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

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

后台运行情况:

arduino 复制代码
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

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

eclipse MAT

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

官网下载地址:eclipse.dev/mat/downloa...

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

ruby 复制代码
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诊断工具。

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

功能特性:

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

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

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

相关诊断指令: arthas.aliyun.com/doc/command...

用户案例:github.com/alibaba/art...

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

如何设置JVM调优参数?

JVM参数类型

  1. 标准参数选项

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

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

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

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

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

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

常用的JVM参数有哪些?

参考文章:articles.zsxq.com/id_3w4u6d0x...

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环境变量来做到这一点:

bash 复制代码
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调优案例。通过实操的方式,结合代码和工具详细介绍如何发现问题、分析问题、解决问题。敬请期待......

往期推荐

分库分表设计及常见问题

项目实战中的异步设计

多级缓存设计和实战应用

如何选择分布式事务解决方案?

结尾

共享即共赢。如有帮助,帮忙点赞和在看。关注公众号【码易有道】,定期更新一些工程实践的总结和个人心得。欢迎你的加入,一起学习、交流、做长期且正确的事情!!!

相关推荐
救救孩子把14 分钟前
深入理解 Java 对象的内存布局
java
落落落sss16 分钟前
MybatisPlus
android·java·开发语言·spring·tomcat·rabbitmq·mybatis
万物皆字节22 分钟前
maven指定模块快速打包idea插件Quick Maven Package
java
夜雨翦春韭29 分钟前
【代码随想录Day30】贪心算法Part04
java·数据结构·算法·leetcode·贪心算法
我行我素,向往自由35 分钟前
速成java记录(上)
java·速成
一直学习永不止步41 分钟前
LeetCode题练习与总结:H 指数--274
java·数据结构·算法·leetcode·数组·排序·计数排序
邵泽明42 分钟前
面试知识储备-多线程
java·面试·职场和发展
程序员是干活的1 小时前
私家车开车回家过节会发生什么事情
java·开发语言·软件构建·1024程序员节
煸橙干儿~~1 小时前
分析JS Crash(进程崩溃)
java·前端·javascript
2401_854391081 小时前
Spring Boot大学生就业招聘系统的开发与部署
java·spring boot·后端