← Назад к вопросам

Какие есть способы переполнить кучу?

3.0 Senior🔥 82 комментариев
#Основы C# и .NET#Память и Garbage Collector

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Способы переполнения кучи (Heap Overflow) в C#/.NET

В C# управление памятью осуществляется через сборщик мусора (Garbage Collector, GC), который автоматически управляет кучей. Однако существуют сценарии, приводящие к чрезмерному потреблению памяти или фактическому переполнению кучи. Рассмотрим основные причины и паттерны.

1. Неограниченный рост коллекций

Наиболее частая причина — постоянное добавление объектов в коллекции без их очистки.

public class HeapOverflowExample
{
    private static List<byte[]> _data = new List<byte[]>();

    public static void CauseOverflow()
    {
        while (true)
        {
            // Каждая итерация добавляет новый массив в список
            _data.Add(new byte[10_000_000]); // 10 МБ
            Thread.Sleep(100); // Задержка для имитации "медленной" утечки
        }
    }
}

Проблема: объекты остаются в корне коллекции, GC не может их собрать → память растет до OutOfMemoryException.

2. Циклические ссылки в объектах

В управляемой среде циклические ссылки не являются проблемой для GC (в отличие от C++), так как он использует алгоритм маркировки. Однако если такие объекты удерживаются корневыми ссылками, они не будут собираться.

public class Node
{
    public Node Next { get; set; }
    public byte[] Data { get; set; }

    public Node(byte[] data) => Data = data;
}

public static void CreateCycles()
{
    Node first = new Node(new byte[100_000_000]);
    Node current = first;

    // Создаем цепочку объектов
    for (int i = 0; i < (int)1e6; i++)
    {
        current.Next = new Node(new byte[10_000]);
        current = current.Next;
    }
    // first остается в стеке → вся цепочка жива
}

3. Неправильное использование кэширования

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

public class CacheOverflow
{
    private static MemoryCache _cache = MemoryCache.Default;

    public static void FillCache()
    {
        for (long i = 0; i < long.MaxValue; i++)
        {
            _cache.Set(i.ToString(), new byte[1_000_000], DateTimeOffset.MaxValue);
        }
    }
}

Решение: задавать MemoryCache ограничения (CacheItemPolicy с SlidingExpiration или AbsoluteExpiration).

4. Большие объекты и фрагментация LOH

LOH (Large Object Heap) — сегмент кучи для объектов >85 000 байт. LOH не компактируется по умолчанию (до .NET 4.5.1), что приводит к фрагментации.

public void FragmentLOH()
{
    // Чередуем большие и маленькие объекты
    for (int i = 0; i < 10000; i++)
    {
        byte[] large = new byte[100_000]; // Попадает в LOH
        byte[] small = new byte[100];     // Попадает в SOH
    }
    // После сборки мусора в LOH остаются "дыры"
}

Итог: при последующих аллокациях больших объектов может не найтись непрерывного блока памяти → OutOfMemoryException.

5. Утечки через обработчики событий

Объекты подписываются на события статических или долгоживущих объектов, но не отписываются.

public class EventSource
{
    public static event EventHandler<EventArgs> StaticEvent;
}

public class Subscriber
{
    public Subscriber() => EventSource.StaticEvent += OnEvent;
    private void OnEvent(object sender, EventArgs e) { }
}

// Каждый созданный Subscriber будет жить вечно
// из-за ссылки из статического события

6. Finalizer-ы и очередь финализации

Объекты с финализаторами (~ClassName) живут дольше, так как попадают в очередь финализации после пометки как недостижимые.

public class ResourceHolder
{
    private byte[] _data = new byte[100_000];

    ~ResourceHolder() { } // Финализатор
}

// При массовом создании таких объектов они долго не освобождают память

7. Неуправляемые ресурсы (P/Invoke, нативные аллокации)

Вызов неуправляемого кода может выделять память вне управления GC.

[DllImport("kernel32.dll")]
static extern IntPtr HeapAlloc(IntPtr hHeap, uint flags, UIntPtr size);

public static void AllocateNative()
{
    IntPtr ptr = HeapAlloc(GetProcessHeap(), 0, (UIntPtr)100_000_000);
    // Если не вызывать HeapFree — неуправляемая утечка
}

8. StackOverflowException как триггер

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

9. ASP.NET-специфичные сценарии

  • Кэширование ответов без ограничений.
  • Статические коллекции в приложении, куда добавляются данные сессий.
  • Неосвобождаемые зависимости (например, DbContext в EF Core не вызывается Dispose).

Как предотвратить переполнение кучи?

  1. Профилирование памяти с помощью dotnet-counters, dotnet-dump, Visual Studio Diagnostic Tools.
  2. Использование слабых ссылок (WeakReference) для кэшей.
  3. Ограничение размеров коллекций и кэшей.
  4. Правильная реализация IDisposable для ресурсов.
  5. Отписка от событий при уничтожении объектов.
  6. Избегание финализаторов, где возможно.
  7. Вызов GC.Collect() в критических местах (редко, только при четком понимании).
  8. Использование ArrayPool<T> для уменьшения нагрузки на GC.

Переполнение кучи в C# — обычно следствие логических ошибок, а не низкоуровневого управления памятью. Ключ к предотвращению — понимание модели памяти .NET и регулярный анализ использования памяти в приложении.