Очистка памяти после чтения гигантского значения элемента XML

Я редко обращаюсь за помощью, но это сводит меня с ума: я читаю xml-файл, который оборачивает произвольное количество элементов, каждый из которых имеет файл в кодировке b64 (и некоторые сопутствующие метаданные для него). Первоначально я просто прочитал весь файл в XmlDocument, но в то время как это был намного более чистый код, я понял, что нет никаких ограничений на размер файла, и XmlDocument ест много памяти и может закончиться, если файл достаточно велик. Поэтому я переписал код, чтобы использовать вместо XmlTextReader, который прекрасно работает, если проблема в том, что программе был отправлен XML-файл с большим количеством вложений разумного размера... но все еще есть большая проблема, и вот к чему я обращаюсь:

Если мой xml-ридер находится в элементе File, этот элемент содержит огромное значение (скажем, 500 МБ), и я вызываю reader.ReadElementContentAsString()Теперь у меня есть строка, которая занимает 500 МБ (или, возможно, исключение OutOfMemoryException). В любом случае я хотел бы просто записать в журнал "это вложение файла было слишком большим, мы собираемся игнорировать его и двигаться дальше", а затем перейти к следующему файлу. Но не похоже, что строка, которую я только что пытался прочитать, когда-либо собиралась мусором, так что на самом деле происходит то, что строка занимает всю оперативную память, и любой другой файл, который она пытается прочитать после этого, также вызывает исключение OutOfMemoryException, хотя большинство файлов будет довольно мало.

Напомним: в этот момент я считываю значение элемента в локальную строку, поэтому я ожидал, что он сразу же будет иметь право на сборку мусора (и что таким образом он будет собираться не позднее, когда программа попытается прочитайте следующий элемент и обнаружите, что у него нет свободной памяти). Но я попробовал все, на всякий случай: установив строку в null, вызвав явный GC.Collect()... без всяких проблем, диспетчер задач указывает, что сборщик мусора собрал только около 40 КБ, из ~500 МБ, которые он только что запросил для сохранения строки, и я все еще получаю исключения из памяти, пытаясь прочитать что-то еще.

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

Я прочитал немного о C# GC и LOH, но ничего, что я прочитал, не указало бы мне, что это произойдет...

Дайте мне знать, если вам нужна дополнительная информация, и спасибо!

редактировать: я понял, что процесс работал как 32-разрядный процесс, что означало, что он нуждался в памяти немного больше, чем следовало бы. Исправлено, это становится меньшей проблемой, но я все еще хочу исправить это поведение. (Требуется больше и / или больше файлов, чтобы достичь точки, где выбрасывается исключение OutOfMemoryException, но, как только оно выбрасывается, я все еще не могу восстановить эту память своевременно.)

3 ответа

Решение

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

Тогда я использовал XDocument вместо XmlDocument, и это помогло мне.

Вы можете использовать метод XmlReader.ReadValueChunk, чтобы читать содержимое элемента по одному "чанку" за раз, вместо того, чтобы пытаться прочитать все содержимое сразу. Таким образом, в какой-то момент вы можете решить, что данные слишком велики, а затем проигнорировать их и записать событие в журнал. StringBuilder это, вероятно, лучший способ объединить собранные фрагменты массива char в одну строку.

Если вы хотите освободить память с GC.Collect(), вы можете принудительно завершить работу и освободить память с помощью GC.WaitForPendingFinalizers(), Это может повлиять на производительность (или даже зависнуть, см. Описание за ссылкой), но вы должны избавиться от больших объектов, если у вас больше нет живых ссылок на них (то есть локальные переменные уже находятся вне области действия или их значения установлен на ноль) и продолжить работу в обычном режиме. Конечно, вы должны использовать это как последнее средство, когда потребление памяти является проблемой, и вы действительно хотите принудительно избавиться от лишних выделений памяти.

Я успешно использовал GC.Collect();GC.WaitForPendingFinalizers(); Комбинация в чувствительной к памяти среде позволяет поддерживать объем памяти приложения ниже 100 МБ, даже когда он читает некоторые действительно большие файлы XML (>100 МБ). Для повышения производительности я также использовал Process.PrivateMemorySize64 отслеживать потребление памяти и форсировать завершения только после достижения определенного предела. До моих улучшений потребление памяти иногда превышало 1 ГБ!

Я не уверен, что это так, но я думаю, что вам нужно избавиться от XmlTextReader, Сохраните xmlpath узла после чрезмерно большого узла в строку, задайте для вашей массивной строки значение null, а затем избавьтесь от XmlTextReader и снова откройте его в узле после большого узла. Из того, что я понимаю, если вы установите строку nullили это выходит из области видимости, GC должен освободить эту память как можно скорее. Мне кажется более вероятным, что вы освобождаете строку, но продолжаете выполнять операции с XmlTextReader который теперь держится за тонну памяти.

Еще одна идея, которая пришла в голову, была попытка сделать это в unsafe блокировать, а затем явно освобождать память, однако, это не выглядит возможным (кто-то еще может знать, но, посмотрев немного, кажется, что небезопасный блок все еще GC'd, он просто дает вам указатели). Еще один вариант, хотя и ужасный, но будет сделать dll для разбора в C или C++ и вызвать его из вашего проекта C#.

Попробуйте первое предложение, прежде чем делать что-нибудь сумасшедшее, как последнее:)

Другие вопросы по тегам