一次内存溢出问题排查与解决
背景
在某次构建发布到测试环境后,容器不定时会重启,导致系统的接口不定时失效崩溃。故打开公司内部的监控工具,发现报OutOfMemory 错误,堆内存不足。
问题解决
在发布平台上,进入到容器内部,进行问题排查:
定位 Java 进程
使用 jps 定位当前系统所运行的 Java 进程号
shell
PS D:\projects\> jps
21984
25408 RemoteMavenServer36
23252 Jps
23704 Launcher
12076 Application
Jmap 观察内存信息
使用 jmap -heap [pid] 查看当前 Java 进程使用内存的信息
shell
PS D:\projects\> jmap -heap 12076
Attaching to process ID 12076, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.131-b11
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4271898624 (4074.0MB)
NewSize = 89128960 (85.0MB)
MaxNewSize = 1423966208 (1358.0MB)
OldSize = 179306496 (171.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 677904384 (646.5MB)
used = 677904384 (646.5MB)
free = 0 (0.0MB)
100.0% used
From Space:
capacity = 376438784 (359.0MB)
used = 0 (0.0MB)
free = 376438784 (359.0MB)
0.0% used
To Space:
capacity = 369623040 (352.5MB)
used = 0 (0.0MB)
free = 369623040 (352.5MB)
0.0% used
PS Old Generation
capacity = 2847932416 (2716.0MB)
used = 2847712944 (2715.7906951904297MB)
free = 219472 (0.2093048095703125MB)
99.99229363735013% used
- 发现伊甸园和老年代的内存都被占满了,前者 100%,后者 99.9%
接着,使用 jmap -histo [pid] | Select-object -First 100 查看内存中对象的数量,对象占用内存的大小
shell
PS D:\projects\payment-backstage> jmap -histo 12076 | Select-Object -First 100
num #instances #bytes class name
----------------------------------------------
1: 34943217 1086871432 [B
2: 18497467 873854200 [C
3: 28428412 682281888 java.lang.String
4: 2042681 212439664 [[B
5: 2015302 209591408 com.xxx.payment.xxx.payment.model.slave.ThirdPaySlave
6: 4030650 161226000 java.math.BigDecimal
7: 4721884 113325216 java.util.Date
8: 2092979 50231496 java.lang.Long
9: 2042671 49024104 com.mysql.jdbc.ByteArrayRow
10: 47816 28013608 [Ljava.lang.Object;
11: 54642 4808496 java.lang.reflect.Method
12: 103452 3310464 java.util.concurrent.ConcurrentHashMap$Node
13: 22292 2468800 java.lang.Class
- 发现 ThirdPaySlave 的实例数创建了 2015302 个对象,占用 209591408 字节,约 200MB,且数量不断上升;
于是,查看 ThirdPaySlave 被使用到的地方,是下面这个 SQL 查询
java
@Override
public List<ThirdPaySlave> selectByPayNoThird(String payNoThird) {
return thirdPaySlaveMapper.selectByPayNoThird(payNoThird);
}
结合 OOM 前的查询日志和公司内部的 SQL 监控平台:
shell
2024-01-15 14:59:10.545 DEBUG 12076 --- [ main] c.y.p.b.p.m.s.b.T.selectByPayNoThird : ==> Preparing: select id, order_no, biz_type, pay_channel, pay_platform, pay_amount, third_discount_amount, pay_no, pay_no_third, pay_time, pay_user, state, app, app_id, merchant_id, create_time, update_time, uid, biz_id, biz_code, channel_no, notified_state from payment_third_pay where pay_no_third = ?
2024-01-15 14:59:10.580 DEBUG 12076 --- [ main] c.y.p.b.p.m.s.b.T.selectByPayNoThird : ==> Parameters: (String)
- 发现,当 pay_no_third 传入的值为空字符串时,这条 SQL 执行速度非常慢
于是我到测试库中进行查看:
- 执行上述的 SQL 后,发现 pay_no_third 为字符串时,该 SQL 从数据库中捞出了 2042673 条数据,导致查询出来的 ThirdPaySlave 过多,多次查询调用后,导致内存被撑爆;
使用 jconsole 可以详细查看 Java 进程中内存和 CPU 的使用情况:
解决措施
对传入的 pay_no_third 参数进行判空,如果为空,就返回空 List 集合
java
@Override
public List<ThirdPaySlave> selectByPayNoThird(String payNoThird) {
if (StringUtils.isBlank(payNoThird)) {
return new ArrayList<>();
}
return thirdPaySlaveMapper.selectByPayNoThird(payNoThird);
}
- 这次 OOM 告诉我们,对传入数据库中的参数一定要做严格的参数校验,不仅防止上述 OOM 的情况,还会避免 SQL 注入的问题;
总结
-
公司内部使用的 k8s 容器编排工具,当 容器内的 Java 进程发生 OOM 时,会将容器重启。因为容器内部的每一个 POD 都会被 k8s 的 livenessProbe 进行探测,一旦容器的内存或 CPU 的使用指标不正常时,就会根据配置的 restartPolicy 的策略进行处理;
-
使用 jps 命令可以定位 Java 进程;
-
使用 jmap -heap 可以查看内存的使用情况,包括 eden,from 区,to 区,老年代;
-
使用 jmap -histo 可以查看 Java 进程中创建的对象多少和一共占用的内存大小;
-
使用 jstack 可以查看当前的线程的快照,上述处理过程中没有使用到;
-
使用 jconsole 可以详细查看 Java 进程中内存和 CPU 的使用情况;