总框架
一、问题描述
生产上有一个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不出来快照一直拖着,真正去解决的时候发现,其实并不算困难
- 善于利用身边的资源,找专业的人咨询是个好办法
- 有时候困难只是我们自己想象出来的,去干就完事了
- 对技术保持敬畏以及好奇心,只会用不懂原理的话,很容易出问题