DOBON.NET DOBON.NETプログラミング掲示板過去ログ

メモリ使用量が解放されない

環境/言語:[XP、VB.NET、.NET Framework1.1]
分類:[.NET]

はじめまして。maskと申します。

現在、VB.Netでの開発を行っておりまして、1つ解決できない問題がありますので、
ご存知の方がいらっしゃいましたら、どうかご助言お願いいたします。

内容としては、ある処理をループしている個所があり、数千件単位でループさせると、
タスクマネージャのメモリ使用量がどんどん増加していくということです。
このループ内で行っている処理として、大きく分けて、@配列が解放されない Anew
したオブジェクトが解放されない の2点が考えられるかと思っております。

そこで上記のことから、テスト的にちょっとしたテストプログラムも作成してみました。
これは、単純に数千件〜数万件のループ処理で、配列のRedim Preserveを行い、メモリ
領域を増やしながら、値も適当に設定していく単純なものです。
もちろん、最後の処理として配列の解放(Erase)も行っています。
また、exeにする際に、リリースビルドでコンパイルもしております。
このプログラムでもやはり同様のことが発生します。

私の考えでは、解放(Erase)のタイミングで(多少の時間誤差はあると思うが)、
タスクマネージャのメモリ使用量は、ある程度解放されるものと認識していたのですが、
数分待っても一向に解放される気配がなく、ずっと数百メガの使用量を維持し続けます。
Eraseではなく、配列 = Nothing でも同じ現象でした。

VB6.0のときには、上記方法で解放されていたと思うのですが、.Netになってからは、
何か仕様的に変わってしまったのでしょうか?
それとも、.Netでの配列等の解放についての私の認識が間違っているのでしょうか?

上記内容に不明点等ある場合は、再度補足説明もしたいと思いますので、この件について
何かご存知の方は、どうか書き込みをお願いいたします!!
■No17329に返信(maskさんの記事)
はじめまして エツです。おはようございます。

内容に興味があります。
宜しければテストプログラムを教えて戴けませんでしょうか?

> そこで上記のことから、テスト的にちょっとしたテストプログラムも作成してみました。

メモリ使用量がどんどん増加して最終的にどうなるのですか?
メモリ不足となり、アプリケーションが落ちるのですか。それとも落ちないが処理速度がどんどん低下するのですか。
■No17329に返信(maskさんの記事)
> 私の考えでは、解放(Erase)のタイミングで(多少の時間誤差はあると思うが)、
> タスクマネージャのメモリ使用量は、ある程度解放されるものと認識していたのですが、
> 数分待っても一向に解放される気配がなく、ずっと数百メガの使用量を維持し続けます。
> Eraseではなく、配列 = Nothing でも同じ現象でした。

ためしに、GC.Collect メソッドで強制的にガベージ コレクトしても変化なしでしょうか?

> VB6.0のときには、上記方法で解放されていたと思うのですが、.Netになってからは、
> 何か仕様的に変わってしまったのでしょうか?

これに関していえば、参照カウント法からガベージ コレクションへと変わっています。
これをご存知だからこそ、"数分待った" のではないのでしょうか?

> それとも、.Netでの配列等の解放についての私の認識が間違っているのでしょうか?

解放という概念を持ち込む必要がないという意味では、誤りがあるのかもしれません。
2006/08/29(Tue) 19:19:04 編集(投稿者)

画像ファイルのダウンロードで
ストリームからbyteの配列に読み込むプログラムでした。
maskさんの状況と一緒でメモリリークしました。
どんどんメモリ使用率が増えて、パンクしてしまいました。

C#のコードで申し訳ないのですが・・・
http://dobon.net/vb/dotnet/internet/webrequestsavefile.html
を参考に作ったものです。

byte[] bytes = new byte[2048];
int bufferSize = 2048;
while (bufferSize == 2048)
{
int n = strm.Read(bytes, 0, bufferSize);
if (n==0)
break;
fs.Write(bytes, 0, n);
}
bytes = new byte[]{}; <---------
で解決しました。

