您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

如何定位内存泄漏

时间:2023-01-09 15:35:28  来源:  作者:睡在床板下

介绍

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

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

现象

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

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

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

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

原图没有了,查看90天的提交内存大致如上

基本上可以确定程序存在内存泄漏,让运维通过工具保存了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文件,下次就不会出现这么低级的问题。

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



Tags:内存泄漏   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
介绍本文主要介绍一种通过windbg分析内存泄漏的方法,方法也适用linux。这个内存泄漏问题比较经典,我个人认为是自己这么多年bug定位中一个非常好的bug,并且在分析的过程中,也有...【详细内容】
2023-01-09  Tags: 内存泄漏  点击:(0)  评论:(0)  加入收藏
作者:rangobai,腾讯CSIG数据工程师| 导语要有性能意识,量变引起质变,简单如一行日志都会在高并发的情况引发血案,考验着研发的技术功底一 案件背景9月的某个上午,业务侧突然反...【详细内容】
2022-09-28  Tags: 内存泄漏  点击:(81)  评论:(0)  加入收藏
记录一个关于线程内存泄漏问题的定位过程,以及过程中的收获。1. 初步定位是否存在内存泄漏:想到内存泄漏,首先查看/proc/meminfo,通过/proc/meminfo可以看出总体内存在下降。确...【详细内容】
2022-04-26  Tags: 内存泄漏  点击:(211)  评论:(0)  加入收藏
简单介绍内存泄漏&内存抖动内存泄漏:Memory leak, 是一种资源泄漏,主因是计算机程序对存储器配置管理失当,失去对一段已分配内存空间的控制,造成程序继续占用已经不再使用的内存...【详细内容】
2022-04-13  Tags: 内存泄漏  点击:(138)  评论:(0)  加入收藏
一、内存泄漏(memory leak)内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存...【详细内容】
2021-09-03  Tags: 内存泄漏  点击:(231)  评论:(0)  加入收藏
导读本文介绍Java诸多优化实例:第一,排查堆上、堆外内存泄露;第二,使用arthas、jaeger、tcpdump、jstack做性能优化;第三,排查进程异常退出的原因,如被杀、System.exit、Java调用的...【详细内容】
2020-11-25  Tags: 内存泄漏  点击:(132)  评论:(0)  加入收藏
前言记录一次线上JVM堆外内存泄漏问题的排查过程与思路,其中夹带一些「JVM内存分配机制」以及「常用的JVM问题排查指令和工具分享」,希望对大家有所帮助。在整个排查过程中,我...【详细内容】
2020-08-27  Tags: 内存泄漏  点击:(87)  评论:(0)  加入收藏
Node.js 使用的是 V8 引擎,会自动进行垃圾回收(Garbage Collection,GC),因而写代码的时候不需要像 C/C++ 一样手动分配、释放内存空间,方便不少,不过仍然需要注意内存的使用,避免造成内存泄漏(Memory Leak)。...【详细内容】
2020-08-24  Tags: 内存泄漏  点击:(132)  评论:(0)  加入收藏
导读本文介绍Java诸多优化实例:第一,排查堆上、堆外内存泄露;第二,使用arthas、jaeger、tcpdump、jstack做性能优化;第三,排查进程异常退出的原因,如被杀、System.exit、Java调用的...【详细内容】
2020-08-11  Tags: 内存泄漏  点击:(96)  评论:(0)  加入收藏
一般情况下,忽视内存管理不会对传统的网页产生显著的后果。这是因为,用户刷新页面后,内存数据都被清理了。...【详细内容】
2020-06-08  Tags: 内存泄漏  点击:(128)  评论:(0)  加入收藏
▌简易百科推荐
介绍本文主要介绍一种通过windbg分析内存泄漏的方法,方法也适用linux。这个内存泄漏问题比较经典,我个人认为是自己这么多年bug定位中一个非常好的bug,并且在分析的过程中,也有...【详细内容】
2023-01-09  睡在床板下    Tags:内存泄漏   点击:(0)  评论:(0)  加入收藏
让我们深入探讨 DevOps 和 DevSecOps 管道中密码密钥管理的各个方面。 当今的数字业务有望以闪电般的速度创新、执行和发布产品。自动化工具的广泛采用,加上 DevOps 和DevSec...【详细内容】
2023-01-09  qaseven     Tags:软件开发   点击:(1)  评论:(0)  加入收藏
WebAssembly (WASM) 在过去几年一直是一个流行词。 这是一项引起广泛关注但在实践中应用较少的技术。 我一直很好奇它的现状,所以我调查并总结了我的发现。 其中一些可能会让...【详细内容】
2023-01-07  启辰8  今日头条  Tags: WebAssembly   点击:(2)  评论:(0)  加入收藏
前言项目是基于swoole开发,框架也是公司内自己开发的框架,并没有用外界热门的swoole框架,swoole是4.0.0版本。项目需要执行大量的自动任务,框架是通过swoole的sendMessage方法将...【详细内容】
2022-12-29  博读代码  51CTO  Tags:后端   点击:(10)  评论:(0)  加入收藏
现在的各种开源项目中使用 Vue 的越来越多了,作为一个后端程序员不会点 Vue 也都玩不转了。所以抽空学习了一下 Vue 的简单用法,整理成笔记,方便有需要的同学一起学习。Vue 是...【详细内容】
2022-12-28  猿来猿往  今日头条  Tags:VUE   点击:(15)  评论:(0)  加入收藏
C语言几乎唯一的缺点就是,需要手动管理内存。抛开这点之外,我觉得其他语言都不如C语言[呲牙]所以,虽然自动内存管理比较复杂,但我还是给scf编译器框架加了静态的GC算法。在编程...【详细内容】
2022-12-27  底层技术栈  今日头条  Tags:编译器   点击:(14)  评论:(0)  加入收藏
编译器在经过词法分析、语法分析之后,就把源代码变成了抽象语法树(AST)。接下来,编译器的任务就是把AST变成机器码。AST,是一个表示代码逻辑的树形结构,它是不能直接顺序遍历的,而...【详细内容】
2022-12-26  底层技术栈  今日头条  Tags:编译   点击:(13)  评论:(0)  加入收藏
大家在平时的开发过程中估计不会经常碰到需要主动取消一个 Fetch 请求的需求,所以一部分同学可能对这一块知识不是很了解。没有关系,看完这篇文章你就能够掌握关于如何终止一...【详细内容】
2022-12-25  嘻呱互联   网易号  Tags: Promise   点击:(12)  评论:(0)  加入收藏
很早前就想写一篇关于 eBPF 的文章,但是迟迟没有动手,这两天有点时间,所以就来写一篇。这文章主要还是简单的介绍 eBPF 是用来干什么的,并通过几个示例来介绍是怎么玩的。这个技...【详细内容】
2022-12-24   IT架构师联盟     Tags:eBPF   点击:(4)  评论:(0)  加入收藏
了解过lombok底层原理的都知道其使用的就是的插入式注解,那么今天笔者就以真实场景演示一下插入式注解的使用。需求我们为公司提供了一套通用的java基础组件包,组件包内有不同...【详细内容】
2022-12-16  王路飞学Java  今日头条  Tags:插入式注解   点击:(15)  评论:(0)  加入收藏
站内最新
站内热门
站内头条