QA@IT
この質問・回答は、@ITの旧掲示板からインポートされたものです。

OutOfMemoryExceptionの対策について

あるテキスト(CSVファイル)を1行ずつ読込み、条件にあう行のみ別ファイルに書き込み処理
を作成したのですが読み込むファイルのサイズが4GB程度あり(書き込み後の想定は1GB程度)、
処理中にSystem.OutOfMemoryExceptionが発生します。
読み込みの方法等、何か良い対策はないでしょうか?

開発:VB.NET

Try
Dim srFile As New System.IO.StreamReader("読込みファイル.csv", System.Text.Encoding.Default)
Dim sw As New System.IO.StreamWriter("書込みファイル.csv", False, System.Text.Encoding.GetEncoding(932))
'1行ずつ最後まで読み込む
Dim strLine As String = "#"

While Not strLine Is Nothing
    '1行ずつ読み込む
    strLine = srFile.ReadLine()
    If strLine = Nothing Then
        '何も読み込まない時はWhile分を抜ける
        'Exit While
    Else
        Dim str1() As String = strLine.Split(",")

        If 条件(str1 = "1") Then
            '書込み処理
            sw.Write(strLine)
            sw.Write(vbCr + vbLf)
        End If
    End If
End While

srFile.Close()
sw.Close()
Return true

Catch ex As Exception
Return false
End Try

質問者:Dra

回答

Draさんの書き込み (2006-05-15 13:29) より:

読み込むファイルのサイズが4GB程度あり(書き込み後の想定は1GB程度)、

処理中にSystem.OutOfMemoryExceptionが発生します。

不思議ですね、解放が追いつかないものなんでしょうか。
Thread.Sleep メソッドを挟むことで、改善するかどうか確認してみてください。

sw.Write(strLine)

sw.Write(vbCr + vbLf)

WriteLine メソッドを使うべきだと思います。

_________________C# と VB.NET の入門サイト
じゃんぬねっと日誌

投稿者:じゃんぬねっと

編集 履歴 (0)

処理中にSystem.OutOfMemoryExceptionが発生します。

1行分の処理が終わったところで、GC.Collect() するようにしてみてください。

投稿者:渋木宏明(ひどり)

編集 履歴 (0)

StreamWriter の方も一行処理が終わるたびに適当に Flush() しないといけないのかも知れませんね。(1GB を抱えたままはでかすぎる。)
_________________囚人のジレンマな日々

投稿者:囚人

編集 履歴 (0)

じゃんぬねっとさん、渋木宏明(ひどり)さん、囚人さんありがとうございます。
遅れてすいません。

まず、WriteLineを使用しある行単位でSleepを使用しましたが、メモリー
はうまいこと開放されていないようでした。
GC.Collect()を試してみます。

投稿者:Dra

編集 履歴 (0)

GC.Collect()を試しました。
Sleep処理を使用するよりは、行の読込み・書込みは多くなりましたが
同様にSystem.OutOfMemoryExceptionが発生します。

Flush()の使用については、理解していない為調べて試してみます。

他に何かよい処理があればご教授願います。

投稿者:Dra

編集 履歴 (0)

Draさんの書き込み (2006-05-15 17:42) より:

Flush()の使用については、理解していない為調べて試してみます。

ということは AutoFlush のままと考えた方が良さそうですね。
囚人さんが懸念されていることが当たっているとすれば、
AutoFlush が追いついていないことに... いや、そんなことはないような...

とりあえず、AutoFlush プロパティ を false に設定しておいて、
繰り返しの最後に Flush メソッド を含めてみてください。

_________________C# と VB.NET の入門サイト
じゃんぬねっと日誌

投稿者:じゃんぬねっと

編集 履歴 (0)

OSはWindows2000ですか?最新のSPはあててますか?タスクマネージャで見ると、システムキャッシュが大きくなっていませんか?.NET以前の話ですが、大きなファイルにシーケンシャルにI/Oすると、メモリ不足に陥る不具合があったような気が・・・。

#その時には適当にSleepを入れて、リトライすることで逃げました。

投稿者:甕星

編集 履歴 (0)

じゃんぬねっとさんありがとうございます。

書き込んでいるファイルのサイズが202322KB(198MB)で毎回エラー発生となります。
何か原因があるのでしょうか?

投稿者:Dra

編集 履歴 (0)

甕星さん、ありがとうございます。
OSはWin2000です。確認してみます。

投稿者:Dra

編集 履歴 (0)