代替方法としては、
問題が発生している部分を
別のexeにして実行すればexeが終了すれば
メモリを開放してくれます。
こんばんは、エツさん。maskです。

今日は都合が悪くて今レスを確認しているところです。
ちなみに、サンプルソースについては、明日の夜にならないと
アップできません。現在の環境にはVBすら入っていないので・・・。

申し訳ありませんが、明日までお待ちいただけますか?
なるべく早めにアップしますので!

以上です。
>代替方法としては、
>問題が発生している部分を
>別のexeにして実行すればexeが終了すれば
>メモリを開放してくれます。

GCの存在を真っ向否定するような実装する意味有るのでしょうか?
プロセスを分けるということのデメリットがどれだけあるか・・・・も一緒に提示しないと。

>私の考えでは、解放(Erase)のタイミングで(多少の時間誤差はあると思うが)、
>タスクマネージャのメモリ使用量は、ある程度解放されるものと認識していたのですが、

タスクマネージャのメモリ使用量はあくまで目安であって、正確なメモリ使用量ではありません。
実は正確なメモリ使用量ってなによ?っていう根源的な問題が有ることを認識する必要があります。

http://d.hatena.ne.jp/NyaRuRu/20060730/p1

とりあえず開発者としては余計な参照を残さないで済むようにする。(スコープを適切に)
じたばたしない。
どうしてもGCを発生させたいときにはGC.Collectただし、GC.Collectすると、消えない分のメモリは余計に消えにくくデメリットもある事を把握してから使う。(要は使う必要はほとんどない)

私のプログラムでGC.Collectを発生させたことは1度もありません。

がんばってください。
こんばんは、じゃんぬねっとさん。maskです。

まず、私は「ガベージ コレクション」という言葉と、ある程度の意味は認識しておりました。
ただ、これはVB側である一定時間で自動的に行われるものと誤認しておりました。

なので、「GC.Collect メソッド」については、恥ずかしながら今回初めて知ることになりました。
まずは、このメソッドについて、もう少し調べ、サンプルプログラムでいろいろテストしてみたいと思います。

貴重な情報、ありがとうございます!また、結果を報告しますね!

以上です。
こんばんは、ROYさん。maskです。

私のプログラムでは、まだメモリリークまでには到っていませんが、タスクマネージャ
でのメモリ使用量の視覚的な増加に、先方からのクレームが来ている状況です。

事例として、ROYさんのようなケースもあるのだなと、改めてなんとか解決したいと
思っております。

exeを別途用意するのも1つの方法ではあるかとは思いますが、今回は同一exe内での
解決を目指していきたいと考えております。

情報提供、ありがとうございました!

以上です。
こんばんは、中博俊さん。maskです。

レスありがとうございます。

参考URLも、まさに今現在問題になっている事象そのものだと思います。
やはり、『余計な参照を残さないで済むようにする。(スコープを適切に)』を再度確認
すると共に、「GC.Collect」についても調べてみようと思っております。

また、できましたら1つお願いがあるのですが、明日、サンプルプログラムをアップしようと
思っているのですが、それについての中博俊さんとしての意見やコーディング方法も一例
として書き込みしていただければありがたいと思っております。
(「GC.Collect」を発生させずにメモリ使用量を少なく維持する方法
 ⇒少なくとも現時点で私は「GC.Collect」を使用したいと考えております)

無理を言ってしまい、大変申し訳ありませんが、引き続き本件について、アドバイスいただきたいと思います。

以上です。
こんばんは、maskです。
昨日は書き込みができなかったので、レスが大変遅れてしまいました。
申し訳ありません。

さて、以下にサンプルプログラムを記載しましたので、ご覧ください。
--------------------------------------------------------------------------------------------------------------------------
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles
Button1.Click

Dim i As Integer
Dim sAryTest() As String

