我的 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。
所以我测试了一下
我让应用运行几个星期了,今天我终于决定测试一下。
首先我在关闭应用程序之前查看内存使用情况,使用 Process Explorer:
- 工作集 (RAM) 是:241 MB
- 使用的总虚拟内存:409 MB
并且我使用资源监视器检查应用程序使用的内存,以及使用的总内存:
- 应用程序分配的虚拟内存: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会浪费时间立即清零所有内存,而不是简单地把它放在备用列表中,并在需要时清零(因为需要满足内存请求),这有点有趣).
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?
- 这不是换出项目的缓存
- 它不是 SuperFetch 缓存
只是那里,消耗 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 对象的句柄,而是搜索句柄(属于内核对象的一般类别)。这里我们可以看到进程使用了大量的句柄。同样,进程内存消耗很低,但使用的会话内存大幅增加。
同样,对象可能是用户对象 - 这些对象是通过调用 CreateWindow
、CreateCursor
或通过 SetWindowsHookEx
设置挂钩创建的。有关创建对象和每种类型的 return 句柄的 WinAPI 调用列表,请参阅:
Handles and Objects : Object Categories -- MSDN
这可以帮助您将问题缩小到可能导致问题的呼叫类型,从而开始追踪问题。如果您使用的话,它也可能位于有问题的第三方组件中。
AQTime 之类的工具可以分析 Windows 分配,但我不确定是否有支持 Delphi5 的版本。可能还有其他分配分析器可以帮助追踪这一点。
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。
所以我测试了一下
我让应用运行几个星期了,今天我终于决定测试一下。
首先我在关闭应用程序之前查看内存使用情况,使用 Process Explorer:
- 工作集 (RAM) 是:241 MB
- 使用的总虚拟内存:409 MB
并且我使用资源监视器检查应用程序使用的内存,以及使用的总内存:
- 应用程序分配的虚拟内存: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会浪费时间立即清零所有内存,而不是简单地把它放在备用列表中,并在需要时清零(因为需要满足内存请求),这有点有趣).
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?
- 这不是换出项目的缓存
- 它不是 SuperFetch 缓存
只是那里,消耗 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 对象的句柄,而是搜索句柄(属于内核对象的一般类别)。这里我们可以看到进程使用了大量的句柄。同样,进程内存消耗很低,但使用的会话内存大幅增加。
同样,对象可能是用户对象 - 这些对象是通过调用 CreateWindow
、CreateCursor
或通过 SetWindowsHookEx
设置挂钩创建的。有关创建对象和每种类型的 return 句柄的 WinAPI 调用列表,请参阅:
Handles and Objects : Object Categories -- MSDN
这可以帮助您将问题缩小到可能导致问题的呼叫类型,从而开始追踪问题。如果您使用的话,它也可能位于有问题的第三方组件中。
AQTime 之类的工具可以分析 Windows 分配,但我不确定是否有支持 Delphi5 的版本。可能还有其他分配分析器可以帮助追踪这一点。