7

まず第一に、あなたの助けに前もって感謝します。

このようなフォーラムで助けを求めることにしました。数か月間懸命に働いた後、問題の解決策を見つけることができなかったからです。

これは次のように説明できます。'GCが強制的に起動された場合でも、VB.netで作成されたオブジェクトが破棄されたときにGCによって解放されないのはなぜですか。「」

次のコードを検討してください。明らかに私のプロジェクトははるかに複雑ですが、問題を切り分けることができました。

Imports System.Data.Odbc
Imports System.Threading
Module Module1
    Sub Main()
        'Declarations-------------------------------------------------      
            Dim connex As OdbcConnection 'Connection to the DB
            Dim db_Str As String         'ODBC connection String      
        'Sentences----------------------------------------------------
            db_Str = "My ODBC connection String to my MySQL database"
            While True
                'Condition: Infinite loop.
                connex = New OdbcConnection(db_Str)
                connex.Open()
                connex.Close()

                'Release created objects
                connex.Dispose()

                'Force the GC to be launched
                GC.Collect()

                'Send the application to sleep half a second
                System.Threading.Thread.Sleep(500)
            End While
    End Sub
End Module

これは、MySQLデータベースに接続するマルチスレッドアプリケーションをシミュレートします。ご覧のとおり、接続は新しいオブジェクトとして作成されてから解放されます。最後に、GCの起動を余儀なくされました。私はこのアルゴリズムをいくつかのフォーラムで見ましたが、MSDNオンラインヘルプでも見ました。私に関する限り、私は何も悪いことをしていません。

問題は、アプリケーションが起動されたときに始まります。作成されたオブジェクトはコード内に配置されますが、しばらくすると、使用可能なメモリが使い果たされ、アプリケーションがクラッシュします。

もちろん、この小さなバージョンではこの問題を確認するのは困難ですが、実際のプロジェクトでは、アプリケーションは非常に迅速にメモリを使い果たし(時間の経過とともに確立された接続の量が原因で)、その結果、稼働時間はわずか2日です。 。次に、アプリケーションを再起動する必要があります。

自分のマシンにメモリプロファイラーをインストールしました(Scitech .Netメモリプロファイラー4.5、ダウンロード可能な試用版 はこちら)。「メモリリークの調査」というセクションがあります。「リアルタイム」タブでこれを見たとき、私は絶対に驚いた。私が正しければ、このグラフィックは、コードで作成されたオブジェクトが実際にリリースされていないことを示しています。

http://www.zuzsso.com/images/screenshot3.jpg

この他の画面を見たときの驚きはさらに大きかった。これによると、破棄されていないオブジェクトはすべてSystem.Transactionsタイプであり、コード上にこのタイプのオブジェクトを作成していないため、.Netライブラリ内で内部的に管理されていると思います。VB.net標準ライブラリにバグがあるということですか?:

http://www.zuzsso.com/images/screenshot4.jpg

私のコードでは、クエリを実行していないことに注意してください。そうした場合、 .Close()メソッドを呼び出しても、 ODBCDataReaderオブジェクトも解放されません(驚くべきことに、このタイプの解放されていないオブジェクトの数は、System.Transactions型の解放されていないオブジェクトとまったく同じです) 。

もう1つの重要なことは、ステートメントGC.Collect()です。これは、メモリプロファイラーが表示する情報を更新するために使用します。コードから削除すると、プロファイラーはリアルタイムダイアグラムを適切に更新せず、すべてが正しいという誤った印象を与えます。

最後に、connex.Open()ステートメントを省略すると、スクリーンショット#1はフラットラインをレンダリングします(つまり、作成されたすべてのオブジェクトが正常に解放されたことを意味します)が、残念ながら、データベースに対してクエリを実行できません。接続が開かれていません。

誰かがこれに対する論理的な説明と、オブジェクトを効果的に解放するための回避策を見つけることができますか?

みなさん、ありがとうございました。

ニコ

4

2 に答える 2

5

Dispose has nothing to do with garbage collection. Garbage collection is exclusively about managed resources (memory). Dispose has no bearing on memory at all, and is only relevant for unmanaged resources (database connections, file handles, gdi resource, sockets... anything not memory). The only relationship between the two has to do with how an object is finalized, because many objects are often implemented such that disposing them will suppress finalization and finalizing them will call .Dispose(). Explicitly Disposing() an object will never cause it to be collected1.

Explicitly calling the garbage collector is almost always a bad idea. .Net uses a generational garbage collector, and so the main effect of calling it yourself is that you'll hold onto memory longer, because by forcing the collection earlier you're likely to check the items before they are eligible for collection at all, which sends them into a higher-order generation that is collected less often. These items otherwise would have stayed in the lower generation and been eligible for collection when the GC next ran on it's own. You may need to use GC.Collect() now for the profiler, but you should try to remove it for your production code.

You mention your app runs for two days before crashing, and are not profiling (or showing results for) your actual production code, so I also think the profiler is in part misleading you here. You've pared down the code to something that produced a memory leak, but I'm not sure it's the memory leak you are seeing in production. This is partly because of the difference in time to reproduce the error, but it's also "instinct". I mention that because some of what I'm going to suggest might not make sense immediately in light of your profiler results. That out of the way, I don't know for sure what is going on with your lost memory, but I can make a few guesses.