For i = 0 To 30000
ReDim Preserve sAryTest(i)
sAryTest(i) = "test"
Next

Erase sAryTest

End Sub
--------------------------------------------------------------------------------------------------------------------------

このように、配列にひたすら値を設定していくような単純なものです。
ここで、ループ中にメモリを消費していくのはわかるのですが、昨日も申しました通り、
Erase sAryTest
で、メモリ使用量がある程度まで削減されるものと思っておりました。

また、GC.Collect() も追加してテストしてみましたが、結果は同じでメモリ使用量が
タスクマネージャ上でほとんど減少することはありませんでした。

とりあえず、ソースと現在までに試した内容をアップさせていただきます。
この後も引き続き、調査とテストを繰り返してみるつもりです。

それでは宜しくお願いいたします。
以上です。
私も試してみました。環境は、.NET 1.1です。
ループ中、メモリ使用量が増え続けることはなく、30MBくらいになると数MB減るのを何度か繰り返しました。
デバッグ、リリース、どちらでも同様でした。

ところで、.NETにおいて「Erase sAryTest」は「sAryTest = Nothing」と同じ結果になるので、この場所では不要ですね。

ループ中にもRedimの度に新しい領域が確保されて、それまでsAryTestが参照していた領域はどこからも参照されなくなるので、随時GCが働くはず(実際、私の試したときはそのようになったと思われる)で、動作の違いがどこから出てきたのか、一見不思議ですが、これは、それぞれの環境でGCが必要になるタイミングが違うだけではないかと思います。そう考えると、「GCが必要ないから動作していないだけなので全く問題ない」ということになりそうですが、いかがでしょう。
見かけ上ではなく実際のメモリ使用量については問題がなくとも、処理速度の面で問題はないでしょうか。テストプログラムでは1件のデータ追加ごとにReDim Preserveを行っているので、1000件ごとに表示させてみると、件数が多くなるに伴いかなりの速度低下が認められました。

これをほぼ解消すると同時に見かけ上のメモリ消費を抑えるには、ある程度の件数分(仮に1万件としても、配列の大きさは40KB ←間違っていたらご指摘下さい)をまとめて確保して、件数が超えたところで次の1万件分を足してRedimする方法があると思います。

ただし、件数が非常に多くなるとそれでも不十分と思われますので、もっとよさそうな方法として、配列の配列をつくって、Redimは親の配列に対してだけ行い、それと同時に(1万件分、などの)子の配列を確保して親の配列に参照させる、というのもあります。これを外部からはひとつの配列のように扱えるメソッドを作ってやれば便利かと思います。
2006/09/01(Fri) 13:52:41 編集(投稿者)

■No17402に返信(千八巧者さんの記事)

こんにちは エツです。
私の環境はxpで.NET 1.0です。(VS2002)
サンプルを試したところ千八巧者さんと同じ結果でした。
1.使用メモリの増加はほとんどありませんでした。
2.件数が多くなる(For ループ内のiが大きくなる)と処理速度の低下がみられました。千八巧者さんが指摘されているように ReDim Preserve に時間が掛かっていると思われます。

実行中のiの値を表示させるよう次のように書き換えて確認しました。

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim i As Integer
Dim sAryTest() As String

For i = 0 To 300000 '30000ではすぐ終了するので大きくした
ReDim Preserve sAryTest(i)
sAryTest(i) = "test"

If (i Mod 2000) = 0 Then '2000行毎に i を表示
Label1.Text = i.ToString("i = #,#00")
Application.DoEvents() '表示更新と応答なし状態の回避
End If

Next

Erase sAryTest
MessageBox.Show("動作テスト終了")

End Sub
2006/09/02(Sat) 09:33:44 編集(投稿者)

次のようなクラスを作ってみました。

