【问题标题】:What can my 32-bit app be doing that consumes gigabytes of physical RAM?我的 32 位应用程序在做什么会消耗千兆字节的物理 RAM?
【发布时间】:2015-11-28 04:58:29
【问题描述】:

几个月前,一位同事向我提到,我们的一个内部 Delphi 应用程序似乎占用了 8 GB 的 RAM。我告诉他:

这是不可能的

32 位应用程序只有一个 32 位虚拟地址空间。即使发生内存泄漏,它最多可以消耗 2 GB 的内存。之后分配将失败(因为虚拟地址空间中没有空白空间)。并且在内存泄漏的情况下,虚拟页面将被换出到页面文件,从而释放物理 RAM。

但他指出,Windows 资源监视器表明系统上的可用 RAM 不足 1 GB。虽然我们的应用只使用了 220 MB 的虚拟内存:关闭它会释放 8 GB 的物理 RAM。

所以我测试了它

我让应用程序运行了几个星期,今天我终于决定测试它。

首先我在关闭应用程序之前查看内存使用情况:

  • 工作集 (RAM) 为 241 MB
  • 使用的总虚拟内存:409 MB

我使用资源监视器来检查应用程序使用的内存,以及正在使用的总 RAM:

  • 应用程序分配的虚拟内存:252 MB
  • 正在使用的物理内存:14 GB

然后是关闭应用后的内存使用情况:

  • 正在使用的物理内存:6.6 GB (7.4 GB 以下)

我还使用 Process Explorer 查看了之前和之后的物理 RAM 使用情况。唯一的区别是 8 GB 的 RAM 真的 未提交,现在是免费的:

| Item                          | Before     | After     |
|-------------------------------|------------|-----------|
| Commit Charge (K)             | 15,516,388 | 7,264,420 |
| Physical Memory Available (K) |  1,959,480 | 9,990,012 |
| Zeroed Paging List  (K)       |    539,212 | 8,556,340 |

注意:有点有趣的是,Windows 会浪费时间立即将所有内存清零,而不是简单地将其放在备用列表中,然后根据需要将其清零(因为需要满足内存请求)。

这些都不能解释 RAM 在做什么(你在做什么就坐在那里!你包含什么!?)

那段记忆里有什么!

该 RAM 必须包含一些有用的东西;它必须有一些目的。为此,我求助于 SysInternals 的 RAMMap。它可以分解内存分配。

RAMMap 提供的唯一线索是 8 GB 物理内存与称为 Session Private 的东西相关联。这些 Session Private 分配不与任何进程(即不是我的进程)相关联:

| Item                   | Before   | After    |
|------------------------|----------|----------|
| Session Private        | 8,031 MB |   276 MB |
| Unused                 | 1,111 MB | 8,342 MB | 

当然不会对 EMS、XMS、AWE 等做任何事情。

在 32 位非管理员应用程序中可能会发生什么导致 Windows 分配额外的 7 GB RAM?

  • 这不是换出项目的缓存
  • 它不是 SuperFetch 缓存

就在那里;消耗内存。

私人会话

关于“Session Private”内存的唯一信息来自a blog post announcing RAMMap

Session Private:专用于特定登录会话的内存。这在 RDS 会话主机服务器上会更高。

这是什么类型的应用

这是一个 32 位本机 Windows 应用程序(即不是 Java,不是 .NET)。因为它是一个原生的 Windows 应用程序,所以它当然会大量使用 Windows API。

需要注意的是,我并没有要求人们调试应用程序;我希望那里的 Windows 开发人员会知道为什么 Windows 可能会保存我从未分配过的内存。话虽如此,最近(在过去 2 或 3 年中)唯一可能导致这种情况发生变化的是每 5 分钟截取一次屏幕截图并将其保存到用户的%LocalAppData% 文件夹的功能。计时器每五分钟触发一次:

QueueUserWorkItem(TakeScreenshotThreadProc);

以及线程方法的伪代码:

void TakeScreenshotThreadProc(Pointer data)
{
   String szFolder = GetFolderPath(CSIDL_LOCAL_APPDTA);
   ForceDirectoryExists(szFolder);

   String szFile = szFolder + "\" + FormatDateTime('yyyyMMdd"_"hhnnss', Now()) + ".jpg";

   Image destImage = new Image();
   try
   {
      CaptureDesktop(destImage);

      JPEGImage jpg = new JPEGImage();
      jpg.CopyFrom(destImage); 
      jpg.CompressionQuality = 13;
      jpg.Compress();

      HANDLE hFile = CreateFile(szFile, GENERIC_WRITE, 
            FILE_SHARE_READ | FILE_SHARE_WRITE, null, CREATE_ALWAYS,
            FILE_ATTRIBUTE_ARCHIVE | FILE_ATTRIBUTE_ENCRYPTED, 0);
      //error checking elucidated
      try
      {
          Stream stm = new HandleStream(hFile);
          try
          {
             jpg.SaveToStream(stm);
          }
          finally
          {
             stm.Free();
          }
       }
       finally
       {
          CloseHandle(hFile);
       }
    }
    finally
    {
       destImage.Free();
    }
}