The first guess is that your real code has try/catch block. An exception is thrown... perhaps not on every connection, but sometimes. When that happens, the catch block allows your program to keep running, but you skipped over the connex.Dispose() line, and therefore leave open connections hanging around. These connections will eventually create a denial of service situation for the database, which can manifest itself in a number of ways. The correction here is to make sure you always use a finally block for anything you .Dispose(). This is true whether or not you currently have a try/catch block, and it's important enough that I would say the code you've posted so far is fundamentally wrong: you need a try/finally. There is a shortcut for this, via a using block.

The next guess is that some of your real commands end up fairly large, possibly with large strings or image (byte[]) data involved. In this case, items end up on a special garbage collector generation called the Large Object Heap (LOH). The LOH is rarely collected, and almost never compacted. Think of compaction as analogous to what happens when you defrag a hard drive. If you have items going to the LOH, you can end up in a situation where the physical memory itself is freed (collected), but the address space within your process (you are normally limited to 2GB) is not freed (compacted). You have holes in your memory address space that will not be reclaimed. The physical RAM is available to your system for other processes, but over time this still results in the same kind of OutOfMemory exception you're seeing. Most of the time this doesn't matter: most .Net programs are short-lived user-facing apps, or ASP.Net apps where the entire thread can be torn down after a page is served. Since you're building something like a service that should run for days, you have to be more careful. The fix may involve significantly re-working some code, to avoid creating the large objects at all. That may mean re-using a single or small set of byte arrays over and over, or using streaming techniques instead of string concatenation or string builders for very large sql queries or sql query data. It may also mean you find this easier to do as a scheduled task that runs daily and shuts itself down at the end of the day, or a program that is invoked on demand.

A final guess is that something you are doing results in your connection objects still being in some way reachable by your program. Event handlers are a common source of mistakes of this sort, though I would find it strange to have event handlers on your connections, especially as this is not part of your example.

1 I suppose I could contrive a scenario that would make this happen. A simple way would be to build an object assumes a global collection for all objects of that type... the objects add themselves to the collection at construction and remove themselves at disposal. In this way, the object could not be collected before disposal, because before that point it would still be reachable... but that would be a very flawed program design.

于 2012-12-03T02:34:43.050 に答える
1

皆さん、非常に役立つ回答をありがとうございました。

ジョエル、その通りです。このコードは、実際のプロジェクトで発生した「リーク」の問題と必ずしも同じではない「リーク」を生成しますが、同じ症状を再現します。つまり、リリースされていないオブジェクトの数が増え続けます (最終的にはメモリを使い果たします)。 ) 上記のコードで。すべてが適切にコーディングされているように見えるので、何が問題なのだろうか。それらが廃棄/収集されない理由がわかりません。しかし、プロファイラーによると、それらはまだメモリ内にあり、最終的には新しいオブジェクトを作成できなくなります。

私の「本当の」プロジェクトについてのあなたの推測の 1 つは、的を射ていました。'catch' ブロックがオブジェクトの破棄を要求していないことに気付きました。これは修正されました。貴重なご提案ありがとうございます。ただし、上記の例のコードに「using」句を実装しましたが、実際には問題を解決していません。

ハンス、あなたも正しいです。質問を投稿した後、上記のコードのライブラリを変更して MySQL に接続しました。

古いライブラリ (例):

System.Data.Odbc

新しいライブラリ:

System.Data
Microsoft.Data.Odbc

新しいものを使用すると、プロファイラーはコードをさらに変更することなく、平らな線をレンダリングしました。これは私が探していたものです。したがって、私の結論はあなたのものと同じです。つまり、古いものには内部エラーがあり、それが発生する可能性があり、それが彼らを本当の「トラブルメーカー」にしています。

ここで、最初はプロジェクトで新しいもの ( System.DataおよびMicrosoft.Data.Odbc )を使用していたことを思い出しましたが、すぐに古いもの ( System.Data.Odbc ) に変更しました。) 新しいものでは複数のアクティブなレコードセット (MARS) を開くことができないためです。私のアプリケーションは MySQL データベースに対して大量のクエリを実行しますが、残念ながら接続数が制限されています。そのため、最初は実際のコードを少数の接続のみを作成するように実装しましたが、それらはコード全体で共有されていました (関数間の接続をパラメーターとして渡します)。(たとえば) レコードセット (クライアントとしましょう) を取得し、同時に多くのチェックを行う必要があったため (たとえば、クライアントには少なくとも 1 つの請求書がある、クライアントには重複した電子メールアドレスがあるなど)、これは素晴らしいことでした。 、これには多くのサイド クエリが含まれます)。「古い」ライブラリでは、同じ接続で複数のコマンドを作成し、異なるクエリを実行できました。

「新しい」ライブラリは MARS を許可しません。セッション/接続ごとに作成できるコマンド (つまり、クエリの実行) は 1 つだけです。別のレコードセットを実行する必要がある場合は、前のレコードセットを閉じて (反復処理を行っているため実際には不可能です)、新しいクエリを作成する必要があります。

両方の問題のバランスを見つける必要がありました。そのため、メモリの問題のために「新しいライブラリ」を使用することになり、接続を共有しないようにアプリケーションを再コーディングし(必要に応じて各プロシージャが新しいライブラリを作成するように)、アプリケーションが接続できる接続の数を減らしました同時に実行して、接続プールを使い果たしないようにします。

このソリューションは、アプリケーションに偽のロジックを導入するため、理想にはほど遠いですが (理想的なケースのシナリオは、SQL サーバーに移行することです)、少なくとも初期段階では、より良い結果が得られ、アプリケーションはより安定しています。新しいバージョン。

あなたの提案に再び感謝します。鉱山も役立つことを願っています。

乾杯。

ニコ

于 2012-12-09T14:15:59.910 に答える