百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

如何定位内存泄漏(内存定位工具)

nanshan 2024-10-05 18:33 28 浏览 0 评论

介绍

本文主要介绍一种通过windbg分析内存泄漏的方法,方法也适用linux。

这个内存泄漏问题比较经典,我个人认为是自己这么多年bug定位中一个非常好的bug,并且在分析的过程中,也有许多需要思考的地方。通过该问题的分析,你可以了解到分析内存的基本方法和思路。

现象

后台检测程序在某天上午上报了内存警告,大概就是某程序的提交内存达到了1.0G。

这里需要解释下:在windows下32位应用程序如果提交内存大于某个阈值,比如我正常程序运行时提交内存最多应该只有500M,当检测程序发现该程序提交内存突然大于1.0G了,说明程序可能出现了内存泄漏。----当时就是这个进程的提交内存大于1.0G并发生了告警。

登陆后台查看,了解到如下信息:

  • 该进程已经连续运行了90天
  • 提交内存每天都在持续上涨,从启动到目前为止大概累计上升了800M。
  • 句柄、线程数等资源均正常

基本上可以确定程序存在内存泄漏,让运维通过工具保存了fulldump,并重启进程(否则内存告警会一直提示)。

这时候对于有经验的人员,这个问题因为并不没有对生产环境造成影响,且等到问题发生异常时还有比较长时间,所以可以不需要立刻恢复现场,否则当问题无法定位时而现场被破坏,将很难解决问题。

分析思路

  • 代码review:通过比较上个版本和上上个版本之间的差异,找到内存泄漏的地方。

的确是可以,但存在几个问题,因为本身每天内存泄漏的非常少,且之前版本大都一个月不到就升级了,不能确定这个问题是否是之前一个版本引入的,也可能是很多个版本前引入的?

其次:这个进程处理的消息类型很多,可能有问题的消息处理早就存在,只是最近一段时间其他服务升级,导致有bug的消息处理模块被触发。所以以上原因通过review近几个版本并不一定能找到。

还有,review可能能找到多个泄漏点,但可能存在遗漏的情况,并不是该问题的本质原因,修改后问题还可能存在。

但这个方法对于有人力富于的公司还是可以的,就让一个同事review代码,还是有效果的。

  • 静态代码检测工具:

公司没有基础,临时部署时间来不及。

  • 构建复现环境:由于问题出现原因不知,而复现时间太长,找不到快速复现的方法。

在平时工作中,通过复现注释代码缩小可疑模块,是我们大都会用的有效方法,但这个场景很难找到复现方法。

  • 规避问题:通过每周半夜重启程序,规避该问题。

这个方法在很多公司都存在,因为疑难问题的解决的确非常耗时,所以一般会有一个看门狗程序,在客户不知不觉时重启进程,快速恢复,也是非常常用的方法。这对于我来说,是下下策,不到万不得已,不会使用,印象中自己没怎么用过。

  • 通过技能查找问题的根本原因。

umdh:通过在A时间点获取一个进程内存镜像,然后一段时间出现内存泄漏后,在B时间点再获取一个进程内存镜像,通过比较找到之间的差异。理论可行,但对于这个问题意义不大,本身进程是一个高并发进程,每秒都要处理上百个消息,内存有上百次的申请和释放,A和B比较后差异会非常大,很难找到真实的内存泄漏模块。

通过以上思考,在有限人力下,通过windbg分析dump的内存,查找真实内存泄漏是快速并有效的方法,下面我就针对该问题给大家介绍下我的分析思路,最后问题的解决大致花费了半个工作日的时间。

准备工作

