Delphi 7 中使用 WMI 的内存泄漏

发布于 2024-07-25 16:04:38 字数 2461 浏览 3 评论 0原文

我在使用 Delphi 7 中的 WMI 查询(远程)电脑时遇到内存泄漏。 内存泄漏仅发生在 Windows 2003(和 Windows XP 64)上。 Windows 2000 可以,Windows 2008 也可以。不知道有没有人遇到过类似的问题。

事实上,泄漏仅发生在某些版本的 Windows 中,这意味着这可能是 Windows 问题,但我一直在网上搜索,但无法找到解决该问题的修补程序。 另外,这可能是 Delphi 问题,因为在 C# 中具有类似功能的程序似乎没有这种泄漏。 后一个事实使我相信可能有另一种更好的方法来获取 Delphi 中我需要的信息而不会发生内存泄漏。

我已经包含了一个小程序的源代码以暴露下面的内存泄漏。 如果 { 下方的 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 代码是否已损坏,是否有更好的方法来获取我需要的信息?

您会注意到,在很多情况下,nil 被分配给对象,这与 Delphi 的精神相反...这些是 COM 对象,它们不继承自 TObject,并且没有我可以调用析构函数。 通过为它们分配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.

I'm experiencing a memory leak when using WMI from Delphi 7 to query a (remote) pc. The memory leak only occurs on Windows 2003 (and Windows XP 64). Windows 2000 is fine, and so is Windows 2008. I'm wondering if anyone has experienced a similar problem.

The fact that the leak only occurs in certain versions of Windows implies that it might be a Windows issue, but I've been searching the web and haven't been able to locate a hotfix to resolve the issue. Also, it might be a Delphi issue, since a program with similar functionality in C# doesn't seem to have this leak. The latter fact has led me to believe that there might be another, better, way to get the information I need in Delphi without getting a memory leak.

I've included the source to a small program to expose the memory leak below. If the line sObject.Path_ below the { Leak! } comment is executed, the memory leak occurs. If I comment it out, there's no leak. (Obviously, in the "real" program, I do something useful with the result of the sObject.Path_ method call :).)

With a little quick 'n dirty Windows Task Manager profiling on my machine, I found the following:

                       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

I guess my question is: has anyone else encountered this problem? If so, is it indeed a Windows issue, and is there a hotfix? Or (more likely) is my Delphi code broken, and is there a better way to get the information I need?

You'll notice on several occasions, nil is assigned to objects, contrary to the Delphi spirit... These are COM objects that do not inherit from TObject, and have no destructor I can call. By assigning nil to them, Windows's garbage collector cleans them up.

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

贪了杯 2024-08-01 16:04:38

我可以重现该行为,该代码在 Windows XP 64 上会泄漏内存,但在 Windows XP 上不会泄漏内存。 有趣的是,只有读取 Path_ 属性时才会发生这种情况,使用相同的代码读取 Properties_Security_ 不会泄漏任何内存。 WMI 中特定于 Windows 版本的问题看起来是最可能的原因。 据我所知,我的系统是最新的,因此可能也没有针对此问题的修补程序。

不过,我想对您重置所有变体和接口变量发表评论。 你写

您会注意到,在某些情况下,nil 被分配给对象,这与 Delphi 的精神背道而驰……这些是 COM 对象,它们不是从 TObject 继承的,并且没有我可以调用的析构函数。 通过将 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)并且销毁本身应该确实发生在此刻。 后者的示例是可以释放大块内存,或者需要关闭文件或释放同步对象。

I can reproduce the behaviour, the code leaks memory on Windows XP 64 and does not on Windows XP. Interestingly this occurs only if the Path_ property is read, reading Properties_ or Security_ with the same code does not leak any memory. A Windows-version-specific problem in WMI looks like the most probable cause of this. My system is up-to-date AFAIK, so there probably isn't a hotfix for this either.

I'd like to comment on your resetting all variant and interface variables, though. You write

You'll notice on several occasions, nil is assigned to objects, contrary to the Delphi spirit... These are COM objects that do not inherit from TObject, and have no destructor I can call. By assigning nil to them, Windows's garbage collector cleans them up.

This is not true, and consequently there is no need to set the variables to nil and Unassigned. Windows does not have a garbage collector, what you are dealing with are reference-counted objects, which are immediately destroyed once the reference count reaches 0. The Delphi compiler does insert the necessary calls to increment and decrement the reference count as necessary. Your assignments to nil and Unassigned decrement the reference count, and free the object when it reaches 0.

A new assignment to a variable, or the exiting of the procedure take care of this as well, so additional assignments are (albeit not wrong) superfluous and decrease the clarity of the code. The following code is completely equivalent and does not leak any additional memory:

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;

I'd say one should explicitly reset interfaces only if this does actually free the object (so the current ref count has to be 1) and the destruction itself should really happen exactly at this point. Examples for the latter are that a large chunk of memory can be freed, or that a file needs to be closed or a synchronization object to be released.

洒一地阳光 2024-08-01 16:04:38

您应该将 的返回值存储

sObject.Path_;

在变量中并将其设为 SWbemObjectPath。 这对于正确进行引用计数是必要的。

you should store the return value of

sObject.Path_;

in a variable and make it SWbemObjectPath. This is necessary to make the reference counting right.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文