出名的建站网站,个人微信公众号注册,wordpress相关文章源文件,深圳好客站seo目录
1、GDI对象泄漏
1.1、何为GDI资源泄漏#xff1f;
1.2、使用GDIView工具排查GDI对象泄漏
1.3、有时可能需要结合其他方法去排查
1.4、如何保证没有GDI对象泄漏#xff1f;
2、进程句柄泄漏
2.1、何为进程句柄泄漏#xff1f;
2.2、创建线程时的线程句柄泄漏
…目录
1、GDI对象泄漏
1.1、何为GDI资源泄漏
1.2、使用GDIView工具排查GDI对象泄漏
1.3、有时可能需要结合其他方法去排查
1.4、如何保证没有GDI对象泄漏
2、进程句柄泄漏
2.1、何为进程句柄泄漏
2.2、创建线程时的线程句柄泄漏
3、内存泄漏
3.1、在多态中没有将父类的析构函数声明为virtual函数导致没有执行到子类的析构函数
3.2、使用智能指针shared_ptr发生循环引用问题导致内存泄漏
3.3、第三方注入库有内存泄漏导致进程有内存泄漏
3.4、内存泄漏的危害
3.5、内存泄漏的排查
4、最后 VC常用功能开发汇总专栏文章列表欢迎订阅持续更新...https://blog.csdn.net/chenlycly/article/details/124272585C软件异常排查从入门到精通系列教程专栏文章列表欢迎订阅持续更新...https://blog.csdn.net/chenlycly/article/details/125529931C软件分析工具从入门到精通案例集锦专栏文章正在更新中...https://blog.csdn.net/chenlycly/article/details/131405795C/C基础与进阶专栏文章持续更新中...https://blog.csdn.net/chenlycly/category_11931267.html 在C程序开发维护过程中时常会遇到资源泄漏问题比如GDI对象泄漏、进程线程句柄泄漏以及内存泄漏问题。今天我们就来深入探讨一下这几类资源泄漏以及排查这些泄露的办法。
1、GDI对象泄漏 在Windows平台上做UI客户端编程很多时候都是使用系统GDI对象进行窗口的绘制常见的GDI对象有Pen用来绘制线条的画笔、Brush用来填充颜色的画刷、Bitmap用来处理图片的位图、Font用来设置文字大小的字体、Region区域、DC设备上下文等。
1.1、何为GDI资源泄漏 对于Pen、Brush、Bitmap和Region等在使用前我们需要调用创建这些对象的接口把对象创建出来比如CreatePen/CreatePenIndirect、CreateSolidBrush/CreateBrushIndirect、CreateFont/CreateFontIndirect、CreateCompatibleBitmap等API接口然后在使用完这些对象后需要调用DeleteObject将对象释放掉。对于DC对象则一般调用GetDC去获取窗口的DC对象然后在不使用时需要调用ReleaseDC将DC释放掉。如果不释放这些对象则会导致GDI对象泄漏。 在Windows程序中一个进程的GDI对象总数是有上限的默认情况下上限值为10000个。可以从如下的注册表中可以看到这个值是系统设置的默认值一般情况下不用修改即使修改也不能改成很大的值。 如果发生GDI对象泄漏的代码段频繁地执行程序在持续运行一段时间后进程的GDI对象总数接近或达到10000个上限。当接近上限时就会出现GDI绘图函数内部发生错误返回失败导致窗口绘制异常。紧接着可能就会产生崩溃闪退。
1.2、使用GDIView工具排查GDI对象泄漏 GDI对象持续泄漏对程序可能是致命的一旦接近或达到上限就会导致程序发声崩溃闪退。GDI对象泄漏问题排查起来相对容易一些先用GDIView工具先看一下是哪类GDI对象有泄漏 然后有针对性的查看操作这类GDI对象的代码然后逐步缩小排查的范围。 如果出现窗口绘制或显示异常或者程序无故闪退可以到任务管理器中查看进程的GDI对象总数的值默认情况下不显示GDI对象列右键点击标题栏在弹出窗口中勾选GDI对象选项即可显示 如果总数接近10000个肯定是GDI对象泄漏导致的。可以重新启动程序然后再任务管理器中持续观察进程的GDI对象总数。
1.3、有时可能需要结合其他方法去排查 有时也要结合其他方法来辅助定位比如可以使用历史版本比对法看看是从哪天开始出现泄漏。然后查看前一天svn或git上的代码提交记录或者底层模块库发布记录这样就能有效的缩小问题的排查范围。有次项目中出的问题就出在底层的WebRTC开源库中。当时排查了UI层的代码没有找到泄漏点所以怀疑可能是底层模块有问题。 当时找到了问题的复现办法然后使用历史版本比对法确定了从哪一天开始出现泄漏。然后查看了svn上的代码提交记录以及底层库的发布记录 发现出问题前一天底层开源组件组发布了新版本的WebRTC开源库在这个版本中开源组件组为了处理一个bug添加了一段代码于是找开源组件的同事排查一下他们提交的代码看看是否存在GDI泄漏。一小段时间后他们给出结论说他们新加的代码没问题应该是其他模块引发的。但根据历史版本比对法的对比问题应该就出在WebRTC开源库中但开源组件组始终觉得他们的代码没问题。 于是我到开源组件组那边查看svn上他们的代码修改记录看到他们新增的一段代码果然有问题如下所示
#if defined (WEBRTC_WIN)//修正程序开启DWM导致的鼠标位置问题int desktop_horzers GetDeviceCaps( GetDC(nullptr) DESKTOPHORZRES); // 问题就出在这个GetDC上int horzers GetDeviceCaps(GetDC(nullptr),HORZRES);float scale_rate(float)desktop_horzers/(float)horzers;relative_position.set( relative_ position.x()*scale_rate, relative_ position.y()*scale_rate );
#endif
这段代码中他们调用GetDC接口获取窗口的DC对象在使用完DC对象后没有调用ReleaseDC将DC对象释放掉所以导致了DC对象的泄漏。修改后的代码如下
#if defined (WEBRTC_WIN)//修正程序开启DWM导致的鼠标位置问题HDC hDC ::GetDC(nullptr);int desktop_horzers GetDeviceCaps( hDC, DESKTOPHORZRES);int horzers GetDeviceCaps(hDC,HORZRES);float scale_rate(float)desktop_horzers/(float)horzers;relative_position.set( relative_ position.x()*scale_rate, relative_ position.y()*scale_rate );::ReleaseDC(nullptr, hDC);
#endif
至于开源组件的同事没找到问题可能是他们对UI编程不熟悉导致的。 另一个GDI对象泄漏排查实例可以参见我之前写的文章
使用GDIView工具排查GDI对象泄漏导致程序UI界面绘制异常的问题https://blog.csdn.net/chenlycly/article/details/128625868https://blog.csdn.net/chenlycly/article/details/128625868
1.4、如何保证没有GDI对象泄漏 要保证不出现GDI对象泄漏在GDI对象使用完成后要将之删除或释放掉如果不删除或释放则会导致GDI泄漏。比如使用CreateXXXXXX创建的GDI对象使用完后要用DeleteObject释放调用LoadXXXXXX函数去加载图片资源使用完后也要用DeleteObject释放调用CreateXXXDC创建的DC对象使用完后要用DeleteDC去释放调用GetDC获取到的DC对象使用完后要用ReleaseDC释放。 调用不用的接口去创建或获取GDI对象释放时也要调用对应的释放接口不能混淆在这里给大家大概的罗列一下
创建或获取GDI对象删除或释放GDI对象CreatePen/CreatePenIndirectpen画笔对象、CreateSolidBrush/CreateBrushIndirectbrush画刷对象、CreateFont/CreateFontIndirectFont字体对象、CreateCompatibleBitmapBItmap位图对象对于Create出来的对象要调用DeleteObject释放CreateDC/CreateCompatibleDC创建DC对象调用DeleteDC释放GetDC获取DC对象调用ReleaseDC释放LoadBitmap加载Bitmap位图调用DeleteObject释放LoadImage加载图片资源 如果加载的是Bitmap位图则调用DeleteObject释放 如果加载的是Cursor光标则调用DestroyCursor释放 如果加载的是Icon图标则调用DestroyIcon释放。
对于上面提到的创建GDI对象的API函数在释放时该调用哪个接口直接到MSDN上查看API接口的Remarks部分就会找到对应的说明。比如创建兼容位图的API函数CreateCompatibleBItmap在Remaks部分的说明如下 再比如加载图片的API函数LoadImage其在Remarks部分的说明如下 在调用Windows系统API函数遇到问题时需要到微软MSDN帮助页面中查看API函数的详细说明可能会给出调用函数时的注意事项或者调用函数的示例代码等在说明中可能会找到相关的原因会使用MSDN是一个Windows开发人员最基本的要求 2、进程句柄泄漏 进程句柄包括文件句柄打开文件时产生的句柄、注册表句柄打开注册表节点时产生的句柄、事件句柄、信号量句柄、线程句柄创建线程时产生的句柄、进程句柄创建子进程时产生的句柄等。
2.1、何为进程句柄泄漏 这些句柄在使用完成后需要及时释放如果不释放则会造成句柄泄漏。一般调用CloseHandle去释放句柄比如进程句柄、线程句柄、事件句柄、文件句柄等。当然也有部分句柄需要对应的接口去释放比如注册表句柄需要调用RegCloseKey去关闭。 在Winows系统中进程句柄数也是有上限的默认也是10000个也有对应的注册表项。当进程的句柄数接近或达到10000个上限时就会导致后续产生句柄的操作会执行失败比如调用CreateThread去创建新的线程会失败。关于进程GDI对象上限值的注册表设置路径为 计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows 不仅进程的GDI对象有上限进程的句柄数比如线程句柄、事件句柄等句柄也是有上限的默认也是10000个。注册表对应的节点配置如下 这两个配置项的说明如下 1GDIProcessHandleQuota项设置GDI句柄数量默认值为2710(16进制)/10000(10进制)该值的允许范围为 256 ~ 16384 将其调整为大于默认的10000的值。如果您的系统配置了2G或更多内容不妨将其设置为允许的最大值 16384(10进制)。 2USERProcessHandleQuota项设置用户句柄数量默认值同样为2710(16进制)/10000(10进制)该值的允许范围为 200 ~ 18000 将其调整为更多的数值。同样地对于具有2GB或更多物理内存的系统不妨将用户句柄数直接设置为上限 18000(10进制)。 2.2、创建线程时的线程句柄泄漏 以前我们在项目中就遇到这样的问题有的业务子系统是通过https和平台服务器交互的客户端每执行一个https操作时都会创建一个线程去执行但创建线程后没有调用CloseHanlde将线程句柄关闭掉导致线程句柄发生泄漏当多次执行https操作导致线程句柄过多导致后续再去创建线程创建失败了。可以到Process Explorer中查看进程都占用哪些具体的句柄 这个地方需要注意一下调用CloaseHandle将线程句柄释放掉
HANDLE hThread ::CreateThread( NULL, NULL, ProcessProc, this, NULL, NULL );
if ( hThread ! NULL )
{CloseHandle( hThread );
}
调用CloseHandle将句柄释放掉并不表示将线程结束掉线程是否结束是要看线程函数的线程函数退出了则线程就结束了。 线程结束了不会自动关闭线程句柄。对于线程函数还有一个细节发起线程创建的CreateThread函数返回了不代表线程的代码已经执行到线程函数中了。这点我们在项目中遇到过这类的场景。当时的问题场景是线程函数中访问了一个指针变量将该指针变量的值初始化为NULL的操作放在CreateThread函数调用之后如下所示
// 1、指针变量定义
CVideoDec* g_pVideoDec// 2、创建线程
HANDLE hThread ::CreateThread( NULL, NULL, ProcessProc, this, NULL, NULL );
if ( hThread ! NULL )
{CloseHandle( hThread );
}
g_pVideoDec NULL // 对指针变量进行初始化// 3、线程函数函数中访问了指针变量g_pVideoDec
DWORD WINAPI ProcessCachedMsgThreadProc( LPVOID lpParameter )
{// 线程函数中访问到了该指针变量g_pVideoDec-StartDec();return 1;
}
当然这种做法是不规范的后来发现程序会时不时崩溃在线程函数中使用Windbg分析下来得知是线程中访问了未初始化的变量但这个问题不是必现的。这个不必现就和CreateThread函数返回后线程是否执行到线程函数中有关。 有时CreateThread返回时还没执行到线程函数中紧接着就去初始化指针变量的值是不会崩溃的。但如果CreateThread返回时已经执行到线程函数中就会访问未初始化的指针变量Release下未初始化的内存是个随机值即指针变量的值为随机值所以一般都会引发异常。
3、内存泄漏 内存泄漏是C程序使用动态申请的内存时容易出现的一类典型内存问题。动态申请内存的方式有多种比如使用new要用delete去释放比如使用malloc要用free去释放再比如调用系统API函数HeapCreate或者HeapAlloc要用HeapFree去释放还有可以调用API函数VirtualAlloc要用VirtualFree去释放当然还有其他的API函数。动态申请的内存没有释放则会导致内存泄漏。 之所以会导致内存泄漏可能是忘记释放也可能是写了释放内存的代码但因为种种原因没有执行到内存释放的代码后面这类情况有一定的隐蔽性。下面我们重点说一下后面的这类情况。
3.1、在多态中没有将父类的析构函数声明为virtual函数导致没有执行到子类的析构函数 比如如下的多态代码
class CBase
{
public:CBase();~CBase(); // 没有将父类的析构函数设置为虚函数
} class CDerived : public class CBase
{
public:CDerived();~CDerived();
} // 将new出来的子类对象赋值给父类指针就是多态
CBase* pBase new CDerived;
// ... // 中间代码省略
delete pBase
上述代码因为没有将父类的析构函数~CBase设置为虚函数导致执行到delete pBase时没有调用子类的析构函数导致子类的部分内存没有释放从而引发内存泄漏。特别是新人比较容易犯这类错误之前在帮新人排查问题时遇到过这个场景下的内存泄漏具有一定的隐蔽性。 如果不是析构函数是其他的成员函数如果父类的接口没有声明为virtual多态就不会生效会导致子类重写的成员函数不会被执行到子类重写的成员函数中可能包含了重要的业务代码这样就会导致重要的业务代码没有执行到导致业务出现异常。 之前我们这边的新人将代码移植到国产化机器上时就遇到过新人忘记在父类的接口前添加virtual声明导致子类的接扣执行不到导致业务出现异常当时他查了很久没找出问题后来找我去排查找到这个原因所以对这个问题印象很深 所以我们在定义C类时如果该类可能会被继承一般都要将父类的析构函数设置为虚函数防止出现上述多态场景下子类的析构函数执行不到导致内存泄漏的问题。当然设置虚函数有一定的副作用如果一个类中包含虚函数则类中会自动添加一个虚函数表指针此外虚函数调用时也涉及到二次寻址问题效率上略有影响。
3.2、使用智能指针shared_ptr发生循环引用问题导致内存泄漏 使用shared_ptr可能会出现循环引用问题这使用shared_ptr智能指针的一个典型问题也是一个关于shared_ptr智能指针的面试题场景是两个类中都包含了指向对方的shared_ptr对象这样会导致new出来的两个类没有走析构引发内存泄漏问题。 循环引用问题的示意图如下 相关代码如下
#include iostream
#includememoryusing namespace std;class B;
class A{public:shared_ptrB bptr;~A(){cout~A()endl;}
}class B
{public:shared_ptrA aptr;~B( ){cout~B()endl;}
}int main() {shared_ptrA pa(new A()); // 引用加1shared_ptrB pb(new B()); // 引用加1pa-bptr pb; // 引用加1pa-aptr pa; // 引用加1return 0;
}
执行到上述return 0这句代码时指向A和B两个对象的引用计数都是2。当退出main函数时先析构shared_ptrB pb对象B对象的引用计数减1B对象的引用计数还为1所以不会delete B对象不会进入B对象析构函数所以B类中的shared_ptrA aptr成员不会析构所以此时A对象的引用计数还是2。当析构shared_ptrA pa时A的引用计数减1A对象的引用计数变为1所以不会析构A对象。所以上述代码会导致A和B两个new出的对象都没释放导致内存泄漏。 为了解决上述问题引入了weak_ptr可以将类中包含的shared_ptr成员换成weak_ptr如下 相关代码如下
#include iostream
#includenemoryusing namespace std;class B;
class A{public:weak_ptrB bptr; // 使用weak_ptr替代shared_ptr~A(){cout~A()endl;}
}class B
{public:weak_ptrA aptr; // 使用weak_ptr替代shared_ptr~B( ){cout~B()endl;}
}int main() {shared_ptrA pa(new A());shared_ptrB pb(new B());pa-bptr pb;pa-aptr pa;return 0;
}
3.3、第三方注入库有内存泄漏导致进程有内存泄漏 第三库注入到我们程序进程中有两个典型的场景一种是输入法模块的注入一种是第三方安全软件的注入。输入法要支持所有进程的文字输入正式通过远程注入到所有进程的模块去感知用户的输入的。第三方安全软件为了监控软件的数据操作一般也是需要远程注入到进程中的。 之前项目中就遇到过第三方安全软件的注入模块有内存泄漏导致进程内存耗尽引发程序闪退。对于这类问题可能其他软件运行不会触发内存泄漏只有我们的软件才会触发内存泄漏这个需要拿出足够的证据证明是第三方安全软件的注入模块引起的内存泄漏否则客户会认为这是我们软件的问题因为其他软件都没问题客户可能会不承认这与第三方安全软件有关。当时的问题原因是第三方安全软件处理UDP数据监控的代码有内存泄漏因为我们的软件有大量的音视频数据收发走的是UDP所以触发了第三方安全软件注入模块的内存泄漏。在给出足够的证据后客户找到第三方安全软件提供商然后安全厂商才修复了这个bug。
3.4、内存泄漏的危害 如果发生内存泄漏的代码不会频繁地的执行只是偶尔的执行一下不会引起太大的问题。但如果有内存泄漏的代码被频繁地执行则会频繁地泄漏内存不释放最终可能会导致进程的内存耗尽引发Out of memory内存耗尽的崩溃。 进程启动时系统会给进程分配指定大小的虚拟内存。以32位程序为例系统会分配4GB的虚拟内存其中用户态虚拟内存2GB内核态虚拟内存2GB一般内存泄漏的代码都在用户态所以内存持续泄漏会导致用户态虚拟内存被用尽引发Out of memory的崩溃。当然对于64位程序会分配足够大的虚拟内存。但用户的电脑可能很多天不关机软件一直在持续的运行如果有持续的内存泄漏总有内存用尽的那一天。 我们可以通过Windows自带的任务管理器 去持续观察目标进程的内存变化情况如果内存持续增长不回落则可能存在内存泄漏。 此外Windows自带的任务管理器看不到进程的总的虚拟内存占用可以使用Process Explorer工具查看进程占用的总虚拟内存该工具显示的是用户态的虚拟内存占用 我们一般只需要关注用户态的虚拟内存因为业务代码占用的是用户态的虚拟内存。 我们的程序是32位的系统给进程分配了4GB的虚拟内存其中用户态虚拟内存占2GB内核态虚拟内存占2GB从上图中看当前程序进程的用户态虚拟内存占用达到1.7GB已经快接近2GB的上限了可能再运行一会2GB用户态的内存就要耗尽了程序就会闪退 注意Process Explorer工具默认是不显示Virtual Size虚拟内存列需要右键点击进程列表的标题栏点击“Select Columns”在弹出的窗口中点击“Process Memory”标签页然后将“Virtual Size”选项 3.5、内存泄漏的排查 内存泄漏问题的排查相对比较麻烦但可以使用一些工具去分析。
3.5.1、Windows平台上内存泄漏的排查 在Windows平台上可以使用Windbg使用!heap命令、umdh.exe该工具位于Windbg的安装目录中、DebugDiag、VMMAP以及Visual C专用的Visual Leak Detector等工具。对于Visual Leak Detector工具需要将相关的库编译到模块中。其他几个工具则可以直接使用。 关于如何使用umdh.exe工具去检测内存泄漏问题可以参见我之前写的文章
使用Windbg定位Windows C程序中的内存泄漏https://blog.csdn.net/chenlycly/article/details/121295720https://blog.csdn.net/chenlycly/article/details/121295720 关于如何使用Visual Leak Detector工具去排查可以参见我之前写的文章使用Visual Leak Detector排查内存泄漏问题https://blog.csdn.net/chenlycly/article/details/133041372 内存泄漏问题的定位也不太容易上述工具可能只能检测某些场景下的内存泄漏如果在使用一个工具检测不出来时可以使用其他工具试试。 此外从Visual Studio 2019的16.9版本开始Visual Studio引入了google的强大内存监测工具AddressSanitizerAddressSanitizer原先只在Linux系统中被支持继承在gcc中就像gcc那样提供编译选项上的支持 在安装高版本的Visual Studio时可以将“C AddressSanitizer”安装选项勾选上这样Visual Studio中就支持AddressSanitizer了。 AddressSanitizer简称ASan是google提供的一款面向C/C语言的内存错误问题检查工具它可以检测出堆溢出Heap buffer overflow、栈溢出Stack buffer overflow、全局变量越界Global buffer overflow、已释放内存使用Use after free 、初始化顺序Initialization order bugs、内存泄漏Use after free 等多个内存问题。 AddressSanitizer项目地址https://github.com/google/sanitizers/wiki/AddressSanitizer 参考文档页面AddressSanitizerAlgorithm · google/sanitizers Wiki · GitHub 如果要使用AddressSanitizer内存检测工具必须要使用Visual Studio 2019的16.9及以上的版本。此外AddressSanitizer不能像Windbg那样独立运行直接附加到目标进程上去分析需要使用AddressSanitizer相关编译选项重新编译代码才行。 对于如何在Visual Studio中使用AddressSanitizer内存分析工具可以看一下微软官方文章的详细说明
在Visual Studio中集成AddressSanitizerhttps://docs.microsoft.com/zh-cn/cpp/sanitizers/asan?viewmsvc-170https://docs.microsoft.com/zh-cn/cpp/sanitizers/asan?viewmsvc-170
3.5.2、Linux平台上内存泄漏的排查 在Linux平台上常用的内存检测工具有Valgrind和AddressSanitizer这两个工具各有优势。 Valgrind工具可以直接监测目标进程不需要重新编译代码用起来比较方便。但Valgrind在监测内存时比较消耗内存同时会严重拖慢程序的运行速度这对于需要实时响应的服务器来讲是个很大的问题。 AddressSanitizer是google出品的内存检测工具gcc4.8及以上版本才内置了AddressSanitizer通过编译选项去使用该工具需要重新编译代码该工具会占用更少的内存不会明显拖慢程序的运行速度。不过要使用该工具需要将gcc4.8及以上的版本才行。 关于AddressSanitizer的优势以及如何使用AddressSanitizer可以查看我之前写的文章
为什么选择C/C内存检测工具AddressSanitizer如何使用AddressSanitizerhttps://blog.csdn.net/chenlycly/article/details/132863447
4、最后 上面详细讲解了GDI对象泄漏、进程句柄资源泄漏和内存泄漏三大类问题希望能给大家提供一定的借鉴和参考。