C# 处理内存泄漏问题

C# 处理内存泄漏问题

  • 背景
    • 什么是 CLR?
    • 什么是内存泄漏?
    • 如何知道产生了内存泄漏?
  • 调试方法
    • 什么是调试器?
    • 调试的原理
    • 调试示例
  • 内存泄漏检测方法:SOS
    • DumpHeap

背景

参考链接:nvestigating .NET Memory Management and Garbage Collection

在 CLR 中,GC(垃圾收集器,Garbage Collection) 管理一切内存。之所以会有内存泄漏,是因为 GC 不能总是正确的释放内存。本文最后将介绍如何使用 SOS(Son of Strike)来检查内存和对象分配

什么是 CLR?

参考:CLR

CLR(Common Language Runtime)是公共语言运行时环境

.NET 提供了一个称为公共语言运行时的运行时环境,可以编写利用托管执行环境的代码。 使用面向运行时的语言编译器开发的代码称为托管代码

简而言之,就是类似于 Java 虚拟机,你写的代码运行在这上面,你申请的内存,释放的内存都由 CLR 进行管理。

托管代码具有许多优点

  • 垃圾回收。
  • 性能改进
  • 能够轻松使用用其他语言开发的组件
  • 类库提供的可扩展类型
  • 语言功能,如面向对象的编程的继承、接口和重载
  • 允许创建多线程的可缩放应用程序的显式自由线程处理支持
  • 结构化异常处理支持
  • 自定义特性支持
  • 使用委托取代函数指针,从而增强了类型安全和安全性

什么是内存泄漏?

简单来说,就是在分配内存时,无论出于何种原因,你的程序运行后都不会释放该内存,于是就导致了内存泄漏。 在.NET语言中,您可以通过创建对象来分配内存,并通过允许对该对象的引用出现范围来释放内存。例如:

void MethodName()
{
    Object o = new Object();
    //Create a new Object and store a reference to it as o
    DoSomethingWith(o);
    //Use the new Object by passing the reference to o
    o = null;
    //Lose the reference to the new Object, it is now eligible for freeing
}
//o is now out of scope so can be freed

当 o 在作用域范围内,GC 可以检查对象是否有任何引用。 如果没有什么引用对象,则可以释放。 如果 DoSomethingWith 方法存储对象 o 的引用,那么当 GC 检查是否正在使用时,仍然会有引用,因此它将无法释放内存。

这里多次提到了引用一词, 当新建对象时,实际上不会返回对象,而是对在堆空间中创建的新对象的引用

只要 DoSomethingWith 不存储对对象的引用,当该对象到达该方法的末尾并超出范围时,引用将丢失,并且垃圾收集器可以释放内存。 因此,在这种情况下,没有内存泄漏

如何知道产生了内存泄漏?

方法很简单,如果程序有内存泄漏,那么程序运行一段时间后,会捕获 OutofMemory(OOM)异常。 或者使用以下的方法实时监视内存增长情况:

Task.Run(async () => {
	while (true) {
		string memory = "";
		using (Process proc = Process.GetCurrentProcess()) {
			long currentMemory = proc.PrivateMemorySize64;
			memory = $"Memory: {currentMemory} B";
		}
		Console.WriteLine(memory);
		await Task.Delay(1000);
	}
});

调试方法

参考链接:debugging

什么是调试器?

关于调试器:Debugging Tools for Windows (WinDbg, KD, CDB, NTSD)

有两种类型的调试器(debugger):内核和用户模式(kernel 和 user-mode)。内核调试器被用来调试驱动程序和Windows内核。 用户模式调试者用于应用程序和服务。

本文只讨论用户模式下的 debugger,Windows调试工具包中有两个调试器,下载地址:Windows Debugger

  • WinDbg:GUI 工具
  • cdb:命令行工具

这两个调试器都提供了对 dbgEng.dll 的封装,即调试的核心方法,它们的调试命令和返回值都是相同的,因此只需选择您喜欢的工具并坚持下去即可。这里推荐CDB。

调试的原理

.NET 应用程序的编写执行过程如下

  1. .NET支持多种语言编写:C#,VB.NET或任何其他符合CLI的语言
  2. 此代码被编译成通用的MSIL格式
  3. 在运行时,将MSIL转换为为对应的CPU架构的二进制文件(JITed),才能执行

举个列子:

① .net 代码

static void DoSomething()
{
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(String.Format("Number: {0}", i));
    }
}

② 编译成 MSIL

.method private hidebysig static void  DoSomething() cil managed  
{
  // Code size       43 (0x2b)
  .maxstack  2
  .locals init ([0] int32 i,
           [1] bool CS$4$0000)
  IL_0000:  nop
  IL_0001:  ldc.i4.0
  IL_0002:  stloc.0
  IL_0003:  br.s       IL_0021
  IL_0005:  nop
  IL_0006:  ldstr      "Number: {0}"
  IL_000b:  ldloc.0
  IL_000c:  box        [mscorlib]System.Int32
  IL_0011:  call       string [mscorlib]System.String::Format(string,
                                                              object)
  IL_0016:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_001b:  nop
  IL_001c:  nop
  IL_001d:  ldloc.0
  IL_001e:  ldc.i4.1
  IL_001f:  add
  IL_0020:  stloc.0
  IL_0021:  ldloc.0
  IL_0022:  ldc.i4.s   10
  IL_0024:  clt
  IL_0026:  stloc.1
  IL_0027:  ldloc.1
  IL_0028:  brtrue.s   IL_0005
  IL_002a:  ret
} // end of method Program::DoSomething

