【八股消消乐】项目中如何排查内存持续上升问题

在这里插入图片描述

😊你好,我是小航,一个正在变秃、变强的文艺倾年。
🔔本专栏《八股消消乐》旨在记录个人所背的八股文,包括Java/Go开发、Vue开发、系统架构、大模型开发、机器学习、深度学习、力扣算法等相关知识点,期待与你一同探索、学习、进步,一起卷起来叭!

题目

💬技术栈: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
❌ [ 勘误 ]   /* 暂无 */
📜 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值