我的32位应用程序可以做些什么来消耗千兆字节的物理RAM?

Ian*_*oyd 17 windows delphi winapi delphi-5 virtual-memory

几个月前,一位同事提到我,我们的一个内部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 |
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

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

这些都没有解释RAM正在做什么(你坐在那里什么!你包含什么!?)

那段记忆里有什么!

RAM必须包含一些有用的东西 ; 它必须有一些目的.为此,我转向SysInternals的RAMMap.它可以破坏内存分配.

RAMMap提供的唯一线索是8 GB的物理内存与Session Private相关联.这些会话专用分配与任何进程都没有关联(即不是我的进程):

| Item                   | Before   | After    |
|------------------------|----------|----------|
| Session Private        | 8,031 MB |   276 MB |
| Unused                 | 1,111 MB | 8,342 MB | 
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

当然没有对EMS,XMS,AWE等做任何事情.

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

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

在那里 ; 消耗RAM.

会话私密

关于"Session Private"内存的唯一信息来自宣布RAMMap的博客文章:

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

这是什么类型的应用程序

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

应该指出的是,我并没有要求人们调试应用程序; 我希望Windows开发人员知道为什么Windows可能会保留我从未分配过的内存.话虽如此,最近(最近2年或3年)唯一可能导致此类事情发生的变化是每隔5分钟拍摄一次屏幕截图并将其保存到用户%LocalAppData%文件夹中的功能.计时器每五分钟触发一次:

QueueUserWorkItem(TakeScreenshotThreadProc);
Run Code Online (Sandbox Code Playgroud)

和线程方法的伪代码:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

J..*_*... 24

很可能在您的应用程序的某个地方,您正在分配系统资源而不是释放它们.任何创建对象并返回句柄的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.
Run Code Online (Sandbox Code Playgroud)

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

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

在这种情况下,分配了大量的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.
Run Code Online (Sandbox Code Playgroud)

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

在此输入图像描述

同样,对象可能是用户对象-这些都是通过调用之类的东西产生CreateWindow,CreateCursor或通过设置挂钩SetWindowsHookEx.有关创建对象和返回每种类型句柄的WinAPI调用的列表,请参阅:

句柄和对象:对象类别 - MSDN

这可以帮助您通过将问题范围缩小到可能导致问题的调用类型来开始追踪问题.它可能也在一个有缺陷的第三方组件中,如果您使用任何组件.

像AQTime这样的工具可以分析Windows分配,但我不确定是否有支持Delphi5的版本.可能有其他分配分析器可以帮助跟踪此情况.

  • 这几乎肯定是答案——手柄泄漏。它解释了为什么不是我的进程分配了内存,并且它符合我认为可能的原因。现在回顾我的问题 - 特别是 Process Explorer 下我的应用程序的屏幕截图,我可以看到它已经分配了 **2,386** GDI 句柄。既然我知道要寻找什么,应该很容易找到它。所以问题的答案是:我的应用程序如何消耗 8 GB RAM:**句柄**。 (2认同)