乐趣区

关于.net:记一次-NET-某妇产医院-WPF内存溢出分析

一:背景

1. 讲故事

上个月有位敌人通过博客园的短消息找到我,说他的程序存在内存溢出状况,寻求如何解决。

要解决还得通过 windbg 剖析啦。

二:Windbg 剖析

1. 为什么会内存溢出

大家都晓得内存溢出对应着 .NET 中的 OutOfMemoryException 异样,这种异样有可能是托管代码手工抛出的,也有可能是 CLR 层面抛出的,话中有话就是能够通过两种形式排查。

  • 托管线程是否挂载着异样?

0:000> !t
ThreadCount:      23
UnstartedThread:  0
BackgroundThread: 5
PendingThread:    0
DeadThread:       17
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 362c 00fac868     26020 Preemptive  7ED701A0:00000000 00fa6b60 0     STA 
   5    2 2d70 00fbeba0     2b220 Preemptive  7EBA7AC0:00000000 00fa6b60 0     MTA (Finalizer) 
   7    3 3264 061c8890   102a220 Preemptive  00000000:00000000 00fa6b60 0     MTA (Threadpool Worker) 
  17   15 3f98 19682b90   202b220 Preemptive  7EBB0830:00000000 00fa6b60 0     MTA 
XXXX   16    0 2845fb00     35820 Preemptive  00000000:00000000 00fa6b60 0     Ukn 
  18   14  a7c 2842b1c8   202b220 Preemptive  00000000:00000000 00fa6b60 0     MTA 
XXXX    6    0 2c9b3778   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX   18    0 288a1318   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX   23    0 288a22f0   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX   10    0 2ccf3550   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX   21    0 288a1860   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX   12    0 288a1da8   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX   11    0 2c993640   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX    8    0 2ccf3a98     35820 Preemptive  00000000:00000000 00fa6b60 0     Ukn 
XXXX    9    0 2ccf2030   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX    7    0 2c9aed88   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX   26    0 28898308   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX   25    0 2c492c68   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX    4    0 2c993b88   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX   20    0 2c9af2d0   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX   17    0 2c9afd60   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
XXXX   24    0 2c9b1280   1039820 Preemptive  00000000:00000000 00fa6b60 0     Ukn (Threadpool Worker) 
  23   22 2658 2c9b02a8   1029220 Preemptive  7ED5BFF8:00000000 00fa6b60 0     MTA (Threadpool Worker) 

从输入信息看,这些线程并没有挂载任何托管异样,我去。。。

  • 是否在 CLR 上抛出

这次要是看 托管堆 (heap) 上的内存调配或者 gc 回收造成的内存不足,能够用 !ao 命令。


0:000> !ao
There was no managed OOM due to allocations on the GC heap

从输入信息看也没有任何异样,难堪了😂😂😂。。。尼玛,那到底是因为什么呢?

2. 摸索溢出起因

呈现这种难堪状况,我只能狐疑生成这个 dump 的时候并没有 get 到那个点,或者是我的常识边界无限,不过天无绝人之路,不在那个 也必定在那个 左近,对吧,接下来用 !address -summary 看一下内存应用的归类信息。


0:000> !address -summary

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
<unknown>                              1520          4c185000 (1.189 GB)  65.57%   59.45%
Image                                  4306          1f140000 (497.250 MB)  26.78%   24.28%
Free                                   1133           bf17000 (191.090 MB)            9.33%
Heap                                    617           7626000 (118.148 MB)   6.36%    5.77%
Stack                                    72           1740000 (23.250 MB)   1.25%    1.14%
Other                                    34             7b000 (492.000 kB)   0.03%    0.02%
TEB                                      24             30000 (192.000 kB)   0.01%    0.01%
PEB                                       1              3000 (12.000 kB)   0.00%    0.00%

--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_MAPPED                              549          34b60000 (843.375 MB)  45.42%   41.18%
MEM_PRIVATE                            1718          20424000 (516.141 MB)  27.80%   25.20%
MEM_IMAGE                              4307          1f155000 (497.332 MB)  26.78%   24.28%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_COMMIT                             4904          66ddd000 (1.607 GB)  88.64%   80.37%
MEM_RESERVE                            1670           d2fc000 (210.984 MB)  11.36%   10.30%
MEM_FREE                               1133           bf17000 (191.090 MB)            9.33%

--- Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy %ofTotal
PAGE_READONLY                          2272          382cf000 (898.809 MB)  48.41%   43.89%
PAGE_READWRITE                         1572          1eead000 (494.676 MB)  26.64%   24.15%
PAGE_EXECUTE_READ                       218           dd59000 (221.348 MB)  11.92%   10.81%
PAGE_WRITECOPY                          449           133e000 (19.242 MB)   1.04%    0.94%
PAGE_EXECUTE_READWRITE                  188            ab4000 (10.703 MB)   0.58%    0.52%
PAGE_NOACCESS                           156             9c000 (624.000 kB)   0.03%    0.03%
PAGE_READWRITE | PAGE_GUARD              48             78000 (480.000 kB)   0.03%    0.02%
PAGE_READWRITE | PAGE_WRITECOMBINE        1              2000 (8.000 kB)   0.00%    0.00%

--- Largest Region by Usage ----------- Base Address -------- Region Size ----------
<unknown>                                   1d200000           a001000 (160.004 MB)
Image                                        fed1000           36e4000 (54.891 MB)
Free                                        33dfe000           1082000 (16.508 MB)
Heap                                        3da84000            a1b000 (10.105 MB)
Stack                                        1a10000             fd000 (1012.000 kB)
Other                                       7fa40000             33000 (204.000 kB)
TEB                                           a4c000              3000 (12.000 kB)
PEB                                           a3d000              3000 (12.000 kB)

从下面的 MEM_COMMIT=1.607 GB 80.37% 信息看,以后内存占用 1.6G,占比 80.37%,能够看出它受到了一个 2G 内存 的限度,而且从 !t 输入中的内存地址看,以后是 32bit 程序,所以这是一个经典的: 64 零碎跑着 32 位程序被 2G 内存限度 的问题。

3. 如何冲破 2G 限度

要寻找答案,还得看最权威的 MSDN: https://docs.microsoft.com/en…

破局 还得设置程序的 IMAGE_FILE_LARGE_ADDRESS_AWARE 标记。

对于具体怎么设置,我找了三种办法。

  • 应用 LargeAddressAware 安装包

参见 github:https://github.com/KirillOsen…

  • 应用 editbin

能够在 vs 的生成事件中输出 editbin /largeaddressaware $(TargetPath)

  • 应用代码形式

这种能够间接给生成好的 exe 减少 LargeAddressAware 标记,除了标记,还能检测,🐂👃


using System;
using System.IO;

namespace PEFile
{
    public class LargeAddressAware
    {public static bool IsLargeAddressAware(string filePath)
        {
            bool isLargeAddressAware = false;
            PrepareStream(filePath, (stream, binaryReader) => isLargeAddressAware = (binaryReader.ReadInt16() & 0x20) != 0);
            return isLargeAddressAware;
        }

        public static void SetLargeAddressAware(string filePath)
        {PrepareStream(filePath, (stream, binaryReader) =>
            {var value = binaryReader.ReadInt16();
                if ((value & 0x20) == 0)
                {value = (short)(value | 0x20);
                    stream.Position -= 2;
                    var binaryWriter = new BinaryWriter(stream);
                    binaryWriter.Write(value);
                    binaryWriter.Flush();}
            });
        }

        private static void PrepareStream(string filePath, Action<Stream, BinaryReader> action)
        {using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
            {if (stream.Length < 0x3C)
                {return;}

                var binaryReader = new BinaryReader(stream);

                // MZ header
                if (binaryReader.ReadInt16() != 0x5A4D)
                {return;}

                stream.Position = 0x3C;
                var peHeaderLocation = binaryReader.ReadInt32();

                stream.Position = peHeaderLocation;

                // PE header
                if (binaryReader.ReadInt32() != 0x4550)
                {return;}

                stream.Position += 0x12;

                action(stream, binaryReader);
            }
        }
    }
}

更多方法参考:https://stackoverflow.com/que…

三:总结

总的来说,2G 内存限度 是一个 32bit 程序所必须面对的问题,晓得了就好解决了,最初有一个问题要解释下,为什么 commit 内存高达 1.6G,这是因为医疗类的软件,大多是 FastReport + DevExpress 这些重量级的经典搭配以及大量的图片资源占用了太多 native memory。

退出移动版