当前位置: 首页 > article >正文

内存泄漏的通用排查方法

本文聊一聊如何系统性地分析查找内存泄漏的具体方法,但不会具体到哪种语言和具体业务代码逻辑中,而是会从 Linux 系统上通用的一些分析方法来入手。这样,不论你使用什么开发语言,不论你在开发什么,它总能给你提供一些帮助。

如何定位谁在消耗内存

内存泄漏的外在表现通常是系统内存不够,严重的话可能会引起 OOM (Out of Memory),甚至系统宕机。那在发生这些现象时,惯用的分析套路是什么呢?

首先,我们需要去找出到底是谁在消耗内存。前文说过,/proc/meminfo 可以帮助我们来快速定位出问题所在。 但/proc/meminfo 中的项目很多,我们没必要全部都背下来,不过有些项是相对容易出问题的,也是你在遇到内存相关的问题时,需要重点去排查的。我将这些项列了一张表格,也给出了每一项有异常时的排查思路。

image-20250127121533096

  • 总之,如果进程的内存有问题,那使用 top 就可以观察出来;如果进程的内存没有问题, 那你可以从 /proc/meminfo 入手来一步步地去深入分析。

分析进程内存泄漏的原因

接下来,我们分析一个实际的案例,来看看如何分析进程内存泄漏是什么原因导致的。如果你已经使用 top 排查出了业务进程的内存异常,即业务进程的虚拟地址空间(VIRT)被消耗很大,但是物理内存 (RES)使用得却很少,所以他怀疑是进程的虚拟地址空间有内存泄漏。

我们在前面几篇文章说过,出现该现象时,可以用 top 命令观察(这是当时保存的生产环境信息,部分信息做了脱敏处理):

PID    USER   PR  NI  VIRT  RES   SHR S %CPU %MEM    TIME+  COMMAND
31108  app    20   0  285g  4.0g  19m S 60.6 12.7  10986:15 app_server 

​ 可以看到 app_server 这个程序的虚拟地址空间(VIRT 这一项)很大,有 285GB。

那该如何追踪 app_server 究竟是哪里存在问题呢?我们可以用 pidstat 命令来追踪下该进程的内存行为,看看能够发现什么现象。

$ pidstat -r -p 31108  1PID    minflt/s  majflt/s     VSZ    RSS     %MEM   Command
04:47:00 PM     31108   353.00       0.00  299029776 4182152  12.73  app_server
...
04:47:59 PM     31108	149.00		0.00  299029776 4181052  12.73  app_server
04:48:00 PM     31108	191.00		0.00  299040020 4181188  12.73  app_server
...
04:48:59 PM     31108	179.00 		0.00  299040020 4181400  12.73  app_server
04:49:00 PM     31108	183.00		0.00  299050264 4181524  12.73  app_server
...
04:49:59 PM     31108	157.00		0.00  299050264 4181456  12.73  app_server
04:50:00 PM     31108	207.00 		0.00  299060508 4181560  12.73  app_server
...
04:50:59 PM	    31108	127.00		0.00  299060508 4180816  12.73  app_server
04:51:00 PM     31108	27.00		0.00  299070752 4180956  12.73  app_server

​ 如上所示,在每个整分钟的时候,VSZ 会增大 244KB,这看起来是一个很有规律的现象。然后,我们再来看下增大的这个内存区域到底是什么,你可以通过 /proc/PID/smaps 来看。

增大的内存区域,具体如下:

$ cat /proc/31108/smaps 
...
7faae0e49000 - 7faae1849000 rw-p 00000000 00:00 0 	#私有地址空间
Size:              10240 kB
Rss:                  80 kB
Pss:                  80 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:        80 kB
Referenced:           60 kB
Anonymous:            80 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
7faae1849000 - 7faae184a000 ---p 00000000 00:00 0 	#保护页(进程无法访问)
Size:                  4 kB
Rss:                   0 kB
Pss:                   0 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:            0 kB
Anonymous:             0 kB
AnonHugePages:         0 kB
Swap:                  0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB

​ 可以看到,它包括:一个私有地址空间,这从 rw -p 这个属性中的 private 可以看出来;以 及一个保护页 ,这从---p 这个属性可以看出来,即进程无法访问。对于有经验的开发者而 言,从这个 4K 的保护页就可以猜测出应该跟线程栈有关了。