ところで、どの行で例外が発生するのでしょう?

投稿者:Jitta

編集 履歴 (0)

返答遅れました、すいません。
ファイルの読み込みでエラーが発生しています。20万行目くらいです。

WinXPでも試してみますが・・・

投稿者:Dra

編集 履歴 (0)

AutoFlush はデフォルトで false だと思うので、Flush() しないからメモリに溜まり放題、と踏んでいるのですが…。



sw.Write(strLine) 
sw.Write(vbCr + vbLf) 
sw.Flush()

とやってみても無駄でした?
_________________囚人のジレンマな日々

投稿者:囚人

編集 履歴 (0)

囚人さん、ありがとうございます。
sw.Write(strLine)
sw.Write(vbCr + vbLf)
sw.Flush()
毎行、書き込み時にFlush()を使用してもファイルサイズ198MBで同様な
エラーが発生します。

投稿者:Dra

編集 履歴 (0)

chackさん、ありがとうございます。
<4GT RAM チューニングのアプリケーションでの利用について
Microsoft Windows NT Server以外のサーバも対象となるのでしょうか。そもそも毎回読み書き時にGCを行っていてもメモリ使用量が増えているのはなぜでしょうか?
ちなみにマシンのメモリは500MB程度です。

投稿者:Dra

編集 履歴 (0)

仮想メモリが無いか非常に小さい、とかいうオチはさすがにないですよね。
_________________囚人のジレンマな日々

投稿者:囚人

編集 履歴 (0)

囚人さんの書き込み (2006-05-16 12:32) より:

仮想メモリが無いか非常に小さい、とかいうオチはさすがにないですよね。

その場合は、Windows 2000 側からも悲鳴があがるはずです。(;^-^)

_________________C# と VB.NET の入門サイト
じゃんぬねっと日誌

投稿者:じゃんぬねっと

編集 履歴 (0)

Draさんの書き込み (2006-05-16 11:42) より:

ちなみにマシンのメモリは500MB程度です。

500MBというのは物理メモリですよね?
物理メモリ容量に関係なく、32bit系OSでプロセスが使用可能な
仮想メモリ空間は 2GB となります。

この 4GT RAM チューニングは、Windows2000, Windows XP, Windows Server 2003等の
32bit NT系OSで有効です。

なお、GCを行っていてもメモリ使用量が増えるというのは、何とも言えませんが、
アンマネージヒープがどこかで使われているのでは。

投稿者:chack

編集 履歴 (0)

ちょっと質問ですが、データは正しいですか?
間違いなくちゃんと改行が入っていますか?
また、一行は大体どれくらいの長さで、どれくらいの項目数でしょうか?

投稿者:なちゃ

編集 履歴 (0)

サイズの問題上、確認できませんがデータは正しいと思います。
改行の点も同様です。
データに関しては、1レコード、54項目で300byte程度です。

投稿者:Dra

編集 履歴 (0)

Draさんの書き込み (2006-05-16 16:22) より:

サイズの問題上、確認できませんがデータは正しいと思います。

改行の点も同様です。

データに関しては、1レコード、54項目で300byte程度です。

一傍観者ですが、不思議ですね。単にテキストファイルを1行ごとに読んでフィルター処理してテキストファイルを書き出すだけなのですよね。こんな単純なことでメモリー不足のエラーになっていたら、巷の大抵のアプリケーションがこのエラーに遭遇してしまいそうです。

つぎの点を確認されてはどうでしょうか。
・読み込むファイルを本番データーではない、単純なテキストファイルにして試してみる。サイズは本番と同じだけど単に "ABCDEFGHI..." が何万行もあるような内容のテストデーターをなんらかの方法で作り出して使う。
・問題のプログラムの部分だけを切り出して単体の Windows アプリケーションとして実行してみる。今は Web アプリケーションなどではないのでしょうか?再現できるプログラムを提示されれば他のかたも手近な環境で試せますし。
・ファイルを読むだけにして、書くのをやめてみる。
・その他、ウィルス対策ソフトウェアなどを疑ってみる?

--
unibon {B73D0144-CD2A-11DA-8E06-0050DA15BC86}

投稿者:unibon

編集 履歴 (0)

unibonさん、ありがとうございます。

・読み込むファイルを本番データーではない、単純なテキストファイルにして試してみる。サイズは本番と同じだけど単に "ABCDEFGHI..." が何万行もあるような内容のテストデーターをなんらかの方法で作り出して使う。
試してみます。

