Как работает функция OutOfMemoryException?

Я немного смущен тем, что мы можем просто поймать OutOfMemoryException используя блок try/catch.

Учитывая следующий код:

Console.WriteLine("Starting");

for (int i = 0; i < 10; i++)
{
    try
    {
        OutOfMemory();
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception.ToString());
    } 
}

try
{
    StackOverflow();
}
catch (Exception exception)
{
    Console.WriteLine(exception.ToString());
}

Console.WriteLine("Done");

Методы, которые я использовал для создания OutOfMemory + StackOverflowException:

public static void OutOfMemory()
{
    List<byte[]> data = new List<byte[]>(1500);

    while (true)
    {
        byte[] buffer = new byte[int.MaxValue / 2];

        for (int i = 0; i < buffer.Length; i++)
        {
            buffer[i] = 255;
        }

        data.Add(buffer);
    }
}

static void StackOverflow()
{
    StackOverflow();
}

Он печатает OutOfMemoryException 10 раз, а затем завершается из-за OutOfMemoryException StackOverflowException, с которым он не может справиться.

График RAM выглядит так, как при выполнении программы: graph showing that memory gets allocated and released 10 times

Мой вопрос теперь, почему мы можем поймать OutOfMemoryException? После этого мы можем просто выполнить любой код, который мы хотим. Как доказано графиком ОЗУ, выпущена память. Как среда выполнения знает, какие объекты она может GC и которые все еще необходимы для дальнейшего выполнения?

Ответ 1

GC проводит анализ ссылок, которые используются в программе, и может отбросить любой объект, который нигде не используется.

OutOfMemoryException не означает, что память полностью исчерпана, это просто означает, что сбой памяти не удался. Если вы попытались одновременно выделить большую область памяти, все равно останется много свободной памяти.

Когда для распределения не хватает свободной памяти, система делает сборку мусора, чтобы попытаться освободить память. Если памяти не хватает для выделения, она будет генерировать исключение.

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

Ответ 2

Исключение OutOfMemoryException вполне возможно, потому что вы используете 32-битную программу, с графиком памяти, на котором вы не указали, сколько у него системы, поэтому, возможно, попробуйте создать его как 64-разрядную версию и, возможно, использовать MemoryFailPoint, чтобы предотвратить это в любом случае.

Вы также можете сообщить нам, что находится в функции OutOfMemory() для более четкого изображения.

PS StackOverFlow - единственная ошибка, с которой невозможно справиться.

Изменение: как упоминалось выше, и я думал, что это логично и, следовательно, не упоминал об этом раньше, если вы, например, попытаетесь выделить больше памяти, чем у вас есть "запасной", тогда это невозможно сделать, и возникает исключение. Поскольку вы распределяете большие массивы с вашими данными. ADD() падает до того, как происходит заключительное "незаконное" добавление, следовательно, есть свободная память.

Поэтому я бы предположил, что именно в этот момент data.Add(buffer); проблема возникает во время построения массива, когда вы преодолеваете ограничение на 2 ГБ процесса, добавив массив байтов размером 400 Мбайт в "данные", например массив из около 1 миллиарда объектов в 4 байта, который я ожидал бы около 400 МБ.

PS До тех пор, пока распределение максимальной памяти.net 4.5 max не станет равным 2 ГБ, после того, как доступно 4.5 больше.

Ответ 3

Не уверен, отвечает ли это на ваш вопрос, но (упрощенное) объяснение того, как он решает, какие объекты очищать:

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

Затем он отмечает объекты следующего уровня, что означает все объекты, отмеченные всеми полями ранее отмеченных объектов. Этот шаг повторяется до тех пор, пока не будут отмечены новые объекты.

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

В вашем случае, если объекты, которые вы выделили для добавления давления в диспетчер памяти, не сохраняются по ссылке, это означает, что GC будет иметь возможность их очистить. Кроме того, имейте в виду, что OutOfMemoryException относится к управляемой памяти вашей программы CLR, в то время как GC работает немного за пределами этого "окна".

Ответ 4

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

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

Ответ 5

OutOfMemory() создает структуру данных (List<byte[]>), которая является локальной для области метода. Хотя ваш поток выполнения находится внутри метода OutOfMemory, текущий стек стека считается корнем GC для списка. Как только ваш поток попадает в блок catch, стек стека выталкивается, и список становится недоступным. Поэтому сборщик мусора определяет, что он может безопасно собирать список (который он делает, как вы видели на графике памяти).