Pull to refresh

.NET Framework. Memory management

Reading time11 min
Views22K
В данной статье рассматриваются некоторые моменты по работе с памятью в .NET Framework. Статья описывает работу GC, как GC контролирует свои хип, режимы работы GC. Приведены примеры, по использованию памяти в обход GC. Я изложил не только легко доступную информацию, ну и ту, что доступна только при изучении дампов приложений, написанных на .NET. Надеюсь статья получилась информативной и не очень скучной. Следующая статья будет про загрузчик, JIT, и его структур данных, таких как Method Tables, Method Descriptors и EEClass.

Теория


Неуправляемая память


Виртуальная память — логическое представление памяти, которая не обязательно отражается на физическую память процесса. На 32х битных операционных системах на процессы выделяется 4Gb виртуального адресного пространства. По умолчанию 2Gb на пользовательский режим. Мы сконцентрируем внимание именно на пользовательском режиме, а не на режиме ядра.
Когда процессу нужно выделить память, он сначала должен её зарезервировать, потом должен зафиксировать память. Этот процесс резервирования/фиксации может быть выполнен в один или два шага в зависимости от того, как вы используете API для манипуляции с виртуальной памятью.
Обычно зарезервированная память разделяется на следующие части:
  • Потоки (и стеки);
  • Файлы .dll;
  • Виртуальные выделения памяти;
  • NT хип;
  • Хипы менеджеров памяти, такие как .NET GC.

Управляемая память выделяется частями. Выделения памяти может быть по 16, 32 и 64Kb. Они должны быть выделены в непрерываемых блоках, и если нет регионов памяти достаточных для выделения, процесс выкидывает OutOfMemoryException. Если процесс не может выполнить сборку мусора, не хватает памяти для внутренних структур сборщика мусора, процесс будет аварийно завершён.
Вы должны исследовать приложение и избегать даже не фатальных OutOfMemoryException исключений, потому, что процесс может быть в нестабильном состоянии после этого исключения.
Роль менеджеров памяти пользовательского режима является в том, чтобы управлять резервированием виртуальной памяти. Менеджеры памяти используют несколько алгоритмов, чтобы достичь разные цели, при управлении памятью, такие как малая фрагментация, резервирование больших блоков.
Перед тем, как процесс может использовать память, он должен зарезервировать и провести фиксацию хотя бы части памяти. Эти 2 шага можно выполнить с помощью фунций API VirtualAlloc, VirtualAllocEx. Освободить память можно с помощью VirtualFree или VirtualFreeEx. Вызов последних функций изменит состояние блоков памяти с “зафиксированных” в “свободные”.
Воруем идеи у разработчиков на c++. Бывают такие ситуации, когда использовать GC (о нём мы поговорим позже) по причинам интенсивной работы с памятью не представляется возможным. Такие ситуации бывают редко, и возникают при специфических ограничениях. В данном случае можно реализовать malloc и free. Malloc – делает вызов к HeapAlloc, а free – это вызов к HeapFree. Создать свой хип можно вызовом HeapCreate. Полный перечень функций по работе с памятью в среде Windows вы можете найти по адресу — Memory Management Functions.
Я не разработчик под linux, поэтому чем заменить вызовы этих API функций я не могу сказать. Тем, кому также будет необходимо использование этих функций, предлагаю реализовать это с помощью паттерна Abstract factory pattern, даже если вы и не собираетесь переносить своё приложение в ближайшее время на платформу Mono. Использование этих функций вообще не очень корректно, так как это создаёт некоторые сложности с переносимостью, но в некоторых очень специфических ситуациях, чтобы уменьшить давление на GC приходится это использовать.

Управляемая память


Память процесса состоит из:
  • Управляемые хипы — сохранение всех управляемых объектов, которых ещё не собрал GC;
  • Хипы загрузчика – динамические модули и сохранение для JIT скомпилированных объектов, такие как таблицы методов, описания методов и EEClass (Следующая статья будет посвящена JITу и его структур);
  • Native хипы – хипы для native памяти;
  • Память используемая для потоков, их стеков и регистров;
  • Память для хранения native файлов dll, а также для native частей управляемых файлов dll;
  • Другие виртуальные выделения памяти, не подходящие не под одну категорию описанную выше;
  • Ещё не используемые участки памяти.