・問題のプログラムの部分だけを切り出して単体の Windows アプリケーションとして実行してみる。今は Web アプリケーションなどではないのでしょうか?再現できるプログラムを提示されれば他のかたも手近な環境で試せますし。
元からWindowsアプリです。

・ファイルを読むだけにして、書くのをやめてみる。
間違っておりました、読み込みではなく書き込み時にエラーが発生していました。
以下の何百万レコードのループ処理で発生しています。
sw.Write(strLine)
sw.Write(vbCr + vbLf)

投稿者:Dra

編集 履歴 (0)

StreamReader/StreamWriterのバッファは指定しない限り、
1024+4096byte以上使用されることはなく適宜Flushされます。
明示的にFlushされるまですべてを保持しているわけではありません。
なので、これらのクラスが原因とは考えにくいです。

試しに4GBのファイルを作成し、Read/ReadLine,Write/WriteLineを
色々と組み合わせてみましたが、読み書き共に問題なく動作しました。

例外情報を編集せずに提示してみては?

ちなみに追いつく追いつかないとはどういう意味なのでしょうか?
GC中は他スレッドは停止すると認識してるんですが・・・

投稿者:うにくま

編集 履歴 (0)

Draさん、こんばんは。

ReadLine メソッドの解説には、以下のような文章があります。

現在のメソッドが OutOfMemoryException をスローした場合、

(途中略)

このような状況を回避し、信頼性の高いコードを作成するには、Read メソッドを使用して、割り当て済みのバッファに読み取った文字を格納する必要があります。

わざわざ解説されているって事は、何かの加減で、割と頻繁に発生する可能性があるって事なんでしょうかねぇ。

あと、重箱の隅を突くようですが、

If strLine = Nothing Then

これは
If strLine Is Nothing Then
にしないと、常に Else の方へ流れませんでしたっけ?

それと、

Catch ex As Exception

Return false 

End Try

StreamReader とか StreamWriter とかは、Finally で Close すべきですね。

投稿者:きくちゃん

編集 履歴 (0)

うにくまさん・きくちゃんさん、ありがとうございます。

>If strLine Is Nothing Then
>StreamReader とか StreamWriter とかは、Finally で Close すべきですね。
ご指摘ありがとうございます。

ReadLine メソッドの解説にあてはまりそうですね。レコード毎にbyte数が異なる為
Readメソッドの使用は難しいと考えられます。(ある項目をみて書込みを判断する)が検討してみます。
ありがとうございます。

投稿者:Dra

編集 履歴 (0)

ちなみに追いつく追いつかないとはどういう意味なのでしょうか?

GC中は他スレッドは停止すると認識してるんですが・・・

いいえ。.NET は GC 専任のスレッドがバックグラウンドでヒープのお掃除をします。

遠い昔に GDNJ で議論になりましたが

for(long i = 0; i < 1000000; i++)
{
byte[] buffer = new byte [256 * 1024 * 1024];
}

みたいなコードは、少なくとも .NET 1.x では OutOfMemoryException がスローされます。

投稿者:渋木宏明(ひどり)

編集 履歴 (0)

うにくまさんの書き込み (2006-05-16 17:45) より:

StreamReader/StreamWriterのバッファは指定しない限り、

1024+4096byte以上使用されることはなく適宜Flushされます。

明示的にFlushされるまですべてを保持しているわけではありません。

なので、これらのクラスが原因とは考えにくいです。

試しに4GBのファイルを作成し、Read/ReadLine,Write/WriteLineを

色々と組み合わせてみましたが、読み書き共に問題なく動作しました。

例外情報を編集せずに提示してみては?

 私も、ディスクの空き領域の関係で 2GB ですが、全く問題ありませんでした。タスクマネージャの報告によると、プロセスが使用するメモリは常に一定でした。なので、Stream 系の問題とは考えにくいです。

 うにくまさんも指摘されているとおり、Exception.ToString() の値を、そのまま書き出してください。2日が過ぎようとしていますが、Read でエラーになることと、Exception.ToString() の値とを最初に出していれば、もっと早くに解決したように思います。

 また、

If 条件(str1 = "1") Then

のところも、文法的におかしいので、いろいろな処理を省略しているのだと思います。そこに本当の問題があるようにも思います。

投稿者:Jitta

編集 履歴 (0)


for(long i = 0; i < 1000000; i++) 
{
    byte[] buffer = new byte [256 * 1024 * 1024];
}

