实战分享:生产Java服务容器占用内存高问题排查与解决

总框架

在这里插入图片描述

一、问题描述

生产上有一个Java应用,在发版后一周内,容器内存指标缓慢上升,最终超过60%触发内存使用告警
在这里插入图片描述

二、思路&解决方案

1、日志占用容器内存

(1)排查JVM占用了多少内存

一般我们惯性思维默认是JVM占用内存,但是通过监控发现,容器申请内存16G,JVM才占用了4G不到,占25%
在这里插入图片描述
分析可能是容器除Java进程外的模块占用了内存

(2)容器内存的组成

在这里插入图片描述

从上图可以看出:

容器内存=进程实际内存+页面缓存+kernel memory+不活跃缓存页

(3)Linux内核-页高速缓存

什么是页高速缓存?

在linux读写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问,所以通过page cache可以有效减少 I/O,提升应用的 I/O 速度,可通过/proc/meminfo、free 、/proc/vmstat等手段来监测

从读写文件这个场景可以推断出,可能是日志文件的读写占用了大量内存。查看了一下应用输出的日志大小,发现日志占用了5G左右,实锤

(4)解决方案

①导出容器本地日志文件,看下日志的组成,排查频率高且冗长、非必要的日志,修改代码取消日志的打印

②查看应用里配置的日志保存时间,可以根据需要调整短一些
在这里插入图片描述

③临时方案,手动去清除日志文件,可以发现内存马上有明显的下降

2、JVM占用容器内存

(1)常用的排查分析命令

#查看CPU占用高的Java线程
top -Hp pid
在这里插入图片描述

#查看占用内存最大的类(前30个)
jmap -histo pid | head -30
在这里插入图片描述

#查看gc统计
jstat -gc pid
在这里插入图片描述
说明:
S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间

#查看实时GC情况(每隔1秒打印一次gc统计信息)
jstat -gcutil pid 1000
在这里插入图片描述
#查看堆信息
jmap -heap pid
在这里插入图片描述

(2)导出内存快照和线程栈信息

①导出内存快照
jmap -dump:format=b,file=xxx.hprof pid

遇到问题:

此命令会触发容器重启,导致无法获取

分析:

开始以为是导出操作占用了容器大量内存,超出限制导致的重启

于是限制了导出使用的内存为2G

jmap -dump:format=b,file=xxx.hprof -J-Xmx2g pid

发现还是会重启

于是找到了容器基础运维的人,分析可能是健康检查导致的:

应用停顿无法响应请求,触发了健康检查的失败阈值

解决方案:

把容器健康检查的阈值适当调宽松,主要是检查间隔要久一些,保证stop the world期间不会触发健康检查

②打印线程当前(当前指的是执行命令的时刻)堆栈信息

jstack -l pid >> xxx.txt

(3)分析内存快照

①将内存快照复制到本地

②准备内存分析工具MAT(需要JDK17以上环境,可以问我拿MAT和JDK的安装包)

改下配置,调大使用的内存
在这里插入图片描述
在这里插入图片描述

③使用MAT分析刚刚下载的快照

在这里插入图片描述
在这里插入图片描述

可以看出是阿里巴巴druid的一个类,占用了91%的内存

查询发现这是druid的监控模块的一个缺陷,https://blog.csdn.net/lypeng13/article/details/121911981

druid开启stat监控,所以sql信息就会存储到该Map中,占用内存,造成内存泄漏

解决方案:

直接关闭druid的stat

spring.datasource.druid.filter.stat=false

(4)分析栈信息

①通过top -Hp pid命令拿到消耗CPU/内存的线程
在这里插入图片描述

②将对应线程号切换为16进制:printf “%x\n” pid

31680 >> 7bc0 ; 31191 >> 79d7; 31295 >> 7a3f

③从栈信息里查找16进制的线程号,定位到代码
在这里插入图片描述

分析栈信息需重点留意线程的状态

public enum State {
 
        /**
         * Thread state for a thread which has not yet started.
         * 线程创建后尚未启动的线程处于这种状态
         */
        NEW,
 
        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * Runable包括了操作系统线程状态中的Running和Ready, 也就是处于此
状态的线程有可能正在执行, 也有可能正在等待着CPU为它分配执行时间
         */
        RUNNABLE,
 
        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * 线程被阻塞了, “阻塞状态”与“等待状态”的区别是: “阻塞状态”在等
待着获取到一个排他锁, 这个事件将在另外一个线程放弃这个锁的时候发生; 而“等待状
态”则是在等待一段时间, 或者唤醒动作的发生。 在程序等待进入同步区域的时候, 线程将
进入这种状态。
         */
        BLOCKED,
 
        /**
         * Thread state for a waiting thread.
         * 无限期等待状态,处于这种状态的线程不会被分配CPU执行时间,它们要等待被
其他线程显式地唤醒
         */
        WAITING,
 
        /**
         * Thread state for a waiting thread with a specified waiting time.
         * 线程等待被唤醒,处于这种状态的线程也不会被分配CPU执行时间, 不过无
须等待被其他线程显式地唤醒, 在一定时间之后它们会由系统自动唤醒
         */
        TIMED_WAITING,
 
        /**
         * Thread state for a terminated thread.
         * 线程已经执行结束
         */
        TERMINATED;
    }

三、经验心得

这个内存问题一直都存在,但是之前因为dump不出来快照一直拖着,真正去解决的时候发现,其实并不算困难

  • 善于利用身边的资源,找专业的人咨询是个好办法
  • 有时候困难只是我们自己想象出来的,去干就完事了
  • 对技术保持敬畏以及好奇心,只会用不懂原理的话,很容易出问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后端之道

如果帮助到你,可以请作者喝杯茶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值