Class Test
    Private Const ARRAY_SIZE As Integer = 10000
    Private m_x()() As String
    Private m_xCount As Integer
    Private arrayCount As Integer
    Private lastIndex As Integer
    ReadOnly Property XCount() As Integer
        Get
            Return m_xCount
        End Get
    End Property
    Public Sub New()
        ReDim m_x(0)
        m_x(0) = New String(ARRAY_SIZE - 1) {}
        arrayCount = 1
        lastIndex = -1
    End Sub
    Public Sub AddX(ByVal x As String)
        If lastIndex < ARRAY_SIZE - 1 Then
            lastIndex += 1
        Else
            ReDim Preserve m_x(arrayCount)
            m_x(arrayCount) = New String(ARRAY_SIZE - 1) {}
            arrayCount += 1
            lastIndex = 0
        End If
        m_x(arrayCount - 1)(lastIndex) = x
        m_xCount += 1
    End Sub
    Public Function X(ByVal i As Integer) As String
        Dim arrayNumber As Integer = i \ ARRAY_SIZE
        Dim index As Integer = i Mod ARRAY_SIZE
        Return m_x(arrayNumber)(index)
    End Function
End Class

これを用い、以下のように1024^2(=1M)回ループさせたところ、所要時間は2秒弱、
メモリ増加量は38MBでしたので、データ1個(すべて7桁の文字列)あたり38バイトの
増加となります。

Dim t As New Test
For i = 1 To 1024 * 1024
    t.AddX((i + 1000000).ToString)
Next

また、maskさんのプログラムのようにt.AddX("test")としてみると、一瞬で終了し、
メモリの増加量は4.6MBでした。
この場合、文字列の実体は1個しか確保されないので、配列に格納される4バイトの
参照値×件数の分しかメモリは消費されないはずですが、その予想に一致した結果と
なっています。

私はコレクションの知識もほとんどない初級アマチュアですのでこの程度しか思い
つきませんが、ご参考になれば幸いです

----------
追記です。やはりもっと簡単な方法がありましたね。No17412をご覧下さい。m(_ _)m
こんばんは、千八巧者さん、エツさん。maskです。
今回も回答が遅れてしまい、申し訳ありませんでした。

私の環境で、エツさんのソースをそのままコピーして実行してみました。
そのときの結果は、
1.使用メモリは、処理実行前:約11M、処理実行後:約24M となりました。
2.私の環境でも件数が多くなる(For ループ内のiが大きくなる)と処理速度の低下がみられました。

私の環境下では、1.に対する原因は依然不明のままですが、配列領域の確保の
仕方については、もう少し検討が必要ということになりますね。
今までは、"ReDim Preserve"を多様しておりましたが、レスポンスに関わるような
処理を行うときは、今回の事象を教訓に、工夫した配列確保を行いたいと思いました。

また、1.についての今のところの見解としては、千八巧者さんのおっしゃるように
「GCが必要ないから動作していないだけなので全く問題ない」ということも想定し
始めているところです。
また、本件における最後の手段としては、今回問題視しているユーザさんが、
どうしてもメモリ使用量が気になると言っておられるので、"GC.Collect"を意図的に
記述する方法も考え始めております。

以前に、中博俊さんにレスしていただいたように、『消えない分のメモリは余計に
消えにくい』というデメリットもあるようですが、このループ処理(実行ファイル)
が実施されるのは、年に数回というもので、且つ、これまでも実際にメモリ使用量が
増えることで障害が発生しているわけでもありませんので、多少のデメリットも承知の
上で、ユーザさんの要求に応えるのも1つの手段ではないかと考えています。

また、これから #17406 についても試させていただきます。
千八巧者さん、エツさん、いつも回答が遅れているにも関わらず、レスありがとう
ございます。非常にありがたく思います。

以上です。
こんばんは、千八巧者さん。maskです。

No17412 での実行結果をご報告します。
以下のように、実行ファイルのループ処理を何度か連続的に実行してみました。
約20M〜約40M前後でメモリ使用量が増減していることがわかりました。
その後も何回も続けてみましたが、以下のようなサイクルで増減され続けます。

