2012-02-19 14 views
1

WindowsサービスでSystem.OutOfMemoryExceptionが発生する(毎回30-60分)。サービスの仕事は、サービスデータが共通のXMLデータフォーマットに洗い流されるデータファイルを含む6つのディレクトリをループすることです。.NETヒープで文字列オブジェクトがいっぱいになる - > OutOfMemoryException

これらの6つのフォルダにはそれぞれ5-10.000個のファイルが含まれているため、ファイルの総数は約45.000であり、その日の新しいファイルが追加されます。 1日に約1-2000の新しいファイルが追加されます。ファイルは4KBから500KBです。

各データファイルは、XElementオブジェクトを介して共通のXMLデータ形式に洗われます。

サービスでRedGates ANTS Memory Profilerを使用しましたが、最もメモリを使用しているオブジェクトは文字列(約90.000.000バイト)とXElement(約51.000.000バイト)です。

メモリプロファイラで、文字列オブジェクトを使用しているものをトレースすると、文字列オブジェクトを使用しているXElementオブジェクトがほとんど(93%)ということがわかります。

サーバーには6つのCPUと6GBのRAMがあるため、なぜOutOfMemoryExceptionが表示されているのか分かりません。プロセスのWindowsサービスを見ると、MAXのRAM使用量は1.2GBとなっています。

文字列オブジェクトがインターンテーブルに格納されているため、.NETガベージコレクタで文字列オブジェクトがクリアされないことがあります。これはエラーかもしれません、もしあれば、私はそれについて何ができますか?

以下のコードは、ファイルをループする方法を示しています。ご覧のとおり、私は一度に20ファイルを取ろうとしました。これはOutOfMemoryExceptionを数時間押し込むだけなので、サービスは30〜60分ではなく4〜5時間実行されます。

なぜ私はOutOfMemoryExceptionを実行できますか?

private static void CheckExistingImportFiles(object sender, System.Timers.ElapsedEventArgs e) 
    { 
     CheckTimer.Stop(); 
     var dir = Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories); 

     List<ManualResetEvent> doneEvents = new List<ManualResetEvent>(); 
     int i = 0; 
     //int doNumberOfFiles = 20; 

     foreach (string existingFile in Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories)) 
     { 
      if (existingFile.EndsWith("ignored") || existingFile.EndsWith("error") || existingFile.EndsWith("importing")) 
      { 
       //if (DateTime.UtcNow.Subtract(File.GetCreationTimeUtc(existingFile)).TotalDays > 5) 
       // File.Delete(existingFile); 
       //continue; 
      } 

      StringBuilder fullFileName = new StringBuilder().Append(existingFile); 

      if (!fullFileName.ToString().ToLower().EndsWith("error") && !fullFileName.ToString().ToLower().EndsWith("ignored") && !fullFileName.ToString().ToLower().EndsWith("importing")) 
      { 
       File.Move(fullFileName.ToString(), fullFileName + ".importing"); 
       fullFileName = fullFileName.Append(".importing"); 

       ImportFileJob newJob = new ImportFileJob(fullFileName.ToString()); 

       doneEvents.Add(new ManualResetEvent(false)); 

       ThreadPool.QueueUserWorkItem(newJob.Run, doneEvents.ElementAt(i)); 
       i++; 
      } 

      //if (i > doNumberOfFiles) 
      //{ 
      // i = 0; 
      // doNumberOfFiles = 20; 
      // break; 
      //} 
     } 
     i = 0; 
     WaitHandle.WaitAll(doneEvents.ToArray()); 

     CheckTimer.Start(); 
    } 
+1

'ImportFileJob'とは何ですか?どのように実装されていますか? – Tigran

+0

あなたの 'StringBuilder'の使用は冗長です。 IOWの利点はありません。 – leppie

+0

ImportFileJobはデータファイルを取り、xlstスタイルシートを使用してデータファイルを共通XMLデータファイルに変換します。 – Poku

答えて

1

Avner Shahar-Kashtanが既に述べたように、私も問題はImportJob(あなたは私たちのコードを示していない)だと思います。

でも、まだ最適化を行うことはできます。

すべてのファイル名を一度に読み込む必要はありません。それは

IEnumerable<string> GetAllFiles(string dirName) 
{ 
    var dirs = Directory.GetDirectories(dirName); 

    foreach (var file in Directory.GetFiles(dirName)) 
     yield return file; 

    foreach (var dir in dirs) //recurse 
     foreach (var file in GetAllFiles(dir)) 
      yield return file; 
} 

