一、JVM 中常用的工具有
jps
: 虚拟机进程状况工具.jstack
: 线程堆栈跟踪工具.jstat
: 统计监测工具.jmap
: 堆内存使用状况工具.JConsole
: JMX的可视化管理工具.VisualVM
: 多合一故障管理工具.Arthas
: Java应用程序诊断和调试工具.
二、【调优命令】排查问题
java
package com.calvin.jvm.structure.heap.question.example;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 案例: 对数据库中的信用卡进行贷款评估。
* - 该程序执行一定时间后可能频繁FullGC,然后OOM。
* - 启动参数: -XX:+PrintGCDetails -Xms200M -Xmx200M
*
* @author calvin
* @date 2024/03/06
*/
public class HeapMemoryLeakExampleByScheduledThreadPool {
/**
* 定时线程池
*/
public static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(
50,
new ThreadPoolExecutor.DiscardOldestPolicy()
);
/**
* 卡信息
* <p>
* - 模拟一张行用卡,里面有个空的m()
*
* @author calvin
* @date 2024/03/06
*/
public static class CardInfo {
/**
* 价格
*/
BigDecimal price = BigDecimal.ZERO;
/**
* 名字
*/
String name = "张三";
/**
* 年龄
*/
long age = 30;
/**
* 出生日期
*/
Date birthDate = new Date();
/**
* 米
*/
public void m() {
}
}
/**
* main方法
*
* @param args 参数
* @throws Exception 异常
*/
public static void main(String[] args) throws Exception {
executor.setMaximumPoolSize(50);
// 死循环 每次去100条数
for (; ; ) {
modFit();
Thread.sleep(100);
}
}
/**
* 模型匹配
*/
public static void modFit() {
// 取100条数据
List<CardInfo> list = getAllCard();
list.forEach(cardInfo -> {
// to do something
executor.scheduleWithFixedDelay(() -> {
cardInfo.m();
}, 2, 3, TimeUnit.SECONDS);
});
}
/**
* 每次从数据库中获得100个行用卡信息
*/
public static List<CardInfo> getAllCard() {
List<CardInfo> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(new CardInfo());
}
return list;
}
}
1. top 命令
- 使用
top 命令
, 先查看CPU、内存等其他信息飙高的进程-H
显示线程信息-p
指定pid
shell
# 查看【进程】使用率
$ top
# 输出结果:
PID COMMAND %CPU TIME #TH #WQ #PORT MEM PURG CMPRS PGRP PPID STATE BOOSTS %CPU_ME %CPU_OTHRS UID FAULTS COW MSGSENT MSGRECV SYSBSD SYSMACH CSW PAGEIN IDLEW POWER INSTRS CYCLES USER
4603 java 560.9 01:57:34 76/8 1 194 255M 0B 66M 967 967 running *0[1] 0.00000 0.00000 501 56316+ 129 5344+ 2593+ 13682699+ 11354483+ 16756058+ 21 1657 560.9 22195113919 15375896232 calvin
# 查看该进程下【线程】使用率 (mac 无法使用该命令)
$ top -Hp 4603
# 输出结果: 已下子线程
PID COMMAND %CPU TIME #TH #WQ #PORT MEM PURG CMPRS PGRP PPID STATE BOOSTS %CPU_ME %CPU_OTHRS UID FAULTS COW MSGSENT MSGRECV
4603 java 560 02:05:39 76/8 1 194 255M 0B 65M 967 967 running *0[1] 0.00000 0.00000 501 58909 129 5706 2770 14201699 12100458 17686388 21 1658 0.0 0 0 calvin N/A N/A N/A N/A N/A N/A
4602 java 0.0 00:01.71 23 1 88 127M 0B 126M 967 967 sleeping *0[1] 0.00000 0.00000 501 10423 142 158 62 90269 875 36848 2 84 0.0 0 0 calvin N/A N/A N/A N/```
2. jps 命令
- 使用
jps 命令
,查看java程序的应用进程。
shell
$ jps
# 输出结果
5698 Jps
967
4602 Launcher
4603 HeapMemoryLeakExampleByScheduledThreadPool
3. jmap 命令
- 打印对于每个Java类,将打印对象数量、内存大小(以字节为单位)和完全限定类名。
- 查看前20行堆对象数量、内存大小(以字节为单位)和完全限定类名。
shell
# 生产环境不建议使用,当前已经发生了GC, 会影响到用户线程
$ jmap -histo 4603 | head -n 20
# 输出结果
num #instances #bytes class name
----------------------------------------------
1: 415378 38174664 [Ljava.lang.Object;
2: 414501 33160016 [S
3: 207546 29859648 [I
4: 209917 15114024 java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask
5: 209917 6717344 com.calvin.jvm.structure.heap.question.example.HeapMemoryLeakExampleByScheduledThreadPool$CardInfo
6: 207540 6641280 java.util.concurrent.ConcurrentHashMap$Node
7: 207250 6632000 com.intellij.rt.debugger.agent.CaptureStorage$WeakKey
8: 207250 6632000 java.lang.Throwable
9: 209917 5038008 java.util.Date
10: 209917 5038008 java.util.concurrent.Executors$RunnableAdapter
11: 209917 3358672 com.calvin.jvm.structure.heap.question.example.HeapMemoryLeakExampleByCardInfo$$Lambda$2/250421012
12: 207250 3316000 com.intellij.rt.debugger.agent.CaptureStorage$ExceptionCapturedStack
13: 10 2099360 [Ljava.util.concurrent.ConcurrentHashMap$Node;
14: 1 1065096 [Ljava.util.concurrent.RunnableScheduledFuture;
15: 2218 192040 [C
16: 842 96696 java.lang.Class
17: 2204 52896 java.lang.String
4. jstat 命令
- 统计gc收集情况
shell
$ jstat -gc 4603
# 输出: FGC 出现 17757 次
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
22528.0 22528.0 0.0 0.0 23040.0 23040.0 136704.0 136523.9 5248.0 4870.3 640.0 497.4 12 0.182 17757 1617.494 1617.676
# `S0C`:年轻代中第一个survivor(幸存区)的容量 (字节)
# `S1C`:年轻代中第二个survivor(幸存区)的容量 (字节)
# `S0U`:年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
# `S1U`:年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
# `EC`:年轻代中Eden(伊甸园)的容量 (字节)
# `EU`:年轻代中Eden(伊甸园)目前已使用空间 (字节)
# `OC`:Old代的容量 (字节)
# `OU`:Old代目前已使用空间 (字节)
# `MC`:metaspace(元空间)的容量 (字节)
# `MU`:metaspace(元空间)目前已使用空间 (字节)
# `CCSC`:当前压缩类空间的容量 (字节)
# `CCSU`:当前压缩类空间目前已使用空间 (字节)
# `YGC`:从应用程序启动到采样时年轻代中gc次数
# `YGCT`:从应用程序启动到采样时年轻代中gc所用时间(s)
# `FGC`:从应用程序启动到采样时old代(全gc)gc次数
# `FGCT`:从应用程序启动到采样时old代(全gc)gc所用时间(s)
# `GCT`:从应用程序启动到采样时gc用的总时间(s)
jstat
命令
-class
: 用于查看类加载情况的统计
-compiler
: 用于查看HotSpot中即时编译器编译情况的统计-gc
: 用于查看JVM中堆的垃圾收集情况的统计-gccapacity
: 用于查看新生代、老生代及持久代的存储容量情况-gcmetacapacity
: 显示metaspace的大小-gcnew
: 用于查看新生代垃圾收集的情况-gcnewcapacity
: 用于查看新生代存储容量的情况-gcold
: 用于查看老生代及持久代垃圾收集的情况-gcoldcapacity
: 用于查看老生代的容量-gcutil
: 显示垃圾收集信息-gccause
: 显示垃圾回收的相关信息(通-gcutil),同时显示最后一次仅当前正在发生的垃圾收集的原因-printcompilation
: 输出JIT编译的方法信息
5. 导出产生堆内存存储文件
- 导出产生堆内存存储文件 (不建议直接导出)
shell
$ jmap -dump:format=b,file=20240306.hprof 4603
# 输出结果:
Dumping heap to /Users/calvin/学习/01-后端开发/Java/02-进阶知识/Jvm 虚拟机/20240306.hprof ...
Heap dump file created
6. VisualVM
- 导入到 VisualVM, 进行分析结果
7. 通过启动参数【配置内存异常后,添加堆转储文件】
- 启动参数【配置内存异常后,添加堆转储文件】,所以一般都是提前规划预防。
shell
java -Xms20m -Xmx20m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError HeapMemoryLeakExampleByScheduledThreadPool.java
8. 分析结果
- 从 VisualVM 反馈问题,可以看出异常线程不断堆积,状态都是等待,说明了当前引发了内存泄漏问题
java
/**
* 模型匹配(内存泄漏的代码)
*/
public static void modFit() {
// 取100条数据
List<CardInfo> list = getAllCard();
// 任务中引用了外部的 cardInfo 对象。由于 cardInfo 是在 getAllCard() 方法中创建的对象
list.forEach(cardInfo -> {
// to do something
executor.scheduleWithFixedDelay(() -> {
// ScheduledThreadPoolExecutor 会持有对这些任务的引用,导致这些 cardInfo 对象无法被释放,从而导致内存泄漏。
cardInfo.m();
}, 2, 3, TimeUnit.SECONDS);
});
}
注意事项
线上环境,内存特别大,jmap 执行期间会对进程产生很大影响,甚至卡顿(电商不适合)
- 设定了参数HeapDump,OOM的时候会自动产生
堆转储文件
(不是很专业,因为多有监控,内存增长就会报警) - 很多服务器备份(高可用),停掉这台服务器对其他服务器不影响.
- 在线定位(一般小点儿公司用不到).
- 在测试环境中压测(产生类似内存增长问题,在堆还不是很大的时候进行转储).
- 不要使用上述第4、5种操作,因为线上环境会导致程序性能更加低下,甚至影响卡顿,部分产生堆存储文件100多G, 会影响到用户线程。
Arthas
1. 启动 Arthas
- 启动
Arthas
, 通过序号
选择有问题程序。
shell
$ java -jar arthas-boot.jar
[INFO] JAVA_HOME: /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre
[INFO] arthas-boot version: 3.7.2
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 6787
[2]: 19204 org.jetbrains.jps.cmdline.Launcher
[3]: 19220 com.calvin.jvm.structure.heap.question.example.HeapMemoryLeakExampleByScheduledThreadPool
3
[INFO] arthas home: /Users/calvin/.arthas/lib/3.7.2/arthas
[INFO] Try to attach process 19220
Picked up JAVA_TOOL_OPTIONS:
[INFO] Attach process 19220 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 3.7.2
main_class com.calvin.jvm.structure.heap.question.example.HeapMemoryLeakExample
ByScheduledThreadPool
pid 19220
time 2024-03-07 14:04:15
2. dashboard 命令
- 观察当前系统的
实时数据面板
,主要查看CPU
、STATE
、Memory
中 used使用状况,以及GC
回收次数。
shell
$ dashboard
- 第一: 从
ParGC
可以看出线上环境已经发生了 GC。 - 第二: 从
pool-1-thread-序号
线程池中的线程在不断堆积, 线程状态都是等待,并且没有执行成功。 - 第三: 从展示内存信息 GC
新生代 17次
和老年代 2352
次,以及内存使用率非常高, 可以判定程序已经接近STW
状态。
3. heapdump 命令
- dump java heap, 类似
jmap 命令
的 heap dump 功能。 - 导出堆存储文件
shell
$ heapdump
# 输出
Dumping heap to /var/folders/_f/40h94x8j6xb5x00hz_xmmf8w0000gn/T/heapdump2024-03-07-15-438412767050560146693.hprof ...
Heap dump file created
4. VisualVM 分析
5. 问题产生
- 从 VisualVM 反馈问题,可以看出异常线程不断堆积,状态都是等待,说明了当前引发了内存泄漏问题
java
/**
* 模型匹配(内存泄漏的代码)
*/
public static void modFit() {
// 取100条数据
List<CardInfo> list = getAllCard();
// 任务中引用了外部的 cardInfo 对象。由于 cardInfo 是在 getAllCard() 方法中创建的对象
list.forEach(cardInfo -> {
// to do something
executor.scheduleWithFixedDelay(() -> {
// ScheduledThreadPoolExecutor 会持有对这些任务的引用,导致这些 cardInfo 对象无法被释放,从而导致内存泄漏。
cardInfo.m();
}, 2, 3, TimeUnit.SECONDS);
});
}
- // 为了解决这个问题,可以在 executor.scheduleWithFixedDelay() 方法中使用局部变量来引用 cardInfo 对象,而不是直接引用外部对象。这样可以确保任务执行完后, cardInfo 对象可以被垃圾回收释放内存。
java
list.forEach(cardInfo -> {
// 使用局部变量引用外部对象
CardInfo localCardInfo = cardInfo;
executor.scheduleWithFixedDelay(() -> {
localCardInfo.m();
}, 2, 3, TimeUnit.SECONDS);
});