③ 转换成二进制文件

55 8b ec 83 ec 18 83 3d 14 2e 92 00 00 74 05 e8 c5 a3 f4 76 33 d2 89 55-fc c7 45 f8 00 00 00 00 90 33 d2 89 55 fc 90 eb 41 90 b9 38 2b 33 79 e8 40 1f 79 fd 89 45 f4 8b 05 30 20 fb 01 89 45 ec 8b 45 f4 8b 55 fc 89 50 04 8b 45 f4 89 45 e8 8b 4d ec 8b 55 e8 e8 ee 72 13 76 89 45 f0 8b 4d f0 e8 cb 36 61 76 90 90 ff 45 fc 83 7d fc 0a 0f 9c c0 0f b6 c0 89 45 f8 83-7d f8 00 75 ac 90 8b e5 5d c3

debugger 会解码字节并显示程序集,如下所示,注意到第二列显示了上述字节

031800a8 55              push    ebp  
031800a9 8bec            mov     ebp,esp
031800ab 83ec18          sub     esp,18h
031800ae 833d142e920000  cmp     dword ptr ds:[922E14h],0
031800b5 7405            je      031800bc
031800b7 e8c5a3f476      call    mscorwks!JIT_DbgIsJustMyCode (7a0ca481)
031800bc 33d2            xor     edx,edx
031800be 8955fc          mov     dword ptr [ebp-4],edx
031800c1 c745f800000000  mov     dword ptr [ebp-8],0
031800c8 90              nop
031800c9 33d2            xor     edx,edx
031800cb 8955fc          mov     dword ptr [ebp-4],edx
031800ce 90              nop
031800cf eb41            jmp     03180112
031800d1 90              nop
031800d2 b9382b3379      mov     ecx,offset mscorlib_ni+0x272b38 (79332b38) (MT: System.Int32)
031800d7 e8401f79fd      call    0091201c (JitHelp: CORINFO_HELP_NEWSFAST)
031800dc 8945f4          mov     dword ptr [ebp-0Ch],eax
031800df 8b053020fb01    mov     eax,dword ptr ds:[1FB2030h] ("Number: {0}")
031800e5 8945ec          mov     dword ptr [ebp-14h],eax
031800e8 8b45f4          mov     eax,dword ptr [ebp-0Ch]
031800eb 8b55fc          mov     edx,dword ptr [ebp-4]
031800ee 895004          mov     dword ptr [eax+4],edx
031800f1 8b45f4          mov     eax,dword ptr [ebp-0Ch]
031800f4 8945e8          mov     dword ptr [ebp-18h],eax
031800f7 8b4dec          mov     ecx,dword ptr [ebp-14h]
031800fa 8b55e8          mov     edx,dword ptr [ebp-18h]
031800fd e8ee721376      call    mscorlib_ni+0x1f73f0 (792b73f0) (System.String.Format(System.String, System.Object), mdToken: 060001bd)
03180102 8945f0          mov     dword ptr [ebp-10h],eax
03180105 8b4df0          mov     ecx,dword ptr [ebp-10h]
03180108 e8cb366176      call    mscorlib_ni+0x6d37d8 (797937d8) (System.Console.WriteLine(System.String), mdToken: 060007c8)
0318010d 90              nop
0318010e 90              nop
0318010f ff45fc          inc     dword ptr [ebp-4]
03180112 837dfc0a        cmp     dword ptr [ebp-4],0Ah
03180116 0f9cc0          setl    al
03180119 0fb6c0          movzx   eax,al
0318011c 8945f8          mov     dword ptr [ebp-8],eax
0318011f 837df800        cmp     dword ptr [ebp-8],0
03180123 75ac            jne     031800d1
<?XML:NAMESPACE PREFIX = SKYPE /> 03180125 90               nop
03180126 8be5            mov     esp,ebp
03180128 5d              pop     ebp
03180129 c3              ret

当使用上述方式进行调试,而不是用 Visual Studio 进行调试时,正在查看的代码不再是编写的 CLI 语言,它已被优化成为CPU可以理解的东西

调试示例

了解汇编语言的基础知识有助于进行此级别的调试。如下是一个控制台应用程序

try
{
    AppSettingsReader asr = new AppSettingsReader();
    string date = asr.GetValue("DateFormat", typeof(string)).ToString();
    RunCommand(date);
}
catch (Exception)
{
    //HA HA HA No one can hear you scream in here!!!!!!
}

debugger 下载后,安装目录如下所示

类型 目录
安装位置 C:\Program Files (x86)\Windows Kits\10
windbg.exe 位置 C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe
windbg.exe 位置 C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe

我们可以把目录 C:\Program Files (x86)\Windows Kits\10\Debuggers\x64 添加到环境变量中,这样子在新打开的 cmd 窗口中即可直接使用 cdb 或者 windbg 命令

内存泄漏检测方法:SOS

使用SOS检查内存中的对象很简单。

命令 1:查找未被引用的对象

!DumpObject

命令 2:检测是否持有某个对象的引用

!GCRoot

命令 3:检查某个对象是否位于内存中

!DumpHeap

检查内存泄露的时候,需要明确两件事情:

  1. 哪些对象正在使用内存
  2. 是什么创造并保持对这些对象的引用

DumpHeap

DumpHeap 有 2 个参数:

  • -stat:打开 debugger,并加载 SOS,查找所有的对象并根据类型分组展示,并根据内存总量降序排列
  • -type

你可能感兴趣的:(c#,开发语言)