みたいなコードは、少なくとも .NET 1.x では OutOfMemoryException がスローされます。

ですが、これはnewobj/newarrで(Large Objectの場合だけ?)適切にCollectされていなかった
の方が自分としてはしっくりくるのですが。。。
まぁ、確認のしようがないんですけどね。

AutoFlushプロパティについてですが、
これは書き込みが既定バッファに満たなくてもFlushするかどうかという事だと思います。
たとえば、1100byteが書き込み(Write)されたとして、
Falseの場合は、1024byte (StreamWriterのバッファ -> FileStreamのバッファ)
Trueの場合は、1100byte (StreamWriterのバッファ -> FileStreamのバッファ -> デバイス)
がFlushされるということだと思います。それと処理も微妙に違います。

投稿者:うにくま

編集 履歴 (0)

みなさんありがとうございます。
現状は以下のように書込みなしの場合でも同様なエラーが発生し、解決しておりません。
エラーに関しては、「種類 System.OutOfMemoryException の例外がスローされました。」
のみしかとれません。

Dim strLine As String = ""
Dim srFile As New System.IO.StreamReader("読込みファイル", System.Text.Encoding.Default)
Dim sw As New System.IO.StreamWriter("書込みファイル", False, System.Text.Encoding.GetEncoding(932))
Try
While Not strLine Is Nothing
strLine = Nothing
'1行ずつ読み込む
strLine = srFile.ReadLine()
If strLine Is Nothing Then
'何も読み込まない時はWhile分を抜ける
Exit While
Else
'書込みはなし
End If
End While
Return true
Catch ex As Exception
Return false
Finally
srFile.Close()
sw.Close()
End Try

投稿者:Dra

編集 履歴 (0)

strLineってのをWhile内で宣言してもダメかいな?
GCの手助けにはなるだろう。(ってNothingと効果は同じだろうが)

投稿者:ぶさいくろう

編集 履歴 (0)

私も 300MB ほどのファイルを作成し同様の実装をしてみましたが、正常に動作しました。
やはりファイル自体か、その他の実装による影響を疑った方が良いのではないでしょうか?

unibon さんが、「問題の切り分け」の仕方を提示してくださっていますが、すべて試されていますか?

_________________C# と VB.NET の入門サイト
じゃんぬねっと日誌

投稿者:じゃんぬねっと

編集 履歴 (0)

Draさん、お早うございます。

レコード毎にbyte数が異なる為Readメソッドの使用は難しいと考えられます。

いや、読みとるデータは、レコード毎である必要はありません。
CR/LFを頼りに、自力で「レコード」を組み立てれば良いんです。

エラーに関しては、「種類 System.OutOfMemoryException の例外がスローされました。」

のみしかとれません。

デバッグ実行して、キャッチした例外の StackTrace を確認すれば、もう少し詳細な情報が得られますよ。

投稿者:きくちゃん

編集 履歴 (0)

きくちゃんさん、ありがとうございます。
>CR/LFを頼りに、自力で「レコード」を組み立てれば良いんです。
対応は保留中です。

>StackTrace を確認すれば
なぜかわかりませんがStackTrace情報はnothingです。

投稿者:Dra

編集 履歴 (0)

なぜかわかりませんがStackTrace情報はnothingです。

ごめんなさい。
私の方でも試してみましたが、OutOfMemoryExceptionの場合は種類以外は取得できないみたいです。
言われてみれば確かにそうですよね。

投稿者:うにくま

編集 履歴 (0)

うにくまさんの書き込み (2006-05-17 10:27) より:

私の方でも試してみましたが、OutOfMemoryExceptionの場合は種類以外は取得できないみたいです。

おっと、いい加減な事を書いてしまったみたいですね。
失礼しました。

甕星さんの情報も気になるんですが、Xpとかで試してみた結果も教えて下さいね。>Dra さん。

投稿者:きくちゃん

編集 履歴 (0)

OSに関しては、Win2000、WinXPで試しました。また.NET2002,2003及びC#に変換し試しましたがまったく同じ結果となりました。
データの問題かどうかも確認できません。

投稿者:Dra

編集 履歴 (0)

Draさんの書き込み (2006-05-17 11:07) より:

データの問題かどうかも確認できません。

これは、"正しく読めることが確定している行" を 300MB になるまで、
延々とコピーしたファイルで試せば確認できますよね。

また、提示された部分だけのソースで動作させていますか?
新規プロジェクトを生成し、単純に Button を 1 つだけ配置し実行してみてください。
(ミニマム コードで試した方が良いということです)