当时的dump我保存到了百度网盘。

  • [下载地址](https://pan.baidu.com/s/1vUjAr7edFTxxcKGnGEaatQ "下载地址")(提取码:11bg)
  • 设置好系统的pdb
e:\mylocalsymbols;SRV*e:\mylocalsymbols*http://msdl.microsoft.com/download/symbols

分析方法

C++的release版程序,内存携带的信息是非常有限的,大致就是三个维度:

  • 内存大小:每次malloc申请的大小,通过大小,我们可以找到对应的结构体、类
  • 内存地址内容:通过查看内存地址内容,比如有字符串、有特殊的值,找到申请的模块
  • 内存申请次数:通过每小时申请的频率,可以找到具体的消息类型

下面就是通过这三个维度找到具体的原因。

查找内存大小

打印所有堆块信息

!heap -s

显示如下

0:000> !heap -s
HEAPEXT: Unable to read ntdll!RtlpDisableHeapLookaside
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-----------------------------------------------------------------------------
006f0000 00000002 1246976 1241928 1246976 982 236 81 0 a LFH
00190000 00001002 3136 1564 3136 390 7 3 0 0 LFH
External fragmentation 24 % (7 free blocks)
00110000 00001002 256 4 256 1 1 1 0 0
02050000 00001002 256 176 256 1 18 1 0 0 LFH
02240000 00001002 256 4 256 2 1 1 0 0
006a0000 00001002 64 12 64 4 2 1 0 0
044f0000 00001002 256 216 256 7 4 1 0 0 LFH
119d0000 00001002 7424 5820 7424 134 133 4 0 c8 LFH
14290000 00001003 256 4 256 2 1 1 0 bad
141d0000 00001003 256 4 256 2 1 1 0 bad
17f20000 00001003 256 4 256 2 1 1 0 bad
19030000 00001003 256 4 256 2 1 1 0 bad
191b0000 00001003 256 4 256 2 1 1 0 bad
19380000 00001003 256 4 256 2 1 1 0 bad
19300000 00001003 256 4 256 2 1 1 0 bad
155f0000 00001003 256 4 256 2 1 1 0 bad
-----------------------------------------------------------------------------

通过观察,我们知道了是006f0000堆块占用了大量内存

HEAPEXT: Unable to read ntdll!RtlpDisableHeapLookaside
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-----------------------------------------------------------------------------
006f0000 00000002 1246976 1241928 1246976 982 236 81 0 a LFH

查看堆块内存百分比

内存持续上涨可能是某块固定大小内存被重复申请,所以统计下该堆块中各个内存大小的分配次数

!heap -stat -h 006f0000

查找堆中各个内存大小占用的百分比

0:000> !heap -stat -h 006f0000
unable to resolve ntdll!RtlpStackTraceDataBase
heap @ 006f0000
group-by: TOTSIZE max-display: 20
size #blocks total ( %) (percent of total busy bytes)
14 23acbbe - 2c97ead8 (92.78)
a4 2ba0c - 1bf2fb0 (3.63)
1000 8f5 - 8f5000 (1.16)
1a4 3b9c - 61cbf0 (0.79)
20c 15fb - 2cfdc4 (0.37)
25 b77d - 1a8511 (0.22)
64 3ba0 - 174a80 (0.19)
24 75ae - 108c78 (0.13)
11c e4a - fda18 (0.13)
84c 164 - b89b0 (0.09)
400 172 - 5c800 (0.05)
234 265 - 54684 (0.04)
1c 2c2e - 4d508 (0.04)
1c0 287 - 46c40 (0.04)
c00 4b - 38400 (0.03)
20 1a12 - 34240 (0.03)
3bc ce - 30148 (0.02)
50 8da - 2c420 (0.02)
800 4c - 26000 (0.02)
2ba d2 - 23c94 (0.02)

size #blocks total ( %) (percent of total busy bytes)
14 23acbbe - 2c97ead8 (92.78)

TOP 20 中显示,最多的一个大小为 0x014 的分配次数为 0x23acbbe 次, 总共大概有700M左右。基本接近内存泄漏的总数。

所以这里得出几个结论:

  • 每次内存泄漏的大小是20字节。
  • 总共分配了0x23acbbe次,运行了90天,也就是每小时17318次/小时

定位内存来源

找到了大量的内存是0x014字节大小的,但是根据这个条件我们也找不到具体的代码啊?下面是几个思路

  • 根据大小

根据内存大小(0x14)去代码中查找大小为(0x14)的类、结构体、宏等等相关代码,然后找到原因。有几个问题:

1)、进程包含了很多其他组的dll,有的我没代码权限,无法遍历

2)、结构体、类太多了,人眼遍历太难了(针对这个问题我后来开发了一个工具,通过pdb文件可以找到程序中指定大小的所有结构体和类,后续章节讲解)

  • 内存内容

