我经常(每30-60分钟)在我的Windows服务中获得System.OutOfMemoryException。该服务的工作是循环6个目录,其中包含服务数据以常见XML数据格式清洗的数据文件。
这6个文件夹每个包含5-10.000个文件,因此文件总数约为45.000,并且在当天添加了新文件。每天新增大约1-2000个新文件。文件大小在4KB到500KB之间。
每个数据文件都通过XElement对象清洗为通用XML数据格式。
我在服务上使用了RedGates ANTS Memory Profiler,使用最多内存的对象是字符串(大约90.000.000字节)和XElement(大约51.000.000字节)。
在Memory Profiler中,当我跟踪什么是使用字符串对象时,我可以看到它主要是(93%)使用字符串对象的XElement对象。
服务器有6个cpu和6GB的RAM,所以我看不出为什么我得到了OutOfMemoryException。如果我查看进程中的Windows服务,它的RAM使用量为1.2GB。
我已经读过.NET垃圾收集器不清除字符串对象,因为字符串对象存储在实习表中。这可能是错误,如果是这样我该怎么办呢?
下面的代码显示了我如何循环浏览文件。正如你所看到的,我也试过一次拿20个文件。这只会将OutOfMemoryException推迟几个小时,因此该服务将运行4-5小时而不是30-60分钟。
为什么我可以使用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();
}
答案 0 :(得分:2)
Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories);
这将返回一个数组。如果目录中包含的文件数量与您所声明的一样多,则这些文件将是非常大的数组,大小足以放置在大对象堆中。 Mutliple海量数组很容易导致OutOfMemoryException。这对以下行没有帮助
var dir = Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories);
的变量'dir'没有做任何事情。每个方法执行创建两次大型数组。
答案 1 :(得分:1)
我可以立即发现一些简单的优化。
您使用了大量fullFileName.ToString().ToLower().EndsWith("ignored")
次来电。这些都有很多开销,因为你总是使用给定的字符串并创建一个新的小写字符串。
相反,您应该使用Endswith(或Contains)重载,以允许不区分大小写的比较:
fullFileName.ToString()
.EndsWith("ignored", StringComparison.CurrentCultureIgnoreCase)
此外,我不认为你的StringBuilders在这种情况下有所帮助。 StringBuilders在构建多部分字符串时非常有用,并且不希望在编写它们时创建多个中间字符串。看来这里所有的字符串连接总是只使用两个字符串 - 基本名称和新后缀 - 所以我不确定它实际上是在节省你的时间或内存。
答案 2 :(得分:1)
正如Avner Shahar-Kashtan所说,我也认为问题出在ImportJob
(你没有向我们展示过它的代码)。
即便如此,您仍然可以进行一些优化。
您不必一次加载所有文件名。它可以通过dir完成,如下所示
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,您可以减少创建的ManualResetEvent
的数量(及其已忘记的 Dispose()
)
Parallel.ForEach(GetAllFiles(RawDataDirectory.FullName) , file =>
{
//ImportFileJob newJob = new ImportFileJob(file);
//newJob.Run
Console.WriteLine(file);
});
顺便说一下,您还应该看到CountdownEvent
答案 3 :(得分:1)
您可以使用FileSystemWatcher而不是使用计时器并循环遍历文件夹的所有内容:http://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.aspx
通过这种方式,您的程序会收到更改的确切文件的通知,您甚至不必为您不关心的文件阵列分配内存。
答案 4 :(得分:0)
在If语句中调用fullFileName.ToString()。ToLower()三次。将此字符串值缓存在局部变量中,并使用if语句(保存三个临时字符串)。
尝试使用XmlWriter而不是XDocument。 XDocument是内存中的对象图,因此对于大型数据集而言,它可能不是最高性能的(在将内容写入整个光盘之前,将整个内容保存在内存中)。使用XmlWriter,您通常可以逐个元素地流式传输到文件缓冲区,内存占用的要求要低得多。
不确定每个导入的工作量是多少,但是您是否为每个目录而不是每个文件尝试了一个线程?
答案 5 :(得分:0)
正如其他人所说,
1)减少字符串操作。
您的目录似乎返回“太多”文件名(字符串),因此需要注意。
2)你的专栏“var dir = Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories);
”
似乎是多余的。看起来你没有使用它。因此,删除此代码,它持有大量的字符串引用。
3)如果可能,迭代从块中的目录返回的文件(比如10K)。因此,这需要编写一个将List拆分为List&gt;的代码,然后在迭代外部循环时清除内部列表所持有的引用。 好像,
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.
}
希望这会有所帮助。