记一次内存溢出问题解决

一次内存溢出问题排查与解决

背景

在某次构建发布到测试环境后,容器不定时会重启,导致系统的接口不定时失效崩溃。故打开公司内部的监控工具,发现报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 的使用情况;

相关推荐
Chenyiax7 分钟前
从 PyTorch Attention 源码理解 KV Cache、缓存命中与 Prefix Cache
后端
IT_陈寒29 分钟前
React状态更新总是不及时?你可能漏了这步批处理机制
前端·人工智能·后端
倔强的石头_1 小时前
KingbaseES 新版MySQL 兼容版体验:旧版迁移 + 功能实测
数据库
Jinkey1 小时前
要用户手机号真的是为了打骚扰电话吗?浅谈微信生态会员账号体系与资产合并
后端·微信·微信小程序
葫芦和十三2 小时前
图解 MongoDB 06|模式演进:无 schema 是优势还是债
后端·mongodb·agent
葫芦和十三9 小时前
图解 MongoDB 05|文档模型设计:内嵌 vs 引用,反范式不是免费午餐
后端·mongodb·agent
不能放弃治疗12 小时前
单 Agent 实现模式
后端
IT_陈寒15 小时前
Redis内存爆了,原来我漏掉了这个致命配置
前端·人工智能·后端
小bo波15 小时前
从"任意文件复制"深挖Java I/O:字符流与字节流的本质抉择
java·nio·io流·后端开发·文件复制
fliter15 小时前
最后一块拼图:用 bitvec 构造 IPv4 包,真正做出自己的 Ping
后端