Как заставить память релиза занят MemoryStream?

У меня есть следующий код:

const int bufferSize = 1024 * 1024;
var buffer = new byte[bufferSize];
for (int i = 0; i < 10; i++)
{
    const int writesCount = 400;
    using (var stream = new MemoryStream(writesCount * bufferSize))
    {
        for (int j = 0; j < writesCount; j++)
        {
            stream.Write(buffer, 0, buffer.Length);
        }
        stream.Close();
    }
}

который я запускаю на 32-битной машине.

Первая итерация заканчивается просто отлично, а затем на следующей итерации я получаю исключение System.OutOfMemoryException в строке, что new MemoryStream.

Почему предыдущая MemoryStream память не исправлена, несмотря на утверждение using? Как принудительно освободить память, используемую MemoryStream?

Ответы

Ответ 1

Я не думаю, что проблема заключается в сборщике мусора, который не выполняет свою работу. Если GC находится под давлением памяти, он должен запускаться и восстанавливать 400 MB, которые вы только что выделили.

Это более вероятно, пока GC не найдет смежный блок 400 МБ.

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

Вы должны прочитать запись в блоге Eric Lippert "Out Of Memory" не относится к физической памяти

Вам гораздо лучше сделать и ниже.

  • Повторное использование выделенного вами блока памяти (почему вы создаете другой с таким же размером)
  • Выделение гораздо меньших фрагментов (менее 85 КБ)

До Dotnet 4.5 Dotnet сконструировал две кучи, Маленькая куча объектов (SOH) и Большая куча объектов (LOH). См. Усовершенствования расширенного объекта в .NET 4.5 от Брэндона Брея. Ваш MemoryStream выделяется в LOH и не сжимается (дефрагментируется) в течение всего процесса, что делает его гораздо более вероятным, что несколько вызовов для выделения этого большого объема памяти вызовут OutOfMemoryException

CLR управляет двумя разными кучами для выделения, маленький объект кучу (SOH) и кучу больших объектов (LOH). Любое распределение больше чем или равно 85 000 байт, идет на LOH. Копирование больших объектов имеет штраф за производительность, поэтому LOH не уплотняется в отличие от SOH. Еще одна определяющая характеристика заключается в том, что LOH собирается только во время собрания поколения 2. Вместе они имеют встроенный предположение о том, что большие распределения объектов нечасты.

Ответ 2

Попробуйте принудительно собрать сборку мусора, если вы уверены, что необходимо очищать объекты без ссылок.

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Другой альтернативой является использование Stream с внешним хранилищем: FileStream, например.

Но в общем случае было бы лучше использовать один достаточно маленький буфер (массив, выделенный один раз) и использовать его для вызовов чтения/записи. Избегайте наличия большого количества объектов в .NET(см. CLR Inside Out: Неверная большая куча объектов).

Обновление

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

const int bufferSize = 1024 * 1024;
const int writesCount = 400;

byte[] streamBuffer = new byte[writesCount * bufferSize];
byte[] buffer = new byte[bufferSize];
for (int i = 0; i < 10; i++)
{
    using (var stream = new MemoryStream(streamBuffer))
    {
        for (int j = 0; j < writesCount; j++)
        {
            stream.Write(buffer, 0, buffer.Length);
        }
    }
}

Ответ 3

Похоже, вы слишком много выделяете, чем может обрабатывать ваша система. Ваш код отлично работает на моей машине, но если я его изменил следующим образом:

const int bufferSize = 1024 * 1024 * 2;

Я получаю ту же ошибку, что и вы.

Но если я изменю целевой процессор на x64, тогда код будет работать, что кажется логичным, так как вы можете решить намного больше памяти.

Подробное объяснение по этой статье: http://www.guylangston.net/blog/Article/MaxMemory И некоторая информация по этому вопросу: Максимальная память, которую может выделить .NET-процесс

Ответ 4

Прежде всего, Dispose() не гарантирует, что память будет выпущена (она не помещает объекты для коллекции GC, в случае MemoryStream - она ​​ничего не выпускает, поскольку MemoryStream не имеет неуправляемых ресурсов). Единственный надежный способ освободить память, используемый MemoryStream, - это потерять все ссылки на него и дождаться появления сборки мусора (и если у вас есть OutOfMemoryException - сборщик мусора уже пытался, но не смог освободить достаточно памяти). Кроме того, выделение таких больших объектов (что-то > 85000 байт) имеет некоторые последствия - эти объекты идут в кучу больших объектов (LOH), которые могут быть фрагментированы (и не могут быть уплотнены). Поскольку объект .NET должен занимать непрерывную последовательность байтов, это может привести к ситуации, когда у вас достаточно памяти, но нет места для большого объекта. Сборщик мусора в этом случае не поможет.

Похоже, основная проблема здесь заключается в том, что ссылка на объект stream хранится в стеке, предотвращая сбор мусора объекта stream (даже принудительная сборка мусора не поможет, поскольку GC считает, что объект все еще жив, вы можете проверить это, создав для него WeakRefrence). Рефакторинг этого образца может исправить:

    static void Main(string[] args)
    {
        const int bufferSize = 1024 * 1024 * 2;
        var buffer = new byte[bufferSize];
        for(int i = 0; i < 10; i++)
        {
            const int writesCount = 400;
            Write(buffer, writesCount, bufferSize);
        }
    }

    static void Write(byte[] buffer, int writesCount, int bufferSize)
    {
        using(var stream = new MemoryStream(writesCount * bufferSize))
        {
            for(int j = 0; j < writesCount; j++)
            {
                stream.Write(buffer, 0, buffer.Length);
            }
        }
    }

Вот пример, который доказывает, что объект не может быть собран с помощью мусора:

    static void Main(string[] args)
    {
        const int bufferSize = 1024 * 1024 * 2;
        var buffer = new byte[bufferSize];
        WeakReference wref = null;
        for(int i = 0; i < 10; i++)
        {
            if(wref != null)
            {
                // force garbage collection
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                // check if object is still alive
                Console.WriteLine(wref.IsAlive); // true
            }
            const int writesCount = 400;
            using(var stream = new MemoryStream(writesCount * bufferSize))
            {
                for(int j = 0; j < writesCount; j++)
                {
                    stream.Write(buffer, 0, buffer.Length);
                }
                // weak reference won't prevent garbage collection
                wref = new WeakReference(stream);
            }
        }
    }