Ну вот мы подошли к хипу GC. Сборщик мусора (GC) выделяет и освобождает память для управляемого кода. GC использует VirtualAlloc чтобы зарезервировать участок памяти для своего хипа. Размер хипа зависит от режима GC, и версии .NET Framework. Размер может быть 16, 32 или 64Мб. Хип GC это неразрывный блок виртуальной памяти изолированный от других хипов процесса и управляемый .NET Runtime. Интересно, что GC не фиксирует сразу весь участок памяти, а только по мере его роста. GC отслеживает следующий свободный адрес в конце управляемого хипа, и запрашивает следующий блок памяти, если потребуется, начиная с него. Отдельный хип создаётся для больших объектов (.NET Framework 2.0, в 1.1 большие объекты находятся в том же хипе, в котором находятся поколения, но в другом сегменте). Большой объект – объект больше 85000 байт.
Сосредоточимся на работе GC. GC использует 3 поколения (0, 1, 2) и LOH (Хип для больших объектов). Созданные объекты попадают в нулевое поколение. Как только размер нулевого поколения достигнет пороговой величины (больше нет для него памяти в сегменте) и создания нового объекта не представляется возможным, в нулевом поколении начинается сбор мусора. Если в сегменте первого поколения происходит нехватка памяти, то сборка мусора будет для первого поколения, и для нулевого. При сборке мусора для 2го поколения, произойдёт также сборка мусора для первого и нулевого поколения.
Объекты выжившие после сборки мусора в нулевом поколении переходят в первое, из первого во второе. Исходя из вышесказанного, крайне не рекомендуется в ручную вызывать сборку мусора, так как это может сильно сказаться на производительности вашего приложения (ищу примеры, когда вызов сборки мусора является корректным, прошу написать в комментарии, для обсуждения этого вопроса).
Для больших объектов нет поколений. В новых .NET Frameworkах начиная с 1.1 если удаляется объект, который находится в конце хипа, приводит к тому, что Private Bytes процесса уменьшается.
Очень интересный вопроc работы GC — Что собственно происходит при сборке мусора? Несколько шагов выполняет GC вне зависимости от того, происходит ли сборка мусора, для 0, 1, 2 поколения или полная сборка мусора. Итак, этапы сборки мусора:
  • Начальная стадия GC – ждёт пока все управляемы потоки достигают точки в выполнении, когда безопасно их приостановить;
  • Объекты, которые не имеют ссылок помечаются как готовые к удалению;
  • GC планирует размеры сегментов для поколений и оценивает уровень фрагментации в хипе после выполнения сборки мусора;
  • Удаляет объекты, помеченные для удаления. Заносит эти адреса объектов в список адресов свободного пространства, если GC некомпактный;
  • Перемещает объекты в младшие адреса управляемого хипа. Наиболее дорогая операция;
  • Возобновление управляемых потоков.

Режимы работы GC:
  • Конкурирующий – создан для GUI приложений, когда важно время отклика. Приостанавливает выполнения приложения несколько раз в процессе сборки мусора давая ему процессорное время, для выполнения. GC использует один хип и один поток;
  • Неконкурирующий – приостаналивает приложение до полного выполнения сборки мусора. GC использует один хип и один поток;
  • Серверный – максимальная производительность на машине с несколькими процессорами или ядрами. GC использует один хип на процессор, а также один поток на ядро.

Как включить нужный режим работы GC. В файле конфигурации секция configuration/runtime:
  • Конкурирующий – <gcConcurrent = true>;
  • Неконкурирующий – <gcConcurrent = false>;
  • Серверный – <gcServer Enabled = true>.

Увеличение производительности GC сводится к решению следующих проблем:
  • Частое и большое выделение объектов – заставляет GC чаще собирать мусор. Частое выделение объектов большого размера могут служить причиной большой загрузки процессора, так как сборка мусора LOH вызывает сборку мусора во втором поколении;
  • Выделение памяти заранее – создаёт несколько проблем. Во первых заставляет GC чаще собирать мусор, во вторых позволяет объектам пережить сборку;
  • Много предков или указателей – при перемещении объектов и уменьшении фрагментации все указатели должны будут изменены, в соответствии с новыми адресами объектов. Объекты могут быть при уменьшении фрагментации разнесены в разные стороны сегмента в хипе. Это всё может негативно сказаться на скорости выполнения;
  • Очень много объектов, которые успевают попасть в следующее поколение, и не живут там долго – объекты, которые переживают сборку мусора, попадают в следующее поколение, но долго там не задерживаются. Они создают давление на следующее поколения, и могут вызвать дорогостоящую операцию сборки мусора в этом поколении;
  • Объекты, которые нельзя перемещать (GCHandleType.Pinned, Interop, fixed) – увеличивает фрагментацию памяти поколений и GC может дольше искать непрерывную область памяти для нового объекта.