以下のようにTPLを使用してディレクトリでdirを行うことができます、あなたがところで作成ManualResetEvent S(およびその忘れDispose()秒)

Parallel.ForEach(GetAllFiles(RawDataDirectory.FullName) , file => 
{ 
    //ImportFileJob newJob = new ImportFileJob(file); 
    //newJob.Run 
    Console.WriteLine(file); 
}); 

の数を減らすことができ、あなたがすべきまた、他の人が示唆したように、CountdownEvent

+0

ImportFileJobクラスはさまざまな機能を備えていますが、XElementクラスは多く使用されていますので、ここで問題が発生する可能性があります。 .NETガベージコレクタはXElementオブジェクトをクリーンアップしてはいけませんか? – Poku

+0

何百万という理由があるかもしれません。私はあなたのコードを知らない。しかし、リソース(ファイルなど)を解放しない限り、XElementやガベージコレクタから疑わしい理由はありません。 –

1

バットで簡単に最適化を行うことができます。

多くのfullFileName.ToString().ToLower().EndsWith("ignored")コールを使用します。これらは、指定された文字列を常に取得し、小文字の新しい文字列を作成するため、オーバーヘッドが大きくなります。

fullFileName.ToString() 
    .EndsWith("ignored", StringComparison.CurrentCultureIgnoreCase) 

また、私はあなたのStringBuildersが、この場合には役立っているとは思わない:

はその代わりに、大文字と小文字を区別しない比較を可能に過負荷をEndswithを使用する(または入って)必要があります。 StringBuildersは、複数パートの文字列を作成しているときに、複数の中間文字列を作成する際のオーバーヘッドを必要としない場合に最も便利です。ここの文字列の連結はすべて、常に2つの文字列(基本名と新しい接尾辞)しか使用していないようですので、実際にはいつでもメモリを節約しているかどうかはわかりません。

+0

-1。あなたは正しいですが、そのどれも問題を引き起こすものではありません。それらの文字列はすぐに破棄されます。 – TomTom

2
Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories); 

これは配列を返します。ディレクトリーがあなたの状態と同数のファイルを持っている場合、これらは非常に大きな配列になり、大きなオブジェクト・ヒープに配置するのに十分な大きさになります。たくさんの大規模配列が簡単にOutOfMemoryExceptionを引き起こす可能性があります。次の行には役に立ちません。

var dir = Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories); 

には何もしていない変数「dir」があります。大きな配列は、メソッドの実行ごとに2回作成されます。

0

fullFileName.ToString()を呼び出します。あなたのIfステートメントでToLower()を3回実行します。この文字列値をローカル変数にキャッシュし、そのif文を使用します(3つの一時文字列を保存します)。

XDocumentではなくXmlWriterを使用してみてください。 XDocumentはメモリ内のオブジェクトグラフなので、大規模なデータセットの場合、最もパフォーマンスの高いものではないかもしれません(全体をディスク全体に書き出すまで、メモリ全体を保持します)。 XmlWriterを使用すると、通常、要素ごとにファイルバッファ要素にストリームを流すことができます。メモリフットプリントはそれほど厳しくありません。

各インポートの作業量は不明ですが、ファイルごとではなくディレクトリごとにスレッドを試しましたか?

0

を参照してください

1)文字列を減らすmanipu Lation。

あなたのディレクトリは「あまりにも多くの」ファイル名(文字列)を戻しているので、注意が必要です。

2)あなたの回線 'var dir = Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories);' は冗長であるようです。あなたがそれを使用していないように見えます。だから、このコードを削除して、それは文字列参照の多くを保持しています。

3)可能であれば、ディレクトリから返されたファイルをチャンク(例:10K)で繰り返します。したがって、List to List>を分割するコードを記述し、外側のループを反復するときに内側のリストに保持されている参照をクリアする必要があります。 何かのように、

foreach(List<List<string>> fileNamesInChunk in GetFilesInChunk(directoryName)){ 
    foreach(var fileName in fileNamesInChunk){ 
    //Do the processing. 
    } 
    fileNamesInChunk.Clear(); //This would reduce the working set as you proceed. 
} 

希望すると、これが役に立ちます。

1

代わりのタイマーを使用して、FileSystemWatcherを使用することができ、フォルダのすべての内容をループ:あなたのプログラムが変更され、正確なファイルが通知され、あなたも割り当てる必要はありませんその方法をhttp://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.aspx

あなたが気にしないファイルの配列のためのメモリ。