また、処理速度も非常に早く一瞬で完了してしまいますね。
----------------------------------------------------------------------------
処理実行前 :約11M
処理実行後1:約19.5M
処理実行後2:約22M
処理実行後3:約28M
処理実行後4:約19.5M
処理実行後5:約18M
処理実行後6:約26M
処理実行後7:約34M
処理実行後8:約42M
処理実行後9:約19M
----------------------------------------------------------------------------
これ程速度アップができるとは思いもよりませんでした。
ArrayListクラスは、これからの開発で大きな効果がありそうですね。
まだ、詳しく理解しておりませんので、これからこのクラスについて勉強してみます。

千八巧者さん、エツさん、貴重なレスありがとうございました。

以上です。
おはようございます。
ArrayListクラスを思いつかずにいろいろ書き込んでしまい、失礼しました。
調べてみると、各種のメソッドが充実していてとても汎用性がありますね。

速度のチェックもしてみましたが、Add()については、先に試したとおり、配列の配列を用いる方法とほとんど変わりませんでしたし、i番めの要素を読み出す速度についても、どちらの方法でも1件あたり数十ナノ秒でした。

ArrayListでは領域の確保が2倍毎ですので、たとえば整数型のデータを何千万件も扱うような場合は、過大なメモリ領域を占有してしまうこともあるかもしれませんが、多くの場合は問題なく使えそうですね。
おはようございます、千八巧者さん。maskです。

私は基本的なコーディング(まさに今回の配列もその1つ)をVB6.0から.Netにそのままの手法で用いてしまっていたため、
ArrayListクラスという便利なものに気づかず、VB6.0でのRedim Preserveという方法をとり続けてしまっていました。

今回の問題では、たまたまこのような新しい発見に到ることができましたが、おそらく、他のコーディング部分についても
まだまだ効果的でない個所もある可能性が高いと思いますので、これを機会に.Netなりのやり方やより質の高いプログラム
を作成できるようにしていきたいと思っております。

以上です。
VBは最近使い始めたのでよくわからないのですが、Redim Preserve時に新たに領域を確保して以前の値をコピーするのは、VB6以前でも同じだったのではないでしょうか。

もし私の理解で正しければ、VB6以前においても、1件ごとにRedim Preserveを行うのが妥当なケースは、かなり限られると思います。それ以外では、メモリ効率、データ追加の処理速度、データ読み出し・更新の速度などいろいろなファクターを吟味して、最初から大きな領域を確保する、必要に応じてある程度の大きさを追加してRedim Preserveする、配列の配列を使う、などの選択肢から最適な方法を採用しますが、その中で、「追加・読み出しとも最高ではない処理速度で問題ない」という多くのケースでは、配列の配列を使う(.NETにおいては、「最高ではないメモリ効率と最高ではない処理速度で問題ない」ならばArrayListを使う)のが妥当、ということになるだろうと思います
>VBは最近使い始めたのでよくわからないのですが、Redim Preserve時に新たに領域を確保して以前の値をコピーするのは、VB6以前でも同じだったのではないでしょうか。

そうです

>その中で、「追加・読み出しとも最高ではない処理速度で問題ない」という多くのケースでは、配列の配列を使う(.NETにおいては、「最高ではないメモリ効率と最高ではない処理速度で問題ない」ならばArrayListを使う)のが妥当、ということになるだろうと思います

何をいわんとしているのかよくわからないのですが、Redim Preserveなんか使うのは限られた局面で、よくわからないならArrayListを使っておけばよいでしょう。
こんばんは、中博俊さん、千八巧者さん。maskです。

レスありがとうございます、ArrayListの件了解しました。
今後もさらに勉強していきたいと思います。

また、本題の件についても、一旦クローズさせていただき、また新たに進展があったときに連絡したいと思います。
とりあえずは、今までのことを整理して、もう少し調査した後にユーザさんへ対応していく予定です。

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

以上です。
解決済み!

DOBON.NET | プログラミング道 | プログラミング掲示板