このように、問題の「切り分け」をどんどんしていく必要があります。
もし試されているのであれば、洗いざらい書いてくださった方が良いです。

OSに関しては、Win2000、WinXPで試しました。

マシンを変えても再現したことから、少なくとも環境依存ではなさそうですね。
(こうやって「切り分け」して狭めていきます)

_________________C# と VB.NET の入門サイト
じゃんぬねっと日誌

投稿者:じゃんぬねっと

編集 履歴 (0)

 ふと気になった。

System.IO.StreamReader("読込みファイル", System.Text.Encoding.Default)
System.IO.StreamWriter("書込みファイル", False, System.Text.Encoding.GetEncoding(932))

 読み込むファイルは、Default コード ページ(Windows 2000 だから UTF-8?)で、人間が文字として認識できる文字のみが書き込まれているのだろうか?

Draさんの書き込み(2006-05-15 19:16)より:

書き込んでいるファイルのサイズが202322KB(198MB)で毎回エラー発生となります。

何か原因があるのでしょうか?

Draさんの書き込み(2006-05-16 00:59)より:

ファイルの読み込みでエラーが発生しています。20万行目くらいです。

 ReadLine で例外が発生するということは、ここまで(20万行目くらいまで)は正常と判断できると思います。
 StreamReader を直接開くのではなく、seek 可能なストリームから StreamReader を作り、例外が発生する直前の位置を取得する。例外が発生したらプログラムを修正して、さっきまで進んでいた位置に飛ばす。そして実行すると、データが累積するのが悪いのか、ファイルのが悪いのか、判断できるでしょう。



Dim stream As Stream = Nothing
Dim reader As StreamReader = Nothing
Dim pos As Long = 0L ' これを換える
Try
    stream = New FileStream(file_name, FileMode.Open, FileAccess.ReadWrite)
    stream.Position = pos
    Try
        reader = New StreamReader(stream, エンコード)
        Dim line As String = reader.ReadLine()
        Dim lnum As Long = 1L
        While Not reader Is Nothing
            System.Diagnostics.Debug.WriteLine(String.Format("{0:d} : {1:d}", stream.Position, lnum)
            line = reader.ReadLine()
            lnum += 1L
        End While
    Finally
        If Not reader Is Nothing Then
            reader.Close()
        End If
    End Try
Finally
    If Not stream Is Nothing Then
        stream.Close()
    End If
End If

 UNIX マシン、または Cygwin などの UNIX コマンドが使える環境があるなら、wc -l で行数を数え、tail で20万行目以降を別のファイルに落とすことも出来るでしょう。

 コマンドプロンプトから、type コマンドで出力することも出来そう。more コマンドは、落ちるかな?

 あるいは、そのファイルが正しく(仕様通りか、こちらの意図通り)出力されていることは、どうやって確認したのでしょう?

投稿者:Jitta

編集 履歴 (0)

Draさんこんにちは。

Stringは値を代入する度に、内部的に新たなStringクラスが作成されると
記憶しています。
以前、.NET開発していた際にも、コード分析を行った方から、Stringに対し
「+=」のような記述を多用する場合は「StringBuilder」を使うべきだと
指摘された事があります。

ぶさいくろうさんも仰っていましたが、「strLine」をWhileの内側で宣言する
方法が良いかもしれません。
或いは、「strLine」をStringBuilderで宣言し、「ReadLine」の直前に
「strLine」を「Remove」する事で、メモリの消費を抑えられそうな気がします。

これでも解決されないようであれば、余分な(処理に影響の無い)コードを
削ぎ落とすなどして、問題点の切り分けを地道にして行くしかなさそうですね。

投稿者:梶

編集 履歴 (0)

試して見られた方はだれも同じ問題が発生していないのですから、
まずデータが正常かの確認が先でしょう。
もしデータにものすごく長い行などが紛れ込んでいたら、
いろいろ小細工してみても意味がありません。
※バイナリで読み込んでうまく処理するとかは別ですが。

梶さんの書き込み (2006-05-19 09:32) より:

或いは、「strLine」をStringBuilderで宣言し、「ReadLine」の直前に

「strLine」を「Remove」する事で、メモリの消費を抑えられそうな気がします。

あんまり変なことはしないほうがいいと思います。

投稿者:なちゃ

編集 履歴 (0)
ウォッチ

この質問への回答やコメントをメールでお知らせします。