Загадка сборщика мусора .NET - PullRequest
28 голосов
/ 13 ноября 2009

В моей работе у нас была проблема с 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 циклов. Есть две странные вещи:

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

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

Ответы [ 4 ]

11 голосов
/ 13 ноября 2009

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

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

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

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

5 голосов
/ 13 ноября 2009

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

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

http://www.simple -talk.com / DotNet / .net-каркасные /-опасности-оф-большого объекта кучи /

2 голосов
/ 08 декабря 2011

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

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

По вашему вопросу:

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

Существуют перезаписанные ссылки на new byte[10000], которые вы передаете, чтобы добавить в список. Локальная переменная компилируется и присваивается new byte[10000]. Для каждой итерации в цикле вы создаете новый byte [] с предопределенным размером 10000, и он назначается локальной переменной. Любое предыдущее значение для переменной будет перезаписано, и эта память будет пригодна для сбора при следующем запуске GC для генерации, в которой живет переменная (в данном случае, возможно, LOH).

0 голосов
/ 19 июня 2012

У меня была похожая проблема в .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

...