显示所有大小为(0x14)内存的地址,看它的地址内容有没有什么特点,比如是否有特殊的字符串、固定的二进制头??? 显示所有分配大小为 0x14的内存

命令
!heap -flt s 14
0:000> !heap -flt s 14
unable to resolve ntdll!RtlpStackTraceDataBase
_HEAP @ 6f0000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
0071c038 0004 0000 [00] 0071c040 00014 - (busy)
0071c2e8 0004 0004 [00] 0071c2f0 00014 - (busy)
0071e498 0004 0004 [00] 0071e4a0 00014 - (busy)
0071e4f8 0004 0004 [00] 0071e500 00014 - (busy)
0071e518 0004 0004 [00] 0071e520 00014 - (busy)
0071e5f8 0004 0004 [00] 0071e600 00014 - (busy)
0071e638 0004 0004 [00] 0071e640 00014 - (busy)
0071e658 0004 0004 [00] 0071e660 00014 - (busy)
0071e798 0004 0004 [00] 0071e7a0 00014 - (busy)
007374f0 0004 0004 [00] 007374f8 00014 - (busy)
00737510 0004 0004 [00] 00737518 00014 - (busy)
00737530 0004 0004 [00] 00737538 00014 - (busy)
00737550 0004 0004 [00] 00737558 00014 - (busy)
00737570 0004 0004 [00] 00737578 00014 - (busy)
00737590 0004 0004 [00] 00737598 00014 - (busy)
007375b0 0004 0004 [00] 007375b8 00014 - (busy)
007375d0 0004 0004 [00] 007375d8 00014 - (busy)
007375f0 0004 0004 [00] 007375f8 00014 - (busy)
00737610 0004 0004 [00] 00737618 00014 - (busy)
00737630 0004 0004 [00] 00737638 00014 - (busy)
00737650 0004 0004 [00] 00737658 00014 - (busy)
00737670 0004 0004 [00] 00737678 00014 - (busy)
00737690 0004 0004 [00] 00737698 00014 - (busy)
..............
..............

随机抽查几个地址,看下地址内存,都是00 00 00 00 00

大都是这样的值,实在是看不出规律。

建议

一般公司都会封装malloc、new函数,并分配一个模块号,每个内存地址头部都会携带id号,如下:

xxx_malloc(int nModleID,size_t size);

这样通过地址空间头也可以找到分配的模块。

  • 分配次数

大小0x14的内存在90天时间内总共分配了23acbbe 次, 0x23acbbe = 37407678/(90(天)*24(小时) ≈ 17318次/小时。 这个内存几乎每小时被申请17318次。进程有个统计功能:每个小时会统计处理的消息类型次数,那分析下数量级在1w~3w左右的消息即可,大概是4个消息类型,然后通过对这四个代码review才发现内存泄漏点。

if(total_fee){
LPADD_FEE pAddFee = new ADD_FEE;
ZeroMemory(pAddFee, sizeof(ADD_FEE));
pAddFee->nFee = total_fee;
gdt.nTotalFee = total_fee;
}

结构体 ADD_FEE ,刚好是20字节

typedef struct _tagADD_FEE{
int nFee;
int nReserved[4];
}ADD_FEE, *LPADD_FEE;

完全符合!! 问题解决

总结

这是一个低级错误导致的。为了避免类视问题,引入代码静态检测

1)、cppcheck

2)、pclint

最后选了pclint。配合jenkins,每天凌晨进行代码静态检查,并输出和上个版本的diff文件,下次就不会出现这么低级的问题。

在大公司里面都会有非常多的检测工具、流程、方法论,都是前人经验的积累,虽然有点冗余繁琐,但却非常有效。当你离开这个平台后,缺少了这些流程,一旦遇到疑难问题你才发现自己能用的手段真的很少。

相关推荐

提升网络安全 cisco asa5512-k8防火墙促

(中关村在线网络安全行情)ciscoasa5512-k8为标准下一代防火墙,能够为中小型网络提供出色的安全防护和流量管控服务。最近这款防火墙设备在京东商城的促销价格为14299元,有需要的用户可以关注...

太一星晨:负载均衡性能参数如何测评?

