使用Delphi 7中的WMI存在内存泄漏问题

5
使用Delphi 7从(远程)计算机查询WMI时,我遇到了内存泄漏问题。内存泄漏仅发生在Windows 2003(和Windows XP 64位)上。Windows 2000没问题,Windows 2008也没问题。我想知道是否有人遇到过类似的问题。泄漏问题只发生在某些版本的Windows中,这表明可能是Windows的问题,但我在网上搜索并没有找到解决此问题的补丁。也可能是Delphi的问题,因为C#中具有类似功能的程序似乎没有这个泄漏问题。后一个事实使我相信,在Delphi中获取所需信息的另一个更好的方法而不会出现内存泄漏。
我在下面提供了暴露内存泄漏的小程序源代码。如果执行位于{ Leak! }注释下面的sObject.Path_ 行,则会发生内存泄漏。如果我将其注释掉,就不会有泄漏。(显然,在“真正”的程序中,我会对sObject.Path_ 方法调用的结果做一些有用的事情:)。
通过我的快速粗略的Windows任务管理器配置文件,我发现以下内容:
                       Before  N=100  N=500  N=1000
With sObject.Path_     3.7M    7.9M   18.2M  31.2M
Without sObject.Path_  3.7M    5.3M    5.4M   5.3M
我想问的是:还有其他人遇到过这个问题吗?如果是,它确实是Windows的问题,并且是否有热修复程序?还是(更可能)我的Delphi代码有问题,并且是否有更好的方法来获取所需信息?
您会注意到,在多个场合下,与Delphi精神相反,将nil 赋值给对象。这些是不继承自TObject 并且没有我可以调用的析构函数的COM对象。通过将nil 分配给它们,Windows的垃圾收集器会清理它们。
program ConsoleMemoryLeak;

{$APPTYPE CONSOLE}

uses
  Variants, ActiveX, WbemScripting_TLB;

const
  N = 100;
  WMIQuery = 'SELECT * FROM Win32_Process';
  Host = 'localhost';

  { Must be empty when scanning localhost }
  Username = '';
  Password = '';

procedure ProcessObjectSet(WMIObjectSet: ISWbemObjectSet);
var
  Enum: IEnumVariant;
  tempObj: OleVariant;
  Value: Cardinal;
  sObject: ISWbemObject;
begin
  Enum := (wmiObjectSet._NewEnum) as IEnumVariant;
  while (Enum.Next(1, tempObj, Value) = S_OK) do
  begin
    sObject := IUnknown(tempObj) as SWBemObject;

    { Leak! }
    sObject.Path_;

    sObject := nil;
    tempObj := Unassigned;
  end;
  Enum := nil;
end;

function ExecuteQuery: ISWbemObjectSet;
var
  Locator: ISWbemLocator;
  Services: ISWbemServices;
begin
  Locator := CoSWbemLocator.Create;
  Services := Locator.ConnectServer(Host, 'root\CIMV2',
                  Username, Password, '', '', 0, nil);
  Result := Services.ExecQuery(WMIQuery, 'WQL',
                  wbemFlagReturnImmediately and wbemFlagForwardOnly, nil);
  Services := nil;
  Locator := nil;
end;

procedure DoQuery;
var
  ObjectSet: ISWbemObjectSet;
begin
  CoInitialize(nil);
  ObjectSet := ExecuteQuery;
  ProcessObjectSet(ObjectSet);
  ObjectSet := nil;
  CoUninitialize;
end;

var
  i: Integer;
begin
  WriteLn('Press Enter to start');
  ReadLn;
  for i := 1 to N do
    DoQuery;
  WriteLn('Press Enter to end');
  ReadLn;
end.
2个回答

7
我可以重现这个问题,代码在Windows XP 64位系统上泄露内存,但在Windows XP上没有。有趣的是,只有当读取Path_属性时才会发生这种情况,使用相同代码读取Properties_Security_不会泄漏任何内存。WMI中特定于Windows版本的问题似乎是造成这种情况最可能的原因。我认为我的系统是最新的,所以可能没有此问题的热修复程序。
我想评论一下您重置所有变量和接口变量的做法。您写道:

您会注意到,在几个场合中,对象被赋值为nil,与Delphi精神相反...这些是不继承自TObject的COM对象,并且没有我可以调用的析构函数。通过将它们赋值为nil,Windows的垃圾回收器会清理它们。

这是不正确的,因此没有必要将变量设置为nilUnassigned。Windows没有垃圾回收器,您正在处理引用计数的对象,一旦引用计数达到0,它们就会立即销毁。Delphi编译器会根据需要插入必要的调用来增加和减少引用计数。将变量赋值为nilUnassigned将减少引用计数,并在达到0时释放对象。
新的变量赋值或过程退出也会处理这个问题,因此额外的变量赋值虽然不是错误的,但是是多余的,并且会降低代码的清晰度。以下代码完全等效,不会泄漏任何额外的内存:
procedure ProcessObjectSet(WMIObjectSet: ISWbemObjectSet);
var
  Enum: IEnumVariant;
  tempObj: OleVariant;
  Value: Cardinal;
  sObject: ISWbemObject;
begin
  Enum := (wmiObjectSet._NewEnum) as IEnumVariant;
  while (Enum.Next(1, tempObj, Value) = S_OK) do
  begin
    sObject := IUnknown(tempObj) as SWBemObject;
    { Leak! }
    sObject.Path_;
  end;
end;

我认为只有在这确实释放了对象(因此当前引用计数必须为1),并且销毁本身确实应该发生在这一点上时,才应明确重置接口。后者的例子是可以释放大块内存,或者需要关闭文件或释放同步对象。


你可能是对的,但显式地重置变量确实解决了另一个内存泄漏问题。也许我有点过头了,重置了绝对所有东西,但是嘿,内存泄漏还没有全部消失: )。不过感谢您复现错误并报告它! - jqno

0

你应该存储返回值

sObject.Path_;

将其存储在一个变量中,并将其命名为SWbemObjectPath。这是为了确保引用计数正确而必需的。

谢谢你的回答!不幸的是,它没有起作用。我声明了一个 var Path: SWbemObjectPath; 并将 sObject.Path_ 的返回值赋给它。无论我是否将 Path 变量设置为 nil,内存占用都保持不变。 - jqno
不是真的,引用计数的管理并不需要分配给一个变量,它可以在没有变量的情况下正常工作。除非编译器无论如何都会优化它,否则这只会增加另一对 _AddRef() 和 _Release()。 - mghie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接