JVM调优
详细阐述内存溢出(OOM、OutOfMemeory)、CPU飙高、GC频繁等JVM问题的排查、定位,以及调优
1.JVM调优步骤
性能的调优总结下来都是三个步骤,即发现问题、定位问题、解决问题
1.监控发现问题
2.工具分析问题
3.性能调优
1.监控发现问题
通过监控工具例如**Prometheus+Grafana**,监控服务器有没有以下情况,有的话需要调优:
- 1.GC频繁
- 2.CPU负载过高
- 3.OOM
- 4.内存泄露
- 5.死锁
- 6.程序响应时间较长
2.工具定位问题
使用分析工具定位oom、内存泄漏等问题。
1.调优依据
JVM调优时,吞吐量和停顿时长两者无法兼顾,吞吐量提高的代价是停顿时间拉长。
所以,如果应用程序跟用户基本不交互,就优先提升吞吐量。如果应用程序和用户频繁交互,就优先缩短停顿时间。
2.JDK自带的命令行调优工具
2.1 常用命令总结
-
jps:查看正在运行的 Java 进程。jps -v查看进程启动时的JVM参数;
-
jstat:查看指定进程的 JVM 统计信息。jstat -gc查看堆各分区大小、YGC,FGC次数和时长。如果服务器没有 GUI 图形界面,只提供了纯文本控制台环境,它是运行期定位虚拟机性能问题的首选工具。
-
jinfo:实时查看和修改指定进程的 JVM 配置参数。jinfo -flag查看和修改具体参数。
-
jstack: 打印指定进程此刻的线程快照。定位线程长时间停顿的原因,例如死锁、等待资源、阻塞。如果有死锁会打印线程的互相占用资源情况。
2.2 jps:查看正在运行的 Java 进程
jps(Java Process Status):显示指定系统内所有的 HotSpot虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程。
说明:对于本地虚拟机进程来说,进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的,是唯一的。
1.基本使用语法:
jps [options参数] [hostid参数]
2.代码示例
一个阻塞状态的线程,等待用户输入:
public class ScannerTest {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);String info = scanner.next();}
}
3.输入 jps 查看进程
4.options 参数:
-
q:仅仅显示 LVMID(local virtual machine id),即本地虚拟机唯一 id。不显示主类的名称等
-
-l:输出应用程序主类的全类名或如果进程执行的是 jar 包,则输出 jar 完整路径
-
-m:输出虚拟机进程启动时传递给主类 main() 的参数
-
-v:列出虚拟机进程启动时的 JVM 参数。比如:-Xms20m -Xmx50m 是启动程序指定的 jvm 参数
5.hostid 参数
RMI 注册表中注册的主机名。如果想要远程监控主机上的 java 程序,需要安装 jstatd。
对于具有更严格的安全实践的网络场所而言,可能使用一个自定义的策略文件来显示对特定的可信主机或网络的访问,尽管这种技术容易受到 IP 地址欺诈攻击。
如果安全问题无法使用一个定制的策略文件来处理,那么最安全的操作是不运行 jstatd 服务器,而是在本地使用 jstat 和 jps 工具
2.3 jstat:查看 JVM 统计信息
1.概述
jstat(JVM Statistics MonitoringTool):用于监视虚拟机各种运行状态信息的命令行工具。
它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。在没有 GUI 图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。
常用于检测垃圾回收问题以及内存泄漏问题。
2.基本使用语法
查看命令相关参数:jstat-h 或 jstat-help
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
#[-t]是程序开启到采样的运行时间
#[-h<lines>]周期性输出,每隔多少行打印一次表头.
#interval查询间隔
#count查询次数
#vmid 是进程 id 号
 {ArrayList<byte[]> list = new ArrayList<>();for (int i = 0; i < 1000; i++) {byte[] arr = new byte[1024 * 100];//100KBlist.add(arr);try {Thread.sleep(120);} catch (InterruptedException e) {e.printStackTrace();}}}
}
JVM 参数: -Xms60m -Xmx60m -XX:SurvivorRatio=8
后面的参数代表 1000 毫秒打印一次,一个打印 10 次。
-
-gccapacity:
显示内容与 -gc 基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间
-
-gcutil:
显示内容与 -gc 基本相同,但输出主要关注已使用空间占总空间的百分比
-
-gccause:
与 -gcutil 功能一样,但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因
-
-gcnew:
显示新生代 GC 状况
-
-gcnewcapacity:
显示内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间
-
-geold:
显示老年代 GC 状况 -
-gcoldcapacity:
显示内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间 -
-gcpermcapacity:
显示永久代使用到的最大、最小空间
3.3 JIT 相关的:
- -compiler:
显示 JIT 编译器编译过的方法、耗时等信息
jstat -compiler
- -printcompilation:
输出已经被 JIT 编译的方法
jstat -printcompilation
3.4 其他参数:
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
- interval 参数:用于指定输出统计数据的周期,单位为毫秒。即:查询间隔
- count 参数:用于指定查询的总次数
- -t 参数:可以在输出信息前加上一个 Timestamp 列,显示程序的运行时间。单位:秒
比较 Java 进程的启动时间以及总 GC 时间(GCT 列),或者两次测量的间隔时间以及总 GC 时间的增量,来得出 GC时间占运行时间的比例。
如果该比例超过 20%,则说明目前堆的压力较大;如果该比例超过 90%,则说明堆里几乎没有可用空间,随时都可能抛出 OOM 异常。
- -h 参数:可以在周期性数据输出时,输出多少行数据后输出一个表头信息
jstat -t
可以在输出信息前加上一个 Timestamp 列,显示程序的运行时间。单位:秒。
jstat -t -h
可以在周期性数据输出时,输出多少行数据后输出一个表头信息。
4.OOM案例
jstat判断内存溢出(OOM):比较GC时长占运行时长的比例
我们可以比较 Java 进程的启动时长以及总 GC 时长 (GCT 列),或者两次测量的间隔时长以及总 GC 时长的增量,来得出 GC时长占运行时长的比例。
1.如果该比例超过 20%,则说明目前堆的压力较大;
2.如果该比例超过 98%,则说明这段时期内几乎一直在GC,堆里几乎没有可用空间,随时都可能抛出 OOM 异常。
示例:统计两次测量的时间间隔内,GC 时长占运行时长的比例:
使用jstat统计GC信息,并显示进程启动时间、统计间隔1000ms、统计20次
5.内存泄漏案例
比较老年代内存量上涨速度
每隔一段较长的时间采样多组 OU(老年代内存量) 的最小值,如果这些最小值在上涨,说明无法回收对象在不断增加,可能是内存泄漏导致的。
-
在长时间运行的 Java 程序中,我们可以运行 jstat 命令连续获取多行性能数据,并取这几行数据中 OU 列(Old Used,已占用的老年代内存)的最小值
-
然后,我们每隔一段较长的时间重复一次上述操作,来获得多组 OU 最小值。如果这些值呈上涨趋势,则说明该 Java 程序的老年代内存已使用量在不断上涨,这意味着无法回收的对象在不断增加,因此很有可能存在内存泄漏(不再使用的对象仍然被引用,导致GC无法回收)。
2.4 jstack 打印指定进程此刻的线程快照
jstack(JVM Stack Trace):用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪)
线程快照:该进程内每条线程正在执行的方法堆栈的集合。
生成线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时,就可以用jstack 显示各个线程调用的堆栈情况。
thread dump 中,要留意下面几种状态:
- 死锁,Deadlock(重点关注)
- 等待资源,Waiting on condition(重点关注)
- 等待获取监视器,Waiting on monitor entry(重点关注)
- 阻塞,Blocked(重点关注)
- 执行中,Runnable
- 暂停,Suspended
- 对象等待中,Object.wait() 或 TIMED_WAITING
- 停止,Parked
1.代码示例(死锁)
package com.sun.jvm;/*** @author lk* @version 1.0.0* @Description TODO* @createTime 2025/5/10 21:59*/
public class DeadLock {Object o1= new Object();Object o2= new Object();public void thread1() throws InterruptedException {synchronized (o1){Thread.sleep(500);synchronized (o2){System.out.println("线程1成功拿到两把锁");}}}public void thread2() throws InterruptedException {synchronized (o2){Thread.sleep(500);synchronized (o1){System.out.println("线程2成功拿到两把锁");}}}public static void main(String[] args) {DeadLock deadLock = new DeadLock();new Thread(()->{try {deadLock.thread1();} catch (InterruptedException e) {e.printStackTrace();}}).start();new Thread(()->{try {deadLock.thread2();} catch (InterruptedException e) {e.printStackTrace();}}).start();}
}
运行后,在命令行使用该命令:
3.JDK自带的可视化监控工具
- jconsole
- JVisual VM:Visual VM可以监视应用程序的 CPU、GC、堆、方法区、线程快照,查看JVM进程、JVM 参数、系统属性。
4.MAT分析堆转储文件
MAT(Memory Analyzer Tool)工具是一款功能强大的 Java 堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。
MAT可以解析Heap Dump(堆转储)文件dump.hprof,查看GC Roots、引用链、对象信息、类信息、线程信息。可以快速生成内存泄漏报表。
MAT 可以分析 heap dump 文件。在进行内存分析时,只要获得了反映当前设备内存映像的 hprof 文件,通过 MAT 打开就可以直观地看到当前的内存信息。一般说来,这些内存信息包含:
- 所有的对象信息,包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值
- 所有的类信息,包括 classloader、类名称、父类、静态变量等
- GCRoot 到所有的这些对象的引用路径
- 线程信息,包括线程的调用栈及此线程的线程局部变量(TLS)
4.1 生成dump文件方式
方法一:jmap
jmap(JVM Memory Map):作用一方面是获取 dump 文件(堆转储快照文件,二进制文件),它还可以获取目标 Java进程的内存相关信息,包括 Java 堆各区域的使用情况、堆中对象的统计信息、类加载信息等。
开发人员可以在控制台中输入命令 jmap -help 查阅 jmap 工具的具体使用方式和一些标准选项配置。
基本语法:
jmap [option] <pid>
jmap [option] <executable <core>
jmap [option] [server_id@] <remote server IP or hostname>
JVM参数:OOM后生成、FGC前生成
方法二:Visual VM
使用 Visual VM 可以导出堆 dump 文件。
1.首先启动程序(需确保程序一直在运行中)
2.打开JvisualVM工具
3.打开对应的程序进程
4.点击线程->线程dump
5.右键快照->另存为
方法三:MAT直接从Java进程导出dump文件
// 开启在出现 OOM 错误时生成堆转储文件
-Xmx1024m
-XX:+HeapDumpOnOutOfMemoryError
// 将生成的堆转储文件保存到 /tmp 目录下,并以进程 ID 和时间戳作为文件名
-XX:HeapDumpPath=/tmp/java_%p_%t.hprof
// 在进行 Full GC 前生成堆转储文件
// 注:如果没有开启自动 GC,则此参数无效。JDK 9 之后该参数已被删除。
-XX:+HeapDumpBeforeFullGC
5.JVM性能调优
5.1 调优JVM参数
调优JVM参数主要关注停顿时间和吞吐量,两者不可兼得,提高吞吐量会拉长停顿时间。