Кучи .NET заполнены строковым объектом -> OutOfMemoryException

Я постоянно (каждые 30-60 минут) получаю исключение System.OutOfMemoryException в моей службе Windows. Задача службы состоит в том, чтобы пройти через 6 каталогов, содержащих файлы данных, которые служба отмывает данные в общий формат данных XML.

Эти 6 папок содержат от 5 до 10 000 файлов каждая, так что общее количество файлов составляет около 45 000, и новые файлы добавляются в течение дня. Ежедневно добавляется от 1 до 2000 новых файлов. Файлы имеют размер от 4 КБ до 500 КБ.

Каждый файл данных преобразуется в общий формат данных XML с помощью объекта XElement.

Я использовал RedGates ANTS Memory Profiler в службе, и объекты, которые используют больше всего памяти, — это строка (около 90 000 000 байт) и XElement (около 51 000 000 байт).

В профилировщике памяти, когда я отслеживаю, что использует строковый объект, я вижу, что это в основном (93%) объект XElement, который использует строковый объект.

Сервер имеет 6 процессоров и 6 ГБ ОЗУ, поэтому я не понимаю, почему я получаю исключение OutOfMemoryException. Если я посмотрю на службу Windows в процессах, то МАКСИМАЛЬНОЕ использование ОЗУ составляет 1,2 ГБ.

Я читал, что сборщик мусора .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();
    }

person Poku    schedule 19.02.2012    source источник
comment
Что делает ImportFileJob? Как это реализовано?   -  person Tigran    schedule 19.02.2012
comment
Ваше использование StringBuilder излишне. Нет выгоды IOW.   -  person leppie    schedule 19.02.2012
comment
ImportFileJob берет файл данных и использует таблицу стилей xlst для преобразования файла данных в общий файл данных XML.   -  person Poku    schedule 19.02.2012
comment
Я использовал строку списка в приложении, из-за которой служба выполняла исключение outofmemoryexception через 3-5 минут. Профилировщик памяти ANTS сказал мне, что причиной была строка списка. Измените это с помощью List‹StringBuilder›, чтобы решить эту проблему. Но исключение продолжало появляться, просто не так быстро, поэтому я попытался заменить все строки на StringBuilder, потому что я читал, что строковые объекты не собираются сборщиком мусора .NET.   -  person Poku    schedule 19.02.2012
comment
Не могли бы вы также показать код для ImportJob? В этом методе нет XDocuments, поэтому я предполагаю, что любые проблемы, приводящие к XDocuments размером 51 МБ, скрыты там.   -  person Avner Shahar-Kashtan    schedule 19.02.2012
comment
StringBuilder избавит вас только от накладных расходов на объединение строк. Каждый раз, когда вы выполняете stringBuilder.ToString(), вы фактически создаете объект String, поэтому на самом деле вы ничего не оптимизировали. Это не то, что StringBuilders должны решить.   -  person Avner Shahar-Kashtan    schedule 19.02.2012


Ответы (6)


Как уже сказал Авнер Шахар-Каштан, я также думаю, что проблема в 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 вы можете уменьшить количество созданных ManualResetEvent (и их забытых Dispose())

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

Кстати, вы также должны увидеть CountdownEvent.

person L.B    schedule 19.02.2012
comment
Класс ImportFileJob делает много разных вещей, и класс XElement часто используется, поэтому вы правы, что проблема может быть здесь. Разве сборщик мусора .NET не должен очищать объект XElement? - person Poku; 20.02.2012
comment
причин может быть миллион. Я не знаю вашего кода. Но нет причин подозревать XElement или сборщик мусора, если вы не освобождаете ресурсы (например, файлы) - person L.B; 20.02.2012

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

Это возвращает массив. Если в каталогах столько файлов, сколько вы указали, это будут очень большие массивы, достаточно большие, чтобы их можно было поместить в кучу больших объектов. Несколько массивных массивов могут легко вызвать исключение OutOfMemoryException. Не помогает, что следующая строка

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

имеет переменную 'dir', которая ничего не делает. Большой массив создается дважды за выполнение метода.

person FrantzX    schedule 19.02.2012

Я сразу же могу заметить пару простых оптимизаций.

Вы используете много fullFileName.ToString().ToLower().EndsWith("ignored") вызовов. У них много накладных расходов, поскольку вы всегда берете заданную строку и создаете новую строку в нижнем регистре.

Вместо этого вы должны использовать перегрузки Endswith (или Contains), которые допускают сравнение без учета регистра:

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

Кроме того, я не думаю, что ваши StringBuilders помогают в этом случае. StringBuilders в основном полезны, когда вы создаете строки, состоящие из нескольких частей, и вам не нужны накладные расходы на создание нескольких промежуточных строк во время их составления. Кажется, что все ваши конкатенации строк здесь всегда используют только две строки - базовое имя и новый суффикс, поэтому я не уверен, что это на самом деле экономит ваше время или память.

person Avner Shahar-Kashtan    schedule 19.02.2012
comment
-1. Вы правы, но ничего из этого не должно вызывать этой проблемы — эти строки немедленно отбрасываются. - person TomTom; 24.02.2012

Вместо использования таймера и циклического просмотра всего содержимого папок вы можете использовать FileSystemWatcher: http://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.aspx

Таким образом, ваша программа уведомляется о точных файлах, которые были изменены, и вам даже не нужно выделять память для массивов файлов, которые вам не нужны.

person Jared Shaver    schedule 19.02.2012

Вы вызываете fullFileName.ToString().ToLower() три раза в своем выражении If. Кэшируйте это строковое значение в локальной переменной и используйте этот оператор if (сохраняет три временные строки).

Попробуйте использовать XmlWriter, а не XDocument. XDocument — это граф объектов в памяти, поэтому для больших наборов данных он может быть не самым производительным (вы держите все это в памяти, пока не запишете его на диск целиком). С XmlWriter вы обычно можете выполнять потоковую передачу в файловый буфер элемент за элементом, объем памяти будет гораздо меньше.

Не уверен, сколько работы требует каждый импорт, но пробовали ли вы поток для каждого каталога, а не для каждого файла?

person Community    schedule 19.02.2012

Как предположили другие,

1) уменьшить манипуляции со строками.

Похоже, ваш каталог возвращает «слишком много» имен файлов (строк), поэтому это требует внимания.

2) ваша строка «var dir = Directory.GetFiles(RawDataDirectory.FullName, "*.*", SearchOption.AllDirectories);» кажется избыточной. Похоже, вы его не используете. Итак, удалите этот код, он содержит много строковых ссылок.

3) ЕСЛИ возможно, перебрать файлы, возвращенные из каталога, частями (скажем, 10 КБ). Таким образом, для этого потребуется написать код, который разбивает список на список>, а затем очищает ссылки, содержащиеся во внутреннем списке, при повторении внешнего цикла. Что-то вроде,

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.
}

Надеюсь, это поможет.

person Manish Basantani    schedule 19.02.2012