likes
comments
collection
share

记一次内存溢出问题解决

作者站长头像
站长
· 阅读数 5

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

背景

在某次构建发布到测试环境后,容器不定时会重启,导致系统的接口不定时失效崩溃。故打开公司内部的监控工具,发现报OutOfMemory 错误,堆内存不足。

记一次内存溢出问题解决

问题解决

在发布平台上,进入到容器内部,进行问题排查:

定位 Java 进程

使用 jps 定位当前系统所运行的 Java 进程号

PS D:\projects\> jps
21984 
25408 RemoteMavenServer36
23252 Jps
23704 Launcher
12076 Application

Jmap 观察内存信息

使用 jmap -heap [pid] 查看当前 Java 进程使用内存的信息

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 查看内存中对象的数量,对象占用内存的大小

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 查询

@Override
public List<ThirdPaySlave> selectByPayNoThird(String payNoThird) {
    return thirdPaySlaveMapper.selectByPayNoThird(payNoThird);
}

结合 OOM 前的查询日志和公司内部的 SQL 监控平台:

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 集合

@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 的使用情况;