【八股消消乐】项目中如何排查内存持续上升问题
😊你好,我是小航,一个正在变秃、变强的文艺倾年。
🔔本专栏《八股消消乐》旨在记录个人所背的八股文,包括Java/Go开发、Vue开发、系统架构、大模型开发、机器学习、深度学习、力扣算法
等相关知识点,期待与你一同探索、学习、进步,一起卷起来叭!
目录
- 题目
- 答案
- 内存监控诊断工具
- top 命令
- vmstat 命令
- pidstat 命令
- jstat 命令
- jstack 命令
- jmap 命令
- 内存泄露
题目
💬技术栈:JVM、Linux
🔍简历内容:熟悉内存监控诊断方法,如Linux top、vmstat、pidstat命令、JDK jstat、jstack、jmap命令等,独立排查并解决ThreadLocal引发的内存泄露问题。
🚩面试问:项目中如何排查内存持续上升问题?
💡建议暂停思考10s,你有答案了嘛?如果你有不同题解,欢迎评论区留言、打卡。
答案
内存监控诊断工具
top 命令
目的:实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载
等信息。其中上半部分显示的是系统的统计信息
,下半部分显示的是进程的使用率统计信息
。
还可以通过 top -Hp pid 查看具体线程使用系统资源情况:
vmstat 命令
vmstat 是一款指定采样周期和次数的功能性监测工具,我们可以看到,它不仅可以统计内存的使用情况
,还可以观测到 CPU 的使用率、swap 的使用情况
。但 vmstat 一般很少用来查看内存的使用情况,而是经常被用来观察进程的上下文切换
。
- r:等待运行的进程数;
- b:处于非中断睡眠状态的进程数;
- swpd:虚拟内存使用情况;
- free:空闲的内存;
- buff:用来作为缓冲的内存数;
- si:从磁盘交换到内存的交换页数量;
- so:从内存交换到磁盘的交换页数量;
- bi:发送到块设备的块数;
- bo:从块设备接收到的块数;
- in:每秒中断数;
- cs:每秒上下文切换次数;
- us:用户 CPU 使用时间;
- sy:内核 CPU 系统使用时间;
- id:空闲时间;
- wa:等待 I/O 时间;
- st:运行虚拟机窃取的时间。
pidstat 命令
pidstat 是 Sysstat 中的一个组件,也是一款功能强大的性能监测工具,我们可以通过命令:yum install sysstat
安装该监控组件。之前的 top 和 vmstat 两个命令都是监测进程的内存、CPU 以及 I/O 使用情况,而 pidstat 命令则是深入到线程级别
。
- -u:默认的参数,显示各个进程的 cpu 使用情况;
- -r:显示各个进程的内存使用情况;
- -d:显示各个进程的 I/O 使用情况;
- -w:显示每个进程的上下文切换情况;
- -p:指定进程号;
- -t:显示进程中线程的统计信息。
可以通过相关命令(例如 ps 或 jps)查询到相关进程 ID,再运行以下命令来监测该进程的内存使用情况:
其中 pidstat 的参数 -p 用于指定进程 ID,-r 表示监控内存的使用情况,1 表示每秒的意思,3 则表示采样次数
。
其中显示的几个关键指标的含义是:
- Minflt/s:任务每秒发生的次要错误,不需要从磁盘中加载页;
- Majflt/s:任务每秒发生的主要错误,需要从磁盘中加载页;
- VSZ:虚拟地址大小,虚拟内存使用 KB;
- RSS:常驻集合大小,非交换区内存使用 KB。
如果我们需要继续查看该进程下的线程内存使用率,则在后面添加 -t 指令即可:
jstat 命令
jstat 可以监测 Java 应用程序的实时运行情况,包括堆内存信息以及垃圾回收信息
。
关键参数信息:
- -class:显示 ClassLoad 的相关信息;
- -compiler:显示 JIT 编译的相关信息;
- -gc:显示和 gc 相关的堆信息;
- -gccapacity:显示各个代的容量以及使用情况;
- -gcmetacapacity:显示 Metaspace 的大小;
- -gcnew:显示新生代信息;
- -gcnewcapacity:显示新生代大小和使用情况;
- -gcold:显示老年代和永久代的信息;
- -gcoldcapacity :显示老年代的大小;
- -gcutil:显示垃圾收集信息;
- -gccause:显示垃圾回收的相关信息(通 -gcutil),同时显示最后一次或当前正在发生的垃圾回收的诱因;
- -printcompilation:输出 JIT 编译的方法信息。
示例:使用 jstat 查看堆内存的使用情况
。
jstat -gc pid 查看:
- S0C:年轻代中 To Survivor 的容量(单位 KB);
- S1C:年轻代中 From Survivor 的容量(单位 KB);
- S0U:年轻代中 To Survivor 目前已使用空间(单位 KB);
- S1U:年轻代中 From Survivor 目前已使用空间(单位 KB);
- EC:年轻代中 Eden 的容量(单位 KB);
- EU:年轻代中 Eden 目前已使用空间(单位 KB);
- OC:Old 代的容量(单位 KB);
- OU:Old 代目前已使用空间(单位 KB);
- MC:Metaspace 的容量(单位 KB);
- MU:Metaspace 目前已使用空间(单位 KB);
- YGC:从应用程序启动到采样时年轻代中 gc 次数;
- YGCT:从应用程序启动到采样时年轻代中 gc 所用时间 (s);
- FGC:从应用程序启动到采样时 old 代(全 gc)gc 次数;
- FGCT:从应用程序启动到采样时 old 代(全 gc)gc 所用时间 (s);
- GCT:从应用程序启动到采样时 gc 用的总时间 (s)。
jstack 命令
它是一种线程堆栈分析工具
,最常用的功能就是使用 jstack pid 命令查看线程的堆栈信息,通常会结合 top -Hp pid 或 pidstat -p pid -t 一起查看具体线程的状态
,也经常用来排查一些死锁
的异常。
每个线程堆栈的信息中,都可以查看到线程 ID、线程的状态(wait、sleep、running 等状态)以及是否持有锁
等。
jmap 命令
查看堆内存初始化配置信息以及堆内存的使用情况
;输出堆内存中的对象信息,包括产生了哪些对象,对象数量多少
等。
jmap 来查看堆内存初始化配置信息以及堆内存的使用情况:
使用 jmap -histo[:live] pid 查看堆内存中的对象数目、大小统计直方图,如果带上 live 则只统计活对象:
可以通过 jmap 命令把堆内存的使用情况 dump 到文件中:
可以将文件下载下来,使用 MAT 工具打开文件进行分析:
内存泄露
我们平时遇到的内存溢出
问题一般分为两种:
- 由于大峰值下没有限流,
瞬间创建大量对象而导致的内存溢出
;【使用限流就可以解决】 - 由于
内存泄漏
而导致的内存溢出。【程序的 BUG,我们需要及时找到问题代码】
创建 100 个线程,由于ThreadLocal 使用不恰当,就可能导致内存泄漏。
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES,new LinkedBlockingQueue<>());// 创建线程池,通过线程池,保证创建的线程存活final static ThreadLocal<Byte[]> localVariable = new ThreadLocal<Byte[]>();// 声明本地变量@RequestMapping(value = "/test0")public String test0(HttpServletRequest request) {poolExecutor.execute(new Runnable() {public void run() {Byte[] c = new Byte[4096*1024];localVariable.set(c);// 为线程添加变量}});return "success";}@RequestMapping(value = "/test1")public String test1(HttpServletRequest request) {List<Byte[]> temp1 = new ArrayList<Byte[]>();Byte[] b = new Byte[1024*20];temp1.add(b);// 添加局部变量return "success";}
开启堆内存异常日志,启动程序:
java -jar -Xms1000m -Xmx4000m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -Xms1g -Xmx1g -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heapTest.log heapTest-0.0.1-SNAPSHOT.jar
分别对 test0、test1 请求10000 次
后台日志打印:test1接口异常,内存溢出。
我们开始进行排查分析问题。
(1)使用Linux top命令查看进程在整个系统中内存的使用率。
结果发现:机器只有 8G 内存且只分配了 4G 内存给 Java 进程的情况下,Java 进程内存使用率已经达到了 55%。
(2)我们再通过 top -Hp pid 查看具体线程占用系统资源情况。
(3)通过 jstack pid 查看具体线程的堆栈信息:
结果发现:该线程一直处于 TIMED_WAITING 状态
,此时 CPU 使用率和负载并没有出现异常
,我们可以排除死锁或 I/O 阻塞的异常问题
了。
(4)通过 jmap 查看堆内存的使用情况:
结果发现:老年代的使用率几乎快占满了,而且内存一直得不到释放。可以确认系统发生了内存泄露,对象一直无法回收
。
(5)通过 jstat 查看存活对象的数量,看看是哪个对象占用了堆内存。
结果发现:Byte 对象占用内存明显异常,说明代码中 Byte 对象存在内存泄漏。
(6)通过 MAT 打开 dump 的内存日志文件:
结果发现:MAT 提示 byte 内存异常。
(7)点击进入到 Histogram 页面,查看到对象数量排序:
结果发现:byte[] 数组排在了第一位。
(8)选中对象后右击选择 with incomming reference 功能,可以查看到具体哪个对象引用了这个对象。
结果发现:ThreadLocal 这块的代码出现了问题。
📌 [ 笔者 ] 文艺倾年
📃 [ 更新 ] 2025.5.11
❌ [ 勘误 ] /* 暂无 */
📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!