我的 32 位应用程序可以做什么来消耗 GB 的物理 RAM?

What can my 32-bit app be doing that consumes gigabytes of physical RAM?

A co-worker 几个月前向我提到我们的一个内部 Delphi 应用程序似乎占用了 8 GB 的 RAM。我告诉他:

That's not possible

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

但他注意到 Windows Resource Monitor 表明系统上可用的 RAM 不足 1 GB。虽然我们的应用程序只使用了 220 MB 的虚拟内存:关闭它释放了 8 GB 的物理 RAM。

所以我测试了一下

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

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

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

  2. 并且我使用资源监视器检查应用程序使用的内存,以及使用的总内存:

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

  3. 然后关闭应用后内存占用:

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

  4. 我还使用 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会浪费时间立即清零所有内存,而不是简单地把它放在备用列表中,并在需要时清零(因为需要满足内存请求),这有点有趣).

None 这些东西解释了 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 位 non-Administrator 应用程序中可能发生什么导致 Windows 分配额外的 7 GB RAM?

只是那里,消耗 RAM。

Session 私人

关于Session私有内存的唯一信息来自a blog post announcing RAMMap:

Session Private: Memory that is private to a particular logged in session. This will be higher on RDS Session Host servers.

这是什么类型的应用程序?

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

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

QueueUserWorkItem(TakeScreenshotThreadProc);

和线程方法的pseudo-code:

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();
    }
}

很可能在您的应用程序的某处您正在分配系统资源而不是释放它们。任何创建对象和 returns 句柄的 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 对象,而不是大量的大 GDI 对象对象(例如,Delphi IDE 会定期创建 >3000 个 GDI 对象,这不一定是个问题)。

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

Program Project1;

{$APPTYPE CONSOLE}
uses
  SysUtils;

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

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

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

Handles and Objects : Object Categories -- MSDN

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

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