GC использует ссылки, чтобы определить возможно ли освобождение памяти занимаемого объектом. Перед тем как выполнить сборку мусора, GC начинает с предков и идёт вверх по ссылкам строя их в виде дерева. При помощи списка ссылок на все объекты он определяет недоступные и готовые к сборке мусора объекты.
Есть несколько типов ссылок:
  • Сильная ссылка. Существующая ссылка на “живой” объект. Это предотвращает объект от сборки мусора;
  • Слабая ссылка. Существующая ссылка на “живой” объект, но позволяющая удалить тот объект, на который ссылается при сборке мусора. Этот тип ссылок может быть использован например для системы кеширования.

Хотел написать про финализацию, но информации по нему полно. Единственно можно отметить, что к каждому процессу добавляется дополнительный поток финализации, и процесс можно подвесить, если с помощью блокировок заблокировать этот поток, и крах потока финализации ведёт начиная с .NET Framework 2.0 к краху всего приложения, в предыдущих версиях будет рестарт потока.

Немного примеров по работе с памятью без GC


using System;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security;

namespace TestApplication
{
    #region NativeMethods

    internal static class NativeMethods
    {
        #region Virtual memory

        #region VirtualAlloc

        [Flags()]
        public enum AllocationType : uint
        {
            MEM_COMMIT = 0x1000,
            MEM_RESERVE = 0x2000,
            MEM_RESET = 0x80000,
            [Obsolete("Windows XP/2000:  This flag is not supported.")]
            MEM_LARGE_PAGES = 0x20000000,
            MEM_PHYSICAL = 0x400000,
            MEM_TOP_DOWN = 0x100000,
            [Obsolete("Windows 2000:  This flag is not supported.")]
            MEM_WRITE_WATCH = 0x200000,
        }

        [Flags()]
        public enum MemoryProtection : uint
        {
            PAGE_EXECUTE = 0x10,
            PAGE_EXECUTE_READ = 0x20,
            PAGE_EXECUTE_READWRITE = 0x40,
            [Obsolete("This flag is not supported by the VirtualAlloc or VirtualAllocEx functions. It is not supported by the CreateFileMapping function until Windows Vista with P1 andWindows Server 2008.")]
            PAGE_EXECUTE_WRITECOPY = 080,
            PAGE_NOACCESS = 0x01,
            PAGE_READONLY = 0x02,
            PAGE_READWRITE = 0x04,
            [Obsolete("This flag is not supported by the VirtualAlloc or VirtualAllocEx functions.")]
            PAGE_WRITECOPY = 0x08,
            PAGE_GUARD = 0x100,
            PAGE_NOCACHE = 0x200,
            PAGE_WRITECOMBINE = 0x400,
        }

        [DllImport(
            "kernel32.dll",
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.Winapi)]
        internal static extern IntPtr VirtualAlloc(
            IntPtr lpAddress,
            IntPtr dwSize,
            AllocationType flAllocationType,
            MemoryProtection flProtect);

        #endregion

        #region VirtualFree

        [Flags()]
        public enum FreeType : uint
        {
            MEM_DECOMMIT = 0x4000,
            MEM_RELEASE = 0x8000,
        }

        [DllImport(
            "kernel32.dll",
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.Winapi)]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool VirtualFree(
            IntPtr lpAddress,
            IntPtr dwSize,
            FreeType dwFreeType);

        #endregion

        #endregion

        #region Heap

        #region HeapCreate

        [Flags()]
        public enum HeapOptions : uint
        {
            Empty = 0x00000000,
            HEAP_CREATE_ENABLE_EXECUTE = 0x00040000,
            HEAP_GENERATE_EXCEPTIONS = 0x00000004,
            HEAP_NO_SERIALIZE = 0x00000001,
        }

        [DllImport(
            "kernel32.dll",
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.Winapi)]
        internal static extern IntPtr HeapCreate(
            HeapOptions flOptions,
            IntPtr dwInitialSize,
            IntPtr dwMaximumSize);

        #endregion

        #region HeapDestroy

        [DllImport(
            "kernel32.dll",
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.Winapi)]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool HeapDestroy(
            IntPtr hHeap);

