Delphi 7でWMIを使用したメモリリーク
-
20-08-2019 - |
質問
Delphi 7のWMIを使用して(リモート)PCを照会すると、メモリリークが発生します。メモリリークは、Windows 2003(およびWindows XP 64)でのみ発生します。 Windows 2000は問題なく、Windows 2008も同様です。同様の問題を経験した人がいるかどうかは疑問です。
特定のバージョンのWindowsでのみリークが発生するという事実は、Windowsの問題である可能性があることを意味しますが、Webを検索しており、問題を解決する修正プログラムを見つけることができませんでした。また、Delphiの問題である可能性があります。C#の同様の機能を備えたプログラムにはこのリークがないようです。後者の事実により、メモリリークを発生させずにDelphiで必要な情報を取得するための別の、より良い方法があると信じるようになりました。
ソースを小さなプログラムに含めて、以下のメモリリークを公開しました。 sObject.Path_
コメントの下の{ Leak! }
行が実行されると、メモリリークが発生します。コメントアウトすれば、漏れはありません。 (明らかに、<!> quot; real <!> quot;プログラムでは、nil
メソッド呼び出しの結果で何か役に立つことを行います:)。
マシン上で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の精神に反して、TObject
がオブジェクトに割り当てられます...これらは<=>を継承しないCOMオブジェクトであり、呼び出すことのできるデストラクタはありません。それらに<=>を割り当てることにより、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.
解決
動作を再現できますが、Windows XP 64ではコードがメモリをリークしますが、Windows XPではメモリがリークしません。興味深いことに、これはPath_
プロパティが読み取られ、同じコードでProperties_
またはSecurity_
を読み取ってもメモリがリークしない場合にのみ発生します。 WMIのWindowsバージョン固有の問題は、これの最も可能性の高い原因のように見えます。私のシステムは最新のAFAIKなので、おそらくこれに対する修正プログラムもありません。
ただし、すべてのバリアントおよびインターフェイス変数のリセットについてコメントしたいと思います。書きます
Delphiの精神に反して、オブジェクトにnilが割り当てられることがあります。これらは、TObjectを継承せず、呼び出すことのできるデストラクタがないCOMオブジェクトです。それらにnilを割り当てることにより、Windowsのガベージコレクターはそれらをクリーンアップします。
これは真実ではないため、変数をnil
およびUnassigned
に設定する必要はありません。 Windowsにはガベージコレクタがありません。処理しているのは参照カウントオブジェクトで、参照カウントが0に達するとすぐに破棄されます。Delphiコンパイラは、必要に応じて参照カウントを増減するために必要な呼び出しを挿入します。 <=>および<=>への割り当てにより、参照カウントが減分され、オブジェクトが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でなければなりません)。後者の例は、メモリの大きなチャンクを解放できること、ファイルを閉じる必要があること、または同期オブジェクトを解放する必要があることです。
他のヒント
の戻り値を保存する必要があります
sObject.Path_;
変数に入れて、SWbemObjectPathにします。これは、参照カウントを正しく行うために必要です。