📜 ⬆️ ⬇️

.NET Framework. Memory management

This article discusses some of the points about working with memory in the .NET Framework. The article describes the work of the GC, how the GC controls its hip, modes of operation of the GC. Examples are given for using memory around GC. I outlined not only easily accessible information, but also that which is available only when studying dumps of applications written in .NET. I hope the article turned out informative and not very boring. The next article will be about the loader, JIT, and its data structures, such as Method Tables, Method Descriptors, and EEClass.

Theory


Unmanaged memory


Virtual memory is a logical representation of memory that does not necessarily affect the physical memory of the process. On 32-bit operating systems, 4Gb of virtual address space is allocated to processes. The default is 2Gb on user mode. We focus on user mode, not kernel mode.
When a process needs to allocate memory, it must first reserve it, then it must fix the memory. This backup / commit process can be performed in one or two steps, depending on how you use the API to manipulate virtual memory.
Usually reserved memory is divided into the following parts:

Managed memory is allocated in parts. Allocations of memory can be on 16, 32 and 64Kb. They must be allocated in uninterrupted blocks, and if there are not enough memory regions to allocate, the process throws an OutOfMemoryException. If the process cannot complete garbage collection, there is not enough memory for the internal structures of the garbage collector, the process will crash.
You must examine the application and avoid even non-fatal OutOfMemoryException exceptions, because the process may be in an unstable state after this exception.
The role of user mode memory managers is to manage virtual memory reservations. Memory managers use several algorithms to achieve different goals in memory management, such as small fragmentation, redundancy of large blocks.
Before a process can use memory, it must reserve and commit at least part of the memory. These 2 steps can be performed using the VirtualAlloc API, VirtualAllocEx . You can free up memory using VirtualFree or VirtualFreeEx . Calling the last functions will change the state of the memory blocks from “fixed” to “free”.
We steal ideas from developers on c ++. There are situations when using GC (we will talk about it later) for reasons of intensive work with memory is not possible. Such situations are rare, and arise with specific limitations. In this case, you can implement malloc and free. Malloc makes a call to HeapAlloc , and free is a call to HeapFree . You can create your own hip by calling HeapCreate . A complete list of functions for working with memory in the Windows environment can be found at - Memory Management Functions .
I am not a linux developer, so I cannot say what to replace the calls to these API functions. Those who will also need to use these functions, I suggest to implement it using the Abstract factory pattern pattern , even if you are not going to transfer your application in the near future to the Mono platform. The use of these functions is generally not very correct, as it creates some difficulties with portability, but in some very specific situations, in order to reduce the pressure on the GC, you have to use it.

Managed memory


The process memory consists of:

Well, here we come to the GC hip. The garbage collector (GC) allocates and frees memory for managed code. GC uses VirtualAlloc to reserve memory for its hip. The heap size depends on the GC mode, and the version of the .NET Framework. The size can be 16, 32 or 64Mb. Hip GC is an inseparable block of virtual memory isolated from other process heaps and managed by the .NET Runtime. Interestingly, the GC does not immediately capture the entire section of memory, but only as it grows. The GC tracks the next free address at the end of the managed heap, and requests the next block of memory, if necessary, starting with it. A separate heap is created for large objects (the .NET Framework 2.0, in 1.1 large objects are in the same heap as the generations are in, but in a different segment). Large object - an object larger than 85000 bytes.
Focus on the work of the GC. GC uses 3 generations (0, 1, 2) and LOH (Hip for large objects). Created objects fall into the zero generation. As soon as the size of the zero generation reaches the threshold value (there is no more memory for it in the segment) and the creation of a new object is not possible, in the zero generation the garbage collection begins. If there is a shortage of memory in the first generation segment, the garbage collection will be for the first generation, and for zero. When collecting garbage for the 2nd generation, there will also be garbage collection for the first and zero generation.
Objects survived after the generation of garbage in the zero generation go to the first, from the first to the second. Based on the foregoing, it is highly not recommended to manually call garbage collection, as this can greatly affect the performance of your application (looking for examples when the garbage collection call is correct, please write in a comment to discuss this issue).
For large objects there are no generations. In the new .NET framework starting from 1.1, if an object that is at the end of the heap is deleted, the Private Bytes process is reduced.
Very interesting issue of the GC - What actually happens when garbage collection? GC performs several steps regardless of whether garbage collection occurs, for 0, 1, 2 generations, or full garbage collection. So, the stages of garbage collection:

Modes of operation GC:

How to enable the desired mode GC. In the configuration file, the configuration / runtime section:

Increasing GC performance comes down to solving the following problems:

GC uses links to determine if it is possible to free up memory occupied by an object. Before you perform garbage collection, GC begins with ancestors and goes up the links building them in the form of a tree. Using a list of links to all objects, it determines objects that are inaccessible and ready for garbage collection.
There are several types of links:

I wanted to write about the finalization, but the information on it is complete. The only thing to note is that an additional stream of finalization is added to each process, and the process can be suspended, if blocking this stream is blocked, and the crash of the finalization stream leads to the crash of the entire application starting from .NET Framework 2.0, in previous versions there will be a restart of the stream.

Some examples of working with memory without 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(); } } } 

')

Source: https://habr.com/ru/post/44208/


All Articles