Утечка памяти с использованием WMI в Delphi 7

У меня возникает утечка памяти при использовании WMI из Delphi 7 для запроса (удаленного) компьютера. Утечка памяти происходит только в Windows 2003 (и Windows XP 64). Windows 2000 в порядке, как и Windows 2008. Мне интересно, сталкивался ли кто-нибудь с подобной проблемой.

Тот факт, что утечка происходит только в определенных версиях Windows, означает, что это может быть проблема Windows, но я искал в Интернете и не смог найти исправление для решения этой проблемы. Кроме того, это может быть проблема Delphi, поскольку программа с аналогичной функциональностью на C #, похоже, не имеет этой утечки. Последний факт заставил меня поверить, что может быть другой, лучший способ получить нужную мне информацию в Delphi без утечки памяти.

Я включил исходный код в небольшую программу, чтобы выявить утечку памяти ниже. Если выполняется строка sObject.Path_ под комментарием { Leak! }, возникает утечка памяти. Если я это прокомментирую, утечки не будет. (Очевидно, что в "настоящей" программе я делаю что-то полезное с результатом вызова метода 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.

person jqno    schedule 25.06.2009    source источник


Ответы (2)


Я могу воспроизвести поведение, код приводит к утечке памяти в Windows XP 64 и не в Windows XP. Интересно, что это происходит только в том случае, если считывается свойство Path_, чтение Properties_ или Security_ с тем же кодом не приводит к утечке памяти. Проблема, связанная с версией Windows в WMI, выглядит наиболее вероятной причиной этого. Моя система обновлена, насколько мне известно, поэтому для нее, вероятно, также нет исправления.

Однако я хотел бы прокомментировать ваш сброс всех переменных варианта и интерфейса. Ты пишешь

Вы заметите, что в нескольких случаях объектам присваивается nil, что противоречит духу Delphi... Это COM-объекты, которые не наследуются от TObject и не имеют деструктора, который я мог бы вызвать. Присваивая им nil, сборщик мусора Windows очищает их.

Это неверно, и, следовательно, нет необходимости устанавливать переменные в nil и Unassigned. В Windows нет сборщика мусора, вы имеете дело с объектами с подсчетом ссылок, которые немедленно уничтожаются, как только счетчик ссылок достигает 0. Компилятор Delphi вставляет необходимые вызовы для увеличения и уменьшения счетчика ссылок по мере необходимости. Ваши назначения nil и Unassigned уменьшают счетчик ссылок и освобождают объект, когда он достигает 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), а само уничтожение действительно должно произойти именно в этот момент. Примеры последнего: можно освободить большой кусок памяти, закрыть файл или освободить объект синхронизации.

person mghie    schedule 25.06.2009
comment
Вероятно, вы правы, но явный сброс переменных решил еще одну утечку памяти. Возможно, я немного переборщил, сбросив абсолютно все, но эй, утечки памяти еще не ушли :). Спасибо, что воспроизвели ошибку и сообщили о ней! - person jqno; 02.07.2009

вы должны сохранить возвращаемое значение

sObject.Path_;

в переменной и сделать ее SWbemObjectPath. Это необходимо для правильного подсчета ссылок.

person J-16 SDiZ    schedule 25.06.2009
comment
Спасибо за Ваш ответ! К сожалению, это не сработало. Я объявил var Path: SWbemObjectPath; и присвоил ему возвращаемое значение sObject.Path_. Объем памяти остается прежним, независимо от того, обнуляю ли я переменную Path или нет. - person jqno; 25.06.2009
comment
Неправда, управление счетчиком ссылок не требует присваивания переменной, оно работает и без него. Если компилятор все равно не оптимизирует его, это просто добавит еще одну пару _AddRef() и _Release(). - person mghie; 25.06.2009