Подтвердить что ты не робот

Тайна сборщика мусора .NET

В моей работе у нас была проблема с OutOfMemoryExceptions. Я написал простой фрагмент кода, чтобы подражать некоторому поведению, и у меня закончилась следующая тайна. Посмотрите на этот простой код, который взрывается, когда заканчивается память.

class Program
{
    private static void Main()
    {
        List<byte[]> list = new List<byte[]>(200000);
        int iter = 0;

        try
        {
            for (;;iter++)
            {
                list.Add(new byte[10000]);
            }
        }
        catch (OutOfMemoryException)
        {
            Console.WriteLine("Iterations: " + iter);
        }
    }
}

На моей машине это закончилось с

Iterations: 148008

Затем я добавил вызов GC.Collect в цикл после каждой тысячи итераций:

            //...
            for (;;iter++)
            {
                list.Add(new byte[10000]);

                if (iter % 1000 == 0)
                    GC.Collect();
            }
            //...

И с удивлением:

Iterations: 172048

Когда я назвал GC.Collect после каждых 10 итераций, я даже получил 193716 циклов. Есть две странные вещи:

  • Как может ручное обращение к GC.Collect иметь такое серьезное воздействие (на 30% больше выделено)?

  • Что, черт возьми, может собрать GC, когда нет "потерянных" ссылок (я даже задал емкость списка)?

4b9b3361

Ответ 1

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

Я пытаюсь запустить несколько тестов, но моя машина не может их обработать. Попробуйте это, он сообщит GC о том, чтобы привязать объекты в памяти, чтобы они не перемещались вокруг

byte[] b = new byte[10000];
GCHandle.Alloc(b, GCHandleType.Pinned);
list.Add(b);

Что касается вашего комментария, когда GC перемещается вокруг, он ничего не вытирает, он просто лучше использует все пространство памяти. Давайте попробуем и упростим это. Когда вы выделяете свой байтовый массив в первый раз, скажем, что он вставлен в память с места от 0 до 10000. В следующий раз, когда вы выделяете массив байтов, он не должен начинаться с 10001, он может начинаться с 10500. Так что теперь у вас есть 499 байт, которые не используются и не будут использоваться вашим приложением. Поэтому, когда GC выполняет уплотнение, он переместит массив 10500 в 10001, чтобы иметь возможность использовать дополнительные 499 байт. И снова это упрощает.

Ответ 2

В зависимости от используемой среды CLR могут возникать некоторые проблемы с крупными объектами.

Взгляните на эту статью, в которой объясняются проблемы с большими выделениями блоков (а список из 200 000 элементов - это большой блок, а другой может быть или не быть, некоторые массивы, похоже, помещаются в LOH, когда они достигают 8k, другие после 85k).

http://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/

Ответ 3

CLR иногда помещает массивы на LOH. Если вы когда-нибудь посмотрите на дамп памяти через WinDbg, вы увидите, что есть массивы, размер которых не превышает 85 000 байт. Является недокументированным поведением, но это так, как оно работает.

Вы получаете OutOfMemoryErrors, потому что вы фрагментируете кучу LOH, а куча LOH никогда не уплотняется.

Что касается вашего вопроса:

2) Что, черт возьми, может собрать GC, когда нет "потерянных" ссылок (я даже задал емкость списка)?

Утеряны ссылки на new byte[10000], которые вы передаете для добавления в список. Копия массива производится и передается в список. Для каждой итерации в цикле вы создаете новый байт [] с заданным размером 10000. CLR внутренне сохраняет ссылку на переменную, которая является локальной только для области действия, если цикл. На каждой итерации переменная выходит за пределы области видимости и будет собрана при следующем запуске GC.

Ответ 4

У меня была аналогичная проблема в .NET с той разницей, что у моего байта [] были случайные размеры.

Я попробовал два способа:

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

  • использовать файл с отображением памяти (на мой взгляд лучшее решение)

Если возможно, вы можете попробовать .NET 4.5 http://blogs.msdn.com/b/dotnet/archive/2012/07/20/the-net-framework-4-5-includes-new-garbage-collector-enhancements-for-client-and-server-apps.aspx