        #endregion

        #region HeapAlloc

        [Flags()]
        public enum HeapAllocFlags : uint
        {
            Empty = 0x00000000,
            HEAP_GENERATE_EXCEPTIONS = 0x00000004,
            HEAP_NO_SERIALIZE = 0x00000001,
            HEAP_ZERO_MEMORY = 0x00000008,
        }

        [DllImport(
            "kernel32.dll",
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.Winapi)]
        internal static extern unsafe void* HeapAlloc(
            HeapHandle hHeap,
            HeapAllocFlags dwFlags,
            IntPtr dwBytes);

        #endregion

        #region HeapFree

        [Flags()]
        public enum HeapFreeFlags : uint
        {
            Empty = 0x00000000,
            HEAP_NO_SERIALIZE = 0x00000001,
        }

        [DllImport(
            "kernel32.dll",
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.Winapi)]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern unsafe bool HeapFree(
            HeapHandle hHeap,
            HeapFreeFlags dwFlags,
            void* lpMem);

        #endregion

        #endregion
    }

    #endregion

    #region Memory handles

    #region VirtualMemoryHandle

    internal sealed class VirtualMemoryHandle : SafeHandle
    {
        public VirtualMemoryHandle(IntPtr handle, IntPtr size)
            : base(handle, true)
        {
            Size = size;
        }

        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        override protected bool ReleaseHandle()
        {
            return NativeMethods.VirtualFree(
                handle,
                Size,
                NativeMethods.FreeType.MEM_RELEASE);

        }

        public unsafe void* GetPointer(out IntPtr sizeOfChunk)
        {
            return GetPointer(IntPtr.Zero, out sizeOfChunk);
        }

        public unsafe void* GetPointer(IntPtr offset, out IntPtr sizeOfChunk)
        {
            if (IsInvalid || (offset.ToInt64() > Size.ToInt64()))
            {
                sizeOfChunk = IntPtr.Zero;

                return (void*)IntPtr.Zero;
            }

            sizeOfChunk = (IntPtr)(Size.ToInt64() - offset.ToInt64());

            return (byte*)handle + offset.ToInt64();
        }

        public unsafe void* GetPointer()
        {
            return GetPointer(IntPtr.Zero);
        }

        public unsafe void* GetPointer(IntPtr offset)
        {
            if (IsInvalid || (offset.ToInt64() > Size.ToInt64()))
            {
                return (void*)IntPtr.Zero;
            }

            return (byte*)handle + offset.ToInt64();
        }

        public override bool IsInvalid
        {
            get { return handle == IntPtr.Zero; }
        }

        public IntPtr Size { get; private set; }
    }

    #endregion

    #region HeapHandle

    internal sealed class HeapHandle : SafeHandle
    {
        public HeapHandle(IntPtr handle)
            : base(handle, true)
        {
        }

        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        override protected bool ReleaseHandle()
        {
            return NativeMethods.HeapDestroy(handle);
        }

        public unsafe void* Malloc(IntPtr size)
        {
            if (IsInvalid)
            {
                return (void*)IntPtr.Zero;
            }

            return NativeMethods.HeapAlloc(
                this,
                NativeMethods.HeapAllocFlags.Empty,
                size);
        }

        public unsafe bool Free(void* lpMem)
        {
            if (lpMem == null)
            {
                return false;
            }

            return NativeMethods.HeapFree(
                this,
                NativeMethods.HeapFreeFlags.Empty,
                lpMem);
        }

        public override bool IsInvalid
        {
            get { return handle == IntPtr.Zero; }
        }
    }

    #endregion

    #endregion

    class Program
    {
        static void Main()
        {
            IntPtr memoryChunkSize = (IntPtr)(1024 * 1024);
            IntPtr stackAllocation = (IntPtr)(1024);

            #region Example 1

            Console.WriteLine("Example 1 (VirtualAlloc, VirtualFree):");

            IntPtr memoryForSafeHandle =
                NativeMethods.VirtualAlloc(
                    IntPtr.Zero,
                    memoryChunkSize,
                    NativeMethods.AllocationType.MEM_RESERVE | NativeMethods.AllocationType.MEM_COMMIT,
                    NativeMethods.MemoryProtection.PAGE_EXECUTE_READWRITE);

            using (VirtualMemoryHandle memoryHandle =
                new VirtualMemoryHandle(memoryForSafeHandle, memoryChunkSize))
            {
                Console.WriteLine(
                    (!memoryHandle.IsInvalid) ?
                    ("Allocated") :
                    ("Not allocated"));

                if (!memoryHandle.IsInvalid)
                {
                    bool memoryCorrect = true;

                    unsafe
                    {
                        int* arrayOfInt = (int*)memoryHandle.GetPointer();
                        long size = memoryHandle.Size.ToInt64();

                        for (int index = 0; index < size / sizeof(int); index++)
                        {
                            arrayOfInt[index] = index;
                        }

                        for (int index = 0; index < size / sizeof(int); index++)
                        {
                            if (arrayOfInt[index] != index)
                            {
                                memoryCorrect = false;
                                break;
                            }
                        }
                    }

                    Console.WriteLine(
                        (memoryCorrect) ?
                        ("Write/Read success") :
                        ("Write/Read failed"));
                }
            }

            #endregion

            #region Example 2

            Console.WriteLine("Example 2 (HeapCreate, HeapDestroy, HeapAlloc, HeapFree):");

            IntPtr heapForSafeHandle = NativeMethods.HeapCreate(
                NativeMethods.HeapOptions.Empty,
                memoryChunkSize,
                IntPtr.Zero);

            using (HeapHandle heap = new HeapHandle(heapForSafeHandle))
            {
                Console.WriteLine(
                    (!heap.IsInvalid) ?
                    ("Heap created") :
                    ("Heap is not created"));

                if (!heap.IsInvalid)
                {
                    bool memoryCorrect = true;

                    unsafe
                    {
                        int* arrayOfInt = (int*)heap.Malloc(memoryChunkSize);

                        if (arrayOfInt != null)
                        {
                            long size = memoryChunkSize.ToInt64();

                            for (int index = 0; index < size / sizeof(int); index++)
                            {
                                arrayOfInt[index] = index;
                            }

                            for (int index = 0; index < size / sizeof(int); index++)
                            {
                                if (arrayOfInt[index] != index)
                                {
                                    memoryCorrect = false;
                                    break;
                                }
                            }

                            if (!heap.Free(arrayOfInt))
                            {
                                memoryCorrect = false;
                            }
                        }
                        else
                        {
                            memoryCorrect = false;
                        }
                    }

                    Console.WriteLine(
                        (memoryCorrect) ?
                        ("Allocation/Write/Read success") :
                        ("Allocation/Write/Read failed"));
                }
            }

            #endregion

            #region Example 3

            Console.WriteLine("Example 3 (stackalloc):");

            unsafe
            {
                bool memoryCorrect = true;

                int* arrayOfInt = stackalloc int[(int)stackAllocation.ToInt64()];

                long size = stackAllocation.ToInt64();

                for (int index = 0; index < size / sizeof(int); index++)
                {
                    arrayOfInt[index] = index;
                }

                for (int index = 0; index < size / sizeof(int); index++)
                {
                    if (arrayOfInt[index] != index)
                    {
                        memoryCorrect = false;
                        break;
                    }
                }

                Console.WriteLine(
                    (memoryCorrect) ?
                    ("Allocation/Write/Read success") :
                    ("Allocation/Write/Read failed"));
            }

            #endregion

            #region Example 4

            Console.WriteLine("Example 4 (Marshal.AllocHGlobal):");

            unsafe
            {
                bool memoryCorrect = true;

                var globalPointer = Marshal.AllocHGlobal(memoryChunkSize);

                int* arrayOfInt = (int*)globalPointer;

                if (IntPtr.Zero != globalPointer)
                {
                    try
                    {
                        long size = memoryChunkSize.ToInt64();

                        for (int index = 0; index < size / sizeof(int); index++)
                        {
                            arrayOfInt[index] = index;
                        }

                        for (int index = 0; index < size / sizeof(int); index++)
                        {
                            if (arrayOfInt[index] != index)
                            {
                                memoryCorrect = false;
                                break;
                            }
                        }
                    }
                    finally
                    {
                        Marshal.FreeHGlobal(globalPointer);
                    }
                }
                else
                {
                    memoryCorrect = false;
                }

                Console.WriteLine(
                    (memoryCorrect) ?
                    ("Allocation/Write/Read success") :
                    ("Allocation/Write/Read failed"));
            }

            #endregion

            Console.ReadKey();
        }
    }
}
Tags:
Hubs:
+26
Comments10

Articles

Change theme settings