然后我们跟踪下进程申请这部分地址空间的目的是什么!

通过 strace 命令来跟踪系统调用就可以了。因为 VIRT 的增加,它的系统调用函数无非是 mmap 或者 brk,那么我们只需 要 strace 的结果来看下 mmap 或 brk 就可以了。

用 strace 跟踪如下:

$ strace -t -f -p 31108 -o 31108.strace

线程数较多,如果使用 -f 来跟踪线程,跟踪的信息量也很大,逐个搜索日志里面的 mmap 或者 brk 真是眼花缭乱, 所以我们来 grep 一下这个大小 (10489856 即 10244KB),然后 过滤下就好了:

$ cat 31108.strace | grep 10489856    
31152 23:00:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31151 23:01:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31157 23:02:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31158 23:03:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31165 23:04:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31163 23:05:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31153 23:06:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31155 23:07:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31149 23:08:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31147 23:09:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31159 23:10:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31157 23:11:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31148 23:12:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31150 23:13:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31173 23:14:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON

从这个日志我们可以看到,出错的是 mmap() 这个系统调用,那我们再来看下 mmap 这个内存的目的:

31151 23:01:00 mmap(NULL, 10489856, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON
31151 23:01:00 mprotect(0x7fa94bbc0000, 4096, PROT_NONE <unfinished ...>   <<<
31151 23:01:00 clone( <unfinished ...>   <<< 创建线程
31151 23:01:00 <... clone resumed> child_stack=0x7fa94c5afe50, flags=CLONE_VM|
|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID
|CLONE_CHILD_CLEARTID, parent_tidptr=0x7fa94c5c09d0, tls=0x7fa94c5c0700, child

可以看出,这是在 clone 时申请的线程栈。

到这里你可能会有一个疑问:既然线程栈消耗了这么多的内存,那理应有很多才对啊? 但是实际上,系统中并没有很多 app_server 的线程,那这是为什么呢?

  • 答案其实比较简单:线程短暂执行完毕后就退出了,可是 mmap 的线程栈却没有被释放。

我们来写一个简单的程序复现这个现象,问题的复现是很重要的,如果很复杂的问题可以用简单的程序来复现,那就是最好的结果了。 如下是一个简单的复现程序:mmap 一个 40K 的线程栈,然后线程简单执行一下就退出。

#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <bits/sched.h>#define _SCHED_H 
#define __USE_GNU 
#define STACK_SIZE 40960int func(void *arg)
{printf("thread enter.\n");sleep(1);printf("thread exit.\n");return 0;
}int main()
{int thread_pid;int status;int w;while (1) {void *addr = mmap(NULL, STACK_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE);if (addr == NULL) {perror("mmap");goto error;}printf("creat new thread...\n");thread_pid = clone(&func, addr + STACK_SIZE, CLONE_SIGHAND|CLONE_FS);printf("Done! Thread pid: %d\n", thread_pid);if (thread_pid != -1) {do {w = waitpid(-1, NULL, __WCLONE | __WALL);if (w == -1) {perror("waitpid");goto error;}} while (!WIFEXITED(status) && !WIFSIGNALED(status));}sleep(10);}
error:return 0;
}

然后我们用 pidstat 观察该进程的执行,可以发现它的现象跟生产环境中的问题是一致的:

$ pidstat -r -p 535 5
11:56:51 PM   UID       PID  minflt/s  majflt/s     VSZ    RSS   %MEM  Command
11:56:56 PM     0       535      0.20      0.00    4364    360   0.00  a.out
11:57:01 PM     0       535      0.00      0.00    4364    360   0.00  a.out
11:57:06 PM     0       535      0.20      0.00    4404    360   0.00  a.out
11:57:11 PM     0       535      0.00      0.00    4404    360   0.00  a.out
11:57:16 PM     0       535      0.20      0.00    4444    360   0.00  a.out
11:57:21 PM     0       535      0.00      0.00    4444    360   0.00  a.out
11:57:26 PM     0       535      0.20      0.00    4484    360   0.00  a.out
11:57:31 PM     0       535      0.00      0.00    4484    360   0.00  a.out
11:57:36 PM     0       535      0.20      0.00    4524    360   0.00  a.out
^C
Average:        0       535      0.11      0.00    4435    360   0.00  a.out

你可以看到,VSZ 每 10s 增大 40K,但是增加的那个线程只存在了 1s 就消失了。 至此我们就可以推断出 app_server 的代码哪里有问题了,你只要去修复该代码 Bug,很快就会把该问题给解决。

当然了,应用程序的内存泄漏问题其实是千奇百怪的,分析方法也不尽相同,我们这个案例的目的是为了告诉你一些通用的分析技巧。掌握了这些通用分析技巧,很多时候就可以以不变来应万变了。

关注我,不迷路!更多嵌入式精品文章某信搜索共众 号【细说嵌入式】

结束语

本文讲述了系统性分析 Linux 上内存泄漏问题的分析方法,要点如下:

  • top 工具和 /proc/meminfo 文件是分析 Linux 上内存泄漏问题,甚至是所有内存问题 的第一步,我们先找出来哪个进程或者哪一项有异常,然后再针对性地分析;
    百怪的,分析方法也不尽相同,我们这个案例的目的是为了告诉你一些通用的分析技巧。掌握了这些通用分析技巧,很多时候就可以以不变来应万变了。

结束语

本文讲述了系统性分析 Linux 上内存泄漏问题的分析方法,要点如下:

  • top 工具和 /proc/meminfo 文件是分析 Linux 上内存泄漏问题,甚至是所有内存问题 的第一步,我们先找出来哪个进程或者哪一项有异常,然后再针对性地分析;
  • 应用程序的内存泄漏千奇百怪,所以你需要掌握一些通用的分析技巧,掌握了这些技巧 很多时候就可以以不变应万变。但是,这些技巧的掌握,是建立在你的基础知识足够扎 实的基础上。你需要熟练掌握我们这个系列课程讲述的这些基础知识,熟才能生巧。

相关文章:

内存泄漏的通用排查方法

本文聊一聊如何系统性地分析查找内存泄漏的具体方法&#xff0c;但不会具体到哪种语言和具体业务代码逻辑中&#xff0c;而是会从 Linux 系统上通用的一些分析方法来入手。这样&#xff0c;不论你使用什么开发语言&#xff0c;不论你在开发什么&#xff0c;它总能给你提供一些帮…...

【Python】第五弹---深入理解函数:从基础到进阶的全面解析

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】【MySQL】【Python】 目录 1、函数 1.1、函数是什么 1.2、语法格式 1.3、函数参数 1.4、函数返回值 1.5、变量作用域 1.6、函数…...

读书笔记--分布式服务架构对比及优势

本篇是在上一篇的基础上&#xff0c;主要对共享服务平台建设所依赖的分布式服务架构进行学习&#xff0c;主要记录和思考如下&#xff0c;供大家学习参考。随着企业各业务数字化转型工作的推进&#xff0c;之前在传统的单一系统&#xff08;或单体应用&#xff09;模式中&#…...

关于WPF中ComboBox文本查询功能

一种方法是使用事件&#xff08;包括MVVM的绑定&#xff09; <ComboBox TextBoxBase.TextChanged"ComboBox_TextChanged" /> 然而运行时就会发现&#xff0c;这个事件在疯狂的触发&#xff0c;很频繁 在实际应用中&#xff0c;如果关联查询数据库&#xff0…...

解析“in the wild”——编程和生活中的俚语妙用

解析“in the wild”——编程和生活中的俚语妙用 看下面的技术文章中遇到 in the wild这个词&#xff0c;想要研究一下&#xff0c;遂产生此文。 Are there ever pointers to pointers to pointers? There is an old programming joke which says you can rate C programmers…...

蓝桥杯练习日常|c/c++竞赛常用库函数(下)

书接上回......蓝桥杯算法日常|c\c常用竞赛函数总结备用-CSDN博客 目录 书接上回......https://blog.csdn.net/weixin_47011416/article/details/145290017 1、二分查找 2、lower_bound uper_bound 3、memset&#xff08;&#xff09; 函数原型 参数说明 返回值 常见用…...

Mybatis-plus缓存

mybatis-plus缓存 MyBatis-Plus 是一个 MyBatis 的增强工具&#xff0c;在 MyBatis 的基础上提供了更多的便利性和强大的功能&#xff0c;包括但不限于分页、条件构造器、通用 Mapper、代码生成器等。MyBatis-Plus 也内置了基础的缓存功能&#xff0c;但需要注意的是&#xff…...

LockSupport概述、阻塞方法park、唤醒方法unpark(thread)、解决的痛点、带来的面试题

目录 ①. 什么是LockSupport? ②. 阻塞方法 ③. 唤醒方法(注意这个permit最多只能为1) ④. LockSupport它的解决的痛点 ⑤. LockSupport 面试题目 ①. 什么是LockSupport? ①. 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作 ②. LockSupport是一个线程阻塞…...

基于 WPF 平台使用纯 C# 实现动态处理 json 字符串

一、引言 在当今的软件开发领域&#xff0c;数据的交换与存储变得愈发频繁&#xff0c;JSON&#xff08;JavaScript Object Notation&#xff09;作为一种轻量级的数据交换格式&#xff0c;以其简洁、易读、便于解析和生成的特点&#xff0c;被广泛应用于各种应用程序中。在 W…...

Rust:Rhai脚本编程示例

当然&#xff0c;以下是一个简单的Rhai脚本编程示例&#xff0c;展示了如何在Rust中使用Rhai执行脚本。 首先&#xff0c;你需要确保你的Rust项目中包含了rhai库。你可以在你的Cargo.toml文件中添加以下依赖项&#xff1a; [dependencies] rhai "0.19" # 请检查最…...

skynet 源码阅读 -- 启动主流程

Skynet 启动主流程分析 Skynet 是一个轻量级、高并发的服务器框架。它在启动时会进行一系列初始化操作&#xff0c;并启动多个不同功能的线程&#xff08;Monitor、Timer、Worker、Socket&#xff09;&#xff0c;从而实现消息分发、定时器、网络I/O等核心功能。本文主要从 ma…...

活动回顾和预告|微软开发者社区 Code Without Barriers 上海站首场活动成功举办!

Code Without Barriers 上海活动回顾 Code Without Barriers&#xff1a;AI & DATA 深入探索人工智能与数据如何变革行业 2025年1月16日&#xff0c;微软开发者社区 Code Without Barriers &#xff08;CWB&#xff09;携手 She Rewires 她原力在大中华区的首场活动“AI &…...

Workerman和Swoole有什么区别

Workerman和Swoole都是PHP的socket服务器框架&#xff0c;它们之间存在一些显著的区别&#xff0c;主要体现在以下几个方面&#xff1a; 一、实现语言与性能 Workerman&#xff1a;使用纯PHP实现。由于PHP本身的性能限制&#xff0c;Workerman在某些方面可能不如C语言实现的框…...

项目部署(springboot项目)

1、安装Nginx&#xff0c;并开启 2、前端项目打包&#xff1a;npm run build:prod--->dist 3、后端项目打包&#xff1a;install--->xxx.jar 4、开放需要的端口号&#xff1a;比如我的后端项目端口号为8282&#xff0c;则需要防火墙和服务器同时开发8282端口 5、将di…...

从0到1:C++ 开启游戏开发奇幻之旅(一)

目录 为什么选择 C 进行游戏开发 性能卓越 内存管理精细 跨平台兼容性强 搭建 C 游戏开发环境 集成开发环境&#xff08;IDE&#xff09; Visual Studio CLion 图形库 SDL&#xff08;Simple DirectMedia Layer&#xff09; SFML&#xff08;Simple and Fast Multim…...

Python-列表

3.1 列表是什么 在Python中&#xff0c;列表是一种非常重要的数据结构&#xff0c;用于存储一系列有序的元素。列表中的每个元素都有一个索引&#xff0c;索引从0开始。列表可以包含任何类型的元素&#xff0c;包括其他列表。 # 创建一个列表my_list [1, 2, 3, four, 5.0]…...

下载Visual Studio Community 2019

官方链接如下&#xff1a;Visual Studio Community 2019下载链接 https://learn.microsoft.com/zh-cn/visualstudio/releases/2019/system-requirements#download 目前官方仅建议2022版&#xff0c;已经关闭vs2019等旧版本&#xff0c;哪天开放了&#xff0c;记得踢我一下。 …...

MongoDB平替数据库对比

背景 项目一直是与实时在线监测相关&#xff0c;特点数据量大&#xff0c;读写操作大&#xff0c;所以选用的是MongoDB。但按趋势来讲&#xff0c;需要有一款国产数据库可替代&#xff0c;实现信创要求。选型对比如下 1. IoTDB 这款是由清华大学主导的开源时序数据库&#x…...

c++ set/multiset 容器

1. set 基本概念 简介&#xff1a; 所有元素都会在插入时自动排序本质&#xff1a; set/multiset属于关联式容器&#xff0c;底层结构是用二叉树实现。set 和 multiset 区别&#xff1a; set容器不允许有重复的元素。 multiset允许有重复的元素。2. set 构造和赋值 构造&a…...

SCRM在企业私域流量与客户管理中的变革之路探索

内容概要 在当今数字化高速发展的时代&#xff0c;SCRM&#xff08;社交客户关系管理&#xff09;作为一种新的管理工具&#xff0c;正逐渐成为企业私域流量管理和客户关系维护的重要基石。它不仅仅是一种软件工具&#xff0c;更是一种整合客户数据和关系管理的全新思维方式。…...

爱的魔力转圈圈,基于carsim与simulink模拟仰望u8原地调头

仰望U8原地转向的示意图如下&#xff0c;不动方向盘的情况下&#xff0c;车可以自己转圈圈&#xff1a; 原理也很简单&#xff0c;仰望u8是四轮驱动&#xff0c;四个轮子都单独由四个轮边电机驱动。主要我们将左右的车轮轮速控制成左右两边轮速相同&#xff0c;但是方向相反&am…...

2025多目标优化创新路径汇总

多目标优化是当下非常热门且有前景的方向&#xff01;作为AI领域的核心技术之一&#xff0c;其专注于解决多个相互冲突的目标的协同优化问题&#xff0c;核心理念是寻找一组“不完美但均衡”的“帕累托最优解”。在实际中&#xff0c;几乎处处都有它的身影。 但随着需求场景的…...

输出九九乘法表

# 题目&#xff1a;输出九九乘法表 #(1) for i in range(1,10): #行数&#xff0c;1到9for j in range(1,10): # 列数1到9resulti*jprint(f"{i}*{j}{result}",end"\t")print("\n")#(2) for i in range(1,10): #行数for j in range(1,i1): #列数1…...

基于微信小程序的新闻资讯系统设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…...

vim如何解决‘’文件非法关闭后,遗留交换文件‘’的问题

过程描述&#xff1a; 由于我修改文件时&#xff08;一定得修改了文件&#xff0c;不做任何修改不会产生这个问题&#xff09;的非法关闭&#xff0c;比如直接关闭虚拟机&#xff0c;或者直接断开远程工具的远程连接&#xff0c;产生了以下遗留交换文件的问题&#xff1a; 点击…...

vue3+elementPlus之后台管理系统(从0到1)(day4-完结)

面包屑 创建一个面包屑组件 将路由导入然后格式化map对象 key-value 将当前路由的key和value获取然后存入list数组中 遍历list数据&#xff0c;渲染内容 <!--BreadcrumbCom.vue--> <template><el-breadcrumb separator">"><el-breadcrum…...

基于Flask的旅游系统的设计与实现

【Flask】基于Flask的旅游系统的设计与实现&#xff08;完整系统源码开发笔记详细部署教程&#xff09;✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 该系统采用Python作为后端开发语言&#xff0c;结合前端Bootstrap框架&#xff0c;为用户提供了丰富…...

代码随想录31

目录 leetcode135.分发糖果 思路&#xff1a; leetcode860.柠檬水找零 思路&#xff1a;就是一个个遍历然后判断&#xff0c;用哈希表存储数。 leetcode406.根据身高重建队列 leetcode135.分发糖果 n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 你需…...

【公因数匹配——暴力、(质)因数分解、哈希】

题目 暴力代码&#xff0c;Acwing 8/10&#xff0c;官网AC #include <bits/stdc.h> using namespace std; const int N 1e610; vector<int> nums[N]; int main() {ios::sync_with_stdio(0);cin.tie(0);int n;cin >> n;for(int i 1; i < n; i){int x;ci…...

WPS数据分析000010

基于数据透视表的内容 一、排序 手动调动 二、筛选 三、值显示方式 四、值汇总依据 五、布局和选项 不显示分类汇总 合并居中带标签的单元格 空单元格显示 六、显示报表筛选页...