海外网2014-08-0816:03:568月6日消息,当前,在云计算和大数据为主导的需求环境下,负载均衡和应用交付已为大型企业数据中心“保驾护航”的必备品。不过,负载均衡作为一种比较“新奇”的设备...

Nginx架构揭秘:如何用5大核心机制扛住百万级并发

1.灵魂拷问:为什么全球Top1000网站中65%选择Nginx?17Nginx不仅是Web服务器,更是高并发架构的核武器。其单机支持10万+并发连接的秘密,源于三大设计哲学:事件驱动模型:非阻塞...

高并发场景下,Nginx性能如何提升10倍?

大家好,我是mikechen。在高并发场景,Nginx是流量入口的第一道防线,如果想拦截亿级流量,需要Nginx合理调优才能应对@mikechen。本文作者:陈睿|mikechen文章来源:mike...

紧急避坑!数据库突现数十GB临时文件?原因与根治方案揭秘

引言:某天深夜,运维小王突然收到磁盘爆满的告警,追踪发现Kingbase数据库的syssql_tmp目录竟堆积了数十GB的临时文件!这些神秘文件为何产生?会引发哪些风险?如何彻底根治?本文将带你深入探...

互联网大厂后端必看!3 步搞定 Nginx IP 限流,服务器扛住百万流量

作为互联网大厂的后端开发人员,你是否曾遇到过这样的场景:服务器突然涌入大量请求,服务响应速度急剧下降,甚至出现崩溃?这时候,Nginx的IP访问限流策略就显得尤为重要。然而,不少开发者在配置N...

MySQL max_connections 达到最大值 – 我们如何解决它

您的网站是否显示MySQLmax_connections达到最大限制错误?通常,当我们尝试连接到MySQL服务器时,MySQLmax_connections值不足会导致“Tooma...

Nginx百万并发背后技术揭秘!(nginx并发能力是多少)

在互联网业务高速发展的今天,用户访问量呈指数级增长,服务器面临的并发压力也越来越大。一个高并发的网站,如果处理不当,可能会出现请求超时、服务器宕机、用户体验下降等问题。Nginx作为当前最流行的高性...

Nginx底层原理:一文解析Nginx为什么并发数可以达到3w!

Nginx以其高性能,稳定性,丰富的功能,简单的配置和低资源消耗而闻名。本文从底层原理分析Nginx为什么这么快!Nginx的进程模型Nginx服务器,正常运行过程中:多进程:一个Mast...

Nginx合集-并发连接能力优化(nginx高并发调优)

一、前言nginx服务器老是报告TIME_WAIT告警,ESTABLISHED告警,检查nginx配置和系统网络配置发现现有的配置并发能力太弱,无法满足现有的并发请求的需求。二、解决方法改进方法...

开源OS上安装Gnome Flashback经典桌面

1安装GnomeFlashback对于用户来说,相比Unity桌面,GnomeFlashback桌面环境是一个简单的并且不错的选择,可以让你找回过去经典的桌面。GnomeFlashback基于G...

新手篇 — 虚拟机系统的使用与常见问题

本文章会详细介绍虚拟机系统的使用与常见问题,有很多读者都会遇到这样的情况,软件装不上,自己的电脑中软件很多,又不想换电脑系统,那么虚拟机可以帮你解决这个烦恼,由于文章内容写的比较详细,内容会比较多,可...

VMware虚拟机与主机之间无法复制粘贴解决

问题:VMware安装系统后发现无法直接与主机之间进行复制粘贴了,怎么办?解决办法:按照以下3步进行1、设置中客户机隔离检查2、重新安装VMwareTools3、重启电脑...

实现VMware虚拟机与物理主机共享文件夹

在安装虚拟机之后,难免会遇到需要将文件从主机拷到虚拟机当中,但是很尴尬的事情就是不能直接将文件从主机拖到虚拟机中,所以只能借助U盘,但是频繁的插拔U盘非常的繁琐。为了解决这一需求,就可以将物理主机和...

在 Windows 11 或 10 上安装 Virt-viewer 的单行命令

Virt-Viewer(或RemoteViewer)是Redhat提供的一个开源程序,允许用户控制和查看运行在本地或远程服务器上的虚拟机。它体积轻巧,并提供了一个简单的图形用户界面来访问由L...

取消回复欢迎 发表评论: