VB.net DLLのメモリリークについて

解決


シロ  2012-08-28 18:41:09  No: 143362  IP: [192.*.*.*]

下記の通りVB.net2008で非同期でpingを行い、結果をpostmessageで送信するDLLを作成し、VB6で定期的に呼び出し、使用しています。
しかし、VB6側でDLLを呼ぶたびにメモリ使用量がどんどん増えていき困っています。
DLL内でなんらかの開放が抜けていると思うのですがアドバイスをお願いします。

以下VB6呼び出しソース(参照設定済み)
Private Sub Timer_NWCHK_Timer()
    

    Text3.Text = "" '応答内容を初期化
    On Error GoTo Err_Proc
    Dim obj As Netchk.Netchk ' (a)
    Dim ret As Long
    Set obj = New Netchk.Netchk ' (b)
    If NWCHKPATH <> "" Then
      ret = obj.ping(NWCHKPATH, Me.hWnd) ' (c)'DMZに対してping送信 レスポンスは後程DLLがSendMassageで非同期でtext3に入れてくれる
    End If
    Set obj = Nothing
    
Err_Proc:
End Sub    



↓以下VB.netソース

Option Strict On

Public Class Netchk



    Public Declare Function RegisterWindowMessage Lib "user32.dll" _
            Alias "RegisterWindowMessageA" (ByVal lpString As String) As Integer

    Declare Function PostMessageStr Lib "user32.dll" Alias "PostMessageA" _
       (ByVal hwnd As Integer, ByVal MSG As Integer, _
        ByVal wParam As Integer, ByVal lParam As Integer) As Integer


    'Pingオブジェクト
    Dim mainPing As System.Net.NetworkInformation.Ping = Nothing

    Public Val_hd As Integer
    Public WM_USER_PING As Integer



      Public ReadOnly Property Ping(ByVal add As String, ByVal hd As Integer) As Boolean

        Get
            '後にPostMessageで使用するメッセージ値はRegisterWindowMessageで取得しておく
            WM_USER_PING = RegisterWindowMessage("WM_USER_PING")
            Val_hd = hd

            'Pingオブジェクトの作成
            If mainPing Is Nothing Then
                mainPing = New System.Net.NetworkInformation.Ping()
                'イベントハンドラを追加
                AddHandler mainPing.PingCompleted, AddressOf Ping_PingCompleted
            End If
            'Pingのオプションを設定
            'TTLを64、フラグメンテーションを無効にする
            Dim opts As New System.Net.NetworkInformation.PingOptions(64, True)
            'Pingで送信する32バイトのデータを作成
            Dim bs As Byte() = System.Text.Encoding.ASCII.GetBytes(New String("A"c, 32))


            'タイムアウトを10秒
            mainPing.SendAsync(add, 10000, bs, opts, Nothing)

            opts = Nothing
            bs = Nothing
            mainPing = Nothing

            Return True

        End Get

    End Property


    Private Sub Ping_PingCompleted(ByVal sender As Object, _
            ByVal e As System.Net.NetworkInformation.PingCompletedEventArgs)
        Dim Ret As Integer

        If e.Cancelled Then

            Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 2)
        ElseIf Not (e.Error Is Nothing) Then
            Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 1)
        Else
            '結果を取得
            If e.Reply.Status = System.Net.NetworkInformation.IPStatus.Success Then

                Console.WriteLine("Reply from {0}:bytes={1} time={2}ms TTL={3}", _
                    e.Reply.Address, e.Reply.Buffer.Length, _
                    e.Reply.RoundtripTime, e.Reply.Options.Ttl)

                Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 7)

            Else
                Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 3)
            End If
        End If
    End Sub
End Class

編集 削除
魔界の仮面弁士  2012-08-29 19:04:57  No: 143363  IP: [192.*.*.*]

ActiveX DLL としての運用ですね。

VB6 連携との部分は抜きにして、まずは .NET 側のコードだけの判断ですが
Ping クラスを Dispose している箇所が無いのが気になりますね。
Ping クラスは IDisposable を実装しているはずなので…。

編集 削除
シロ  2012-08-30 08:26:33  No: 143364  IP: [192.*.*.*]

アドバイスありがとうございます
mainPing = Nothing
の一行前に
mainPing.dispose
を入れても変わらずでした
結果の内容によらず呼び出す度に80kくらいのメモリが食われます
 System.GC.Collct
もダメなようです

編集 削除
魔界の仮面弁士  2012-08-30 10:38:58  No: 143365  IP: [192.*.*.*]

VB6 側のコードは、
  Set obj = New Netchk.Netchk
  If NWCHKPATH <> "" Then
    ret = obj.ping(NWCHKPATH, Me.hWnd)
  End If
  Set obj = Nothing
ではなく、
  If NWCHKPATH <> "" Then
    Set obj = New Netchk.Netchk
    ret = obj.ping(NWCHKPATH, Me.hWnd)
    Set obj = Nothing
  End If
の方が良いのではないでしょうか。

また、コメントが「SendMassageで非同期でtext3に入れてくれる」とありますが、
実際に使われているのは PostMessage でしたよね。



> 呼び出す度に80kくらいのメモリが食われます
ある程度のところで打ち止めになったりはせず、80KB ずつ上限無しに消費されるのでしょうか。

.NET 側の GC の動作を見るのであれば、パフォーマンスモニタで、.NET CLR Memory の遷移を確認してみてください。
http://msdn.microsoft.com/ja-jp/magazine/dd882521.aspx
http://msdn.microsoft.com/ja-jp/library/dd297765.aspx

> mainPing.dispose
解決するかどうかは分かりませんが、.NET Framework 2.0〜3.5 では、
  DirectCast(mainPing, System.IDisposable).Dispose()
で呼び出してみてください。
(.NET 4 以降であれば mainPing.Dispose のままで OK)

編集 削除
シロ  2012-08-31 10:54:40  No: 143366  IP: [192.*.*.*]

回答ありがとうございます。

DirectCastはどこにいれるのが正解でしょうか?
Disposeの代わりに下記の通り追加してみました。
するとPing_PingCompleted関数はe.Cancelledとなり、キャンセルしたことになりPingが成功しなくなりました。
逆にPingCompletedの最後にDirectCastの処理を入れるとVB6側のソフトが落ちました。

            'タイムアウトを10秒
            mainPing.SendAsync(add, 10000, bs, opts, Nothing)
            DirectCast(mainPing, System.IDisposable).Dispose()
            mainPing = Nothing
            opts = Nothing
            bs = Nothing

元のソフトでは80KB ずつ上限無しに消費します。
1週間起動していたら落ちるまではいかないものの、大量のメモリを消費したままでした。

NET CLR Memory の遷移については時間をとって調べてみます
私のイメージではVB6側で
Nothingをした時点でメモリは開放されるものと思っていたのですがなかなかうまくいかないですね。

編集 削除
魔界の仮面弁士  2012-08-31 13:32:57  No: 143367  IP: [192.*.*.*]

> メモリは開放されるものと
開放は、自身が管理している資源を、他者にも利用できるよう開け放つ(free/open)こと。
解放は、自身が管理していた資源を解き放ち、自身の管理権を破棄する(dispose)こと。

学校の図書館を一般カイホウするのは前者、捕虜をカイホウするのは後者ですが、
メモリについてはどちらの意味でも使われるので、使い分けに注意が必要ですね。


閑話休題。

まず、シロさんの方で、問題個所の切り分けは出来ていますか? たとえば、
  (1) COM 化することなく、単に Ping.SendAsync を繰り返した場合
  (2) 何もしない DLL を作り、それをVB6から繰り返し呼び出した場合
  (3) Ping クラスを生成するだけで、SendAsnyc を呼ばなかった場合
  (4) WM_USER_PING メッセージ交換のみで Ping を実行しない DLL とした場合
などといった判定作業です。(他にも検討すべき個所はあるかもしれません)


先の私の回答は、上記(1)に対するものだけであり、それ以外の
要因には触れていません。もしも複合的な要因であった場合、
IDisposable.Dispose だけでは解決しない可能性があります。


さて、そもそもの Ping.SendAsnyc のメモリリーク問題については、
下記を参照してみてください。
http://blog.mbcharbonneau.com/2006/11/14/using-the-ping-class-in-net-20-without-memory-leaks/
http://blogs.msdn.com/b/joncole/archive/2005/12/15/debugging-a-memory-leak-in-managed-code_3a00_-ping-_2d00_-sendasync.aspx

.NET 2.0〜3.5 の Ping クラスは、Component クラスを継承しているのですが、
Dispose(Boolean)がオーバーライドされていないため、Ping.Dispose() を呼び出しても、
単にベースクラスの Component.Dispose() が呼ばれるだけとなり、Ping クラスの
内部リソースには影響を与えないという問題があります。そもため VB2008 から
意図的に「IDisposable の」Dispose を呼び出す必要があるということです。

なお、.NET 4 以降の Ping クラスは、Component.Dispose(Boolean) メソッドが
オーバーライドされているため、Ping.Dispose() メソッドの呼び出しだけで
問題ないと思います。個人的な予想であって、検証したわけでは無いですけれどね。


> DirectCastはどこにいれるのが正解でしょうか?
IDisposable.Dispose すべきは、オブジェクトの使用後です。
つまり、少なくとも Ping 完了後に実施されるようにしておいてください。

作成された DLL は イベントを公開していないようなので、
PingCompleted イベントを抜けた後のタイミングが適切では無いでしょうか。


> Disposeの代わりに下記の通り追加してみました。
SendAsnyc 処理中に破棄しては、流石にマズイでしょう。


> 逆にPingCompletedの最後にDirectCastの処理を入れるとVB6側のソフトが落ちました。
それは、実行時にトラップ可能なエラーでしょうか。また、どの行で落ちたのでしょうか。

現時点の情報だけでは、VB6 に何が起きたのかを私には想像できないのですが、とりあえず
  (案1) VB6 側が、受信完了後に Dispose 発行依頼のメソッドを呼び出す形に修正する。
  (案2) Timer 等を併用し、PingCompleted イベント完了後に Dispose する形にする。
などと修正してみるのは如何でしょうか。

その上で、DLL 側(Netchk クラス)を IDisposable パターンで
実装しておく必要もあるでしょう。理由については後述します。
http://msdn.microsoft.com/ja-jp/library/s9bwddyx%28v=vs.80%29.aspx


> Nothingをした時点でメモリは開放されるものと思っていたのですがなかなかうまくいかないですね。
そもそも、VB6 側での Nothing 代入は不要です。

今回のコードでは obj がローカル変数となっているため、何もせずとも
変数がスコープ外になった時点で、自動的に参照カウントが減じられます。
また、Nothing 代入も End Sub の直前にあるのみなので、早期解放の
意図ともなっておらず、この時点での Nothing 代入自体にはあまり意味がありません。


それはさておき、VB6 側参照カウントがゼロになった時点で、
ActiveX オブジェクトの終了処理は完了します。ただし、
ここで破棄されるのは COM が使用しているリソースだけです。

もちろんそこから付随して、COM 呼び出し可能ラッパー(CCW)によって、
.NET 側のマネージ オブジェクトの参照も自動的に解放されます。

ただしそれは、あくまでも「マネージオブジェクト」に対してのみであり、
Ping クラスのようにアンマネージなオブジェクトの場合、解放処理は
開発者自身が手動で破棄せねばなりません。


とはいえ多くのマネージオブジェクトは、プログラマが
Dispose 等の終了処理を呼び出し忘れていたとしても、GC 回収時に
終了処理が自動的に施され、アンマネージリソース等(今回の場合で言えば、
IcmpCreateFile API のハンドルなど)のリークが起きないよう
設計されています。(たとえば IcmpCloseHandle 等を呼び出すなど)

ところが .NET 3.5 以下では、先述した理由によって、Ping.Dispose が
実質機能していないため、IDisposable.Dispose の明示的実行が
必要となります。
しかも、Component が正しく実装されていませんし、かといって
Ping クラスに Finalize が明示実装されているわけでもありません。
そのため、GC 回収時にアンマネージリソースの後始末が自動的に
実施されることも無さそうなので、Ping を呼び出す自作クラス自体も、
IDisposable インターフェイスを実装するなどの対策が必要でしょう。

編集 削除
シロ  2012-08-31 19:13:21  No: 143368  IP: [192.*.*.*]

何度もご指導ありがとうございます。


>(1) COM 化することなく、単に Ping.SendAsync を繰り返した場合
DLLではなく.netアプリとして行うということでしょうか?
それであればメモリリークするようです
>(2) 何もしない DLL を作り、それをVB6から繰り返し呼び出した場合
メモリリークはありませんでした。
>(3) Ping クラスを生成するだけで、SendAsnyc を呼ばなかった場合
メモリリークはありませんでした。
>(4) WM_USER_PING メッセージ交換のみで Ping を実行しない DLL とした場合
メモリリークはありませんでした。




>作成された DLL は イベントを公開していないようなので、
これの意味がわかりませんでした。
公開とはどのようなことですか?
なにか公開するとメリットがあったり・公開すると別の対策や方法が生まれてくるという方法があるのでしょうか?


>PingCompleted イベントを抜けた後のタイミング
ということなのでDLL側は下記の通りしてみました。

****当初のソース内Ping関数一部抜粋*****
      <★前半省略★>
          'Pingで送信する32バイトのデータを作成
            Dim bs As Byte() = System.Text.Encoding.ASCII.GetBytes(New String("A"c, 32))
            'タイムアウトを10秒
            mainPing.SendAsync(add, 10000, bs, opts, Nothing)    
            opts = Nothing
            bs = Nothing

            Return True
        End Get

    End Property


    Private Sub Ping_PingCompleted(ByVal sender As Object, _
            ByVal e As System.Net.NetworkInformation.PingCompletedEventArgs)
        Dim Ret As Integer

        If e.Cancelled Then
            Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 2)
        ElseIf Not (e.Error Is Nothing) Then
            Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 1)
        Else
            '結果を取得
            If e.Reply.Status = System.Net.NetworkInformation.IPStatus.Success Then
                Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 7)
            Else
                Ret = PostMessageStr(Val_hd, WM_USER_PING, 1, 3)
            End If
        End If
        DirectCast(mainPing, System.IDisposable).Dispose()  ←★追加しました
    End Sub


VB6側をもっとシンプルに
Private Sub Timer1_Timer()
   On Error GoTo Err_Proc
   Dim obj As Netchk.Netchk 
   Dim ret As Long
   Set obj = New Netchk.Netchk 
   ret = obj.ping("192.168.0.1", Me.hWnd)
Err_Proc:
End Sub
のみのシンプルなフォームを作り試験をしてみました。(postmassege受信処理は作っていません)

VB6側、DLL側いかがでしょうか?


DirectCastのおかげか80kのメモリ浪費はなくなりました。落ちることもないのでその件はまた別の問題にようでした。
いまこのシンプルな試験フォームではタイマーイベント毎に8k増加しつづけているようです。
これが普通のことなのかどうかもわかりませんし時間が経過していないためどこかで上限以上消費しないのか無尽蔵に消費するのかはまだわかりません。
あとはVB2010にすると解消する等のことも考えられますか?

編集 削除
シロ  2012-08-31 19:44:24  No: 143369  IP: [192.*.*.*]

>  (案1) VB6 側が、受信完了後に Dispose 発行依頼のメソッドを呼び出す形に修正する。
こちらの方法ですと万が一受信できないことがあるたびにDisposeができないと思うのですが大丈夫なのでしょうか?

>  (案2) Timer 等を併用し、PingCompleted イベント完了後に Dispose する形にする。
別のタイマー等でDisposeを呼び出す場合、今回のコードでは obj がローカル変数となっているため別の関数でまたDisposeするためだけにset obj〜としてもよいものなのでしょうか?

編集 削除
魔界の仮面弁士  2012-08-31 23:31:40  No: 143370  IP: [192.*.*.*]

> DLLではなく.netアプリとして行うということでしょうか?
> それであればメモリリークするようです
問題箇所は Ping.SendAsnyc であるという仮定で間違い無さそうですね。

> 公開とはどのようなことですか?
VB6 側で WithEvents して使うタイプの DLL ではない、ということです。
今回はイベント通知ではなく、PostMessage で通知しているのですよね。


> DirectCastのおかげか
DirectCast は単に型変換(正確にはキャスト)を行うだけのものなので
その言い方は語弊がありますが、IDisposable.Dispose の呼び出しが
必須であるという点は間違いないかと。

> 万が一受信できないことがあるたびにDisposeができないと思うのですが大丈夫なのでしょうか?
大丈夫ではないので、Netchk クラス自体を Finalize で対処して
備えておく必要があります。いわゆる IDisposable パターンの実装ですね。

> 今回のコードでは obj がローカル変数となっているため
そのコードだと、VB6 側のオブジェクト寿命管理に問題がありませんか?

それと 案2 の Timer 案において、タイマーを用意するのは DLL 側です。
VB6 の Timer コントロールのことではありません(説明不足でしたね…)。

PingCompleted 内で IDisposable.Dispose してはまずいのであれば、
PingCompleted 後に Dispose するために、System.Threading.Timer 等で
遅延処理させるというのが案2です。ただし PingCompleted 内で解放しても
先のエラー問題が再現しないのなら、その必要は無さそうですけれども。

> 8k増加しつづけているようです。
その実装(VB6 側で Timer を使っているサンプル)は、
そもそも別の問題があるので除外して…。

当初のコードにおいて、IDisposable.Dispose 以外の解放処理を
必要とするとなれば、あと思いつくのは、PingCompleted 完了時に 
RemoveHandler を呼び出してイベントを解除することと、
フィールド変数である mainPing に Nothing を代入して、
GC の回収対象にされやすくすることぐらいですかね。

ただ、これらは通常気にする必要は無いと思うので、
IDisposable.Dispose のような明確な効果は得られないかもしれません。


> あとはVB2010にすると解消する等のことも考えられますか?
気にするべきは VB のバージョンではなく .NET Framework のバージョンです。

既に何度か書いたように、「.NET 4 や .NET 4.5 版の Ping クラス」では
Dispose が適切に実装されているため、IDisposable.Dispose であろうと
Ping.Dispose であろうと、どちらの呼び方でも問題ないと思われます。

また、万一 Dispose の呼び出しを忘れても Finalize によって
GC 回収時に自動破棄されることが期待されます。

なので、VB2012 や VB2010 を使えば、問題点を解消できる可能性は
ありますが、これらのバージョンでも、ターゲットフレームワークに
.NET 2.0〜3.5 を採用した場合はやはり IDisposable.Dispose を
明示的に呼び出すという、同様の対策が要求されることになるでしょう。

編集 削除
シロ  2012-09-07 09:14:02  No: 143371  IP: [192.*.*.*]

いろいろ試行錯誤の結果VB2010(.NET4)を使用し、 
Ping_PingCompleted関数内で
MainPing.Dispose()
を行うことで全てうまくいきました。
.net3.5以前にはそのような症状があるとは知りませんでした。
大変助かりました。

ありがとうございました

編集 削除