【问题讨论】:

  • 顺便说一句,人们通常应该很高兴发现您的 RAM 被使用了。把所有的 RAM 塞进去让它闲置有什么意义?
  • 来自 Technet:每个用户都有自己的会话空间映射到虚拟内存中。会话空间分为四个不同的区域: 会话结构——内存管理控制结构,包括会话工作集列表。会话映像空间——保存 Win32k.sys 修改数据的私有副本、Win32k.sys 代码和未修改数据的单个副本以及各种会话驱动程序。会话视图空间——会话映射视图,包括桌面堆会话分页池——用于此会话的分页池内存
  • 这可能是由于大量创建和未释放的windows资源(句柄)造成的。我们在一个项目中遇到了这样的问题。
  • 非常简短的示例:在控制台应用程序中:for i := 1 to 1000000 do FindFirst('c:\*', faAnyFile, sr); 在此循环之后:应用程序 133M,总计 3GB,关闭应用程序:总计 1.5 GB
  • 虽然我从不相信伪代码是真实事物的准确表示,但在调试时,你似乎忘记释放你的 JPEGImage...

标签: windows delphi winapi virtual-memory delphi-5


【解决方案1】:

您很可能在应用程序的某个地方分配系统资源而不是释放它们。任何创建对象并返回句柄的 WinApi 调用都可能是可疑的。例如(请小心在内存有限的系统上运行它 - 如果您没有 6GB 可用空间,它会出现糟糕的分页):

Program Project1;

{$APPTYPE CONSOLE}
uses
  Windows;

var
  b : Array[0..3000000] of byte;
  i : integer;    
begin
  for i := 1 to 2000 do 
    CreateBitmap(1000, 1000, 3, 8, @b);
  ReadLn;
end.

由于分配了随后未释放的位图对象,这会消耗 6GB 的会话内存。应用程序内存消耗仍然很低,因为对象不是在应用程序的堆上创建的。

但是,如果不了解您的应用程序的更多信息,就很难更具体。以上是演示您正在观察的行为的一种方法。除此之外,我认为您需要调试。

在这种情况下,分配了大量的 GDI 对象 - 但是,这并不一定是指示性的,因为在应用程序中通常分配了大量的小 GDI 对象,而不是大量的大对象 (例如,Delphi IDE 会定期创建超过 3000 个 GDI 对象,这不一定是个问题。

在@Abelisto 的示例中(在 cmets 中),相比之下:

Program Project1;

{$APPTYPE CONSOLE}
uses
  SysUtils;

var
  i : integer;
  sr : TSearchRec;
begin
  for i := 1 to 1000000 do FindFirst('c:\*', faAnyFile, sr);
  ReadLn;
end.

这里返回的句柄不是 GDI 对象,而是搜索句柄(属于内核对象的一般类别)。在这里我们可以看到有大量的句柄被进程使用。同样,进程内存消耗很低,但使用的会话内存却大幅增加。

同样,这些对象可能是用户对象——这些对象是通过调用 CreateWindowCreateCursor 或通过使用 SetWindowsHookEx 设置挂钩来创建的。有关创建对象并返回每种类型的句柄的 WinAPI 调用列表,请参阅:

Handles and Objects : Object Categories -- MSDN

这可以帮助您通过将问题缩小到可能导致问题的呼叫类型来开始追踪问题。如果您使用任何第三方组件,它也可能位于有问题的第三方组件中。

像 AQTime 这样的工具可以分析 Windows 分配,但我不确定是否有支持 Delphi5 的版本。可能有其他分配分析器可以帮助追踪这一点。

【讨论】:

  • 这几乎可以肯定是答案 - 泄漏句柄。它解释了为什么不是我的进程分配了内存,并且它符合我认为可能的原因。现在回头看看我的问题 - 特别是我在 Process Explorer 下的应用程序的屏幕截图,我可以看到它已经分配了 2,386 GDI 句柄。既然我知道要寻找什么,应该很容易找到它。所以问题的答案是:我的应用程序如何消耗 8 GB 的 RAM:handles.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-01-31
  • 1970-01-01
  • 2020-05-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-01
相关资源
最近更新 更多