Очистка памяти после чтения гигантского значения элемента 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#.
Попробуйте первое предложение, прежде чем делать что-нибудь сумасшедшее, как последнее:)