.NET Framework Memory management
Theory
Unmanaged memory
Virtual memory is a logical representation of memory that does not necessarily reflect on the physical memory of a process. On 32-bit operating systems, 4Gb of virtual address space is allocated to processes. The default is 2Gb per user mode. We will focus on user mode, not kernel mode.
When a process needs to allocate memory, it must first reserve it, then it must commit the memory. This backup / commit process can be done in one or two steps, depending on how you use the API to manipulate virtual memory.
Typically, the reserved memory is divided into the following parts:
- Streams (and stacks);
- .Dll files;
- Virtual memory allocation;
- NT hip;
- Hip memory managers like the .NET GC.
Managed memory is allocated in parts. Allocations of memory can be 16, 32 and 64Kb. They should be allocated in non-interruptible 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 should 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 redundancy. Memory managers use several algorithms to achieve different goals when managing memory, 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 , VirtualAllocEx API functions . You can free 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 c ++ developers. There are situations when it is not possible to use GC (we will talk about it later) for reasons of intensive work with memory. Such situations are rare, and arise under specific restrictions. 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 how to replace the calls to these API functions. For those who will also need to use these functions, I propose to implement this using the Abstract factory pattern , even if you are not going to port your application in the near future to the Mono platform. Using these functions is generally not very correct, as it creates some difficulties with portability, but in some very specific situations it is necessary to use this to reduce pressure on the GC.
Managed memory
The process memory consists of:
- Managed heap - saving all managed objects that GC has not yet collected;
- Loader hips - dynamic modules and saving compiled objects for JIT, such as method tables, method descriptions and EEClass (The next article will be devoted to JIT and its structures);
- Native Hips - Hips for native memory;
- Memory used for streams, their stacks and registers;
- Memory for storing native dll files, as well as for native parts of managed dll files;
- Other virtual memory allocations that do not fit into more than one category described above;
- Unused portions of memory.
Well, here we come to the GC hip. Garbage Collector (GC) allocates and frees memory for managed code. GC uses VirtualAlloc to reserve a piece of memory for its heap. The size of the heap depends on the GC mode and version of the .NET Framework. The size can be 16, 32 or 64MB. Hip GC is an inextricable block of virtual memory isolated from other process heaps and managed by the .NET Runtime. Interestingly, GC does not immediately capture the entire 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 from it. A separate hip is created for large objects (.NET Framework 2.0, in 1.1 large objects are in the same hip that generations are in, but in a different segment). Large object - an object larger than 85,000 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 a threshold value (there is no more memory for it in the segment) and creating a new object is not possible, in the zero generation garbage collection begins. If there is a shortage of memory in the first generation segment, then 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 surviving after garbage collection in the zero generation go to the first, from the first to the second. Based on the foregoing, it is highly discouraged 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 the object located at the end of the heap is deleted, the Private Bytes of the process are reduced.
A very interesting question about the work of GC - What actually happens when garbage collection? The GC performs several steps, regardless of whether garbage collection occurs, for generation 0, 1, 2 or complete garbage collection. So, the steps of garbage collection:
- The initial stage of the GC - waits until all managed threads reach a point in execution when it is safe to suspend them;
- Objects that have no links are marked as ready for deletion;
- GC plans segment sizes for generations and estimates the level of fragmentation in the heap after garbage collection;
- Deletes objects marked for deletion. Enters these addresses of objects into the list of free space addresses if the GC is not compact;
- Moves objects to the lower addresses of the managed hip. The most expensive operation;
- Resuming managed threads.
GC operating modes:
- Competing - created for GUI applications when response time is important. Pauses application execution several times during the garbage collection process giving it processor time to execute. GC uses one hip and one thread;
- Non-competing - pauses the application until garbage collection is complete. GC uses one hip and one thread;
- Server - maximum performance on a machine with multiple processors or cores. GC uses one hip per processor, as well as one thread per core.
How to enable the desired mode of operation of the GC. In the configuration file, the configuration / runtime section:
- Competing - <gcConcurrent = true>;
- Non-competing - <gcConcurrent = false>;
- Server - <gcServer Enabled = true>.
Increasing GC performance comes down to solving the following problems:
- Frequent and large selection of objects - causes the GC to collect garbage more often. Frequent allocation of large objects can cause a large processor load, since LOH garbage collection causes garbage collection in the second generation;
- Allocating memory in advance creates several problems. Firstly, it forces the GC to collect garbage more often; secondly, it allows objects to survive the assembly;
- Many ancestors or pointers - when moving objects and reducing fragmentation, all pointers will need to be changed in accordance with the new addresses of the objects. When fragmentation is reduced, objects can be spaced in different directions of a segment in heap. This can all negatively affect execution speed;
- There are a lot of objects that manage to get into the next generation, and do not live there for a long time - objects that survive garbage collection fall into the next generation, but do not stay there for a long time. They create pressure on the next generation, and can cause an expensive garbage collection operation in this generation;
- Objects that cannot be moved (GCHandleType.Pinned, Interop, fixed) - increases the fragmentation of memory of generations, and the GC can look longer for a continuous memory area for a new object.
The GC uses references to determine whether it is possible to free the memory occupied by the object. Before garbage collection, GC starts with its ancestors and goes up the links building them in a tree. Using the list of links to all objects, it determines objects that are inaccessible and ready for garbage collection.
There are several types of links:
- Strong link. Existing reference to a live object. This prevents the object from garbage collection;
- Weak link. Existing reference to a “live” object, but allowing you to delete the object that is referenced during garbage collection. This type of links can be used, for example, for a caching system.
I wanted to write about the finalization, but the information on it is full. It can only be noted that an additional finalization stream is added to each process, and the process can be suspended if you block this stream with locks, and the finalization stream crashes starting from the .NET Framework 2.0 to crash the entire application, in previous versions there will be a restart of the stream.
Some examples of working with memory without a GC
using System;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security;
namespaceTestApplication
{
#region NativeMethodsinternalstaticclassNativeMethods
{
#region Virtual memory#region VirtualAlloc
[Flags()]
publicenum 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()]
publicenum 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)]
internalstaticextern IntPtr VirtualAlloc(
IntPtr lpAddress,
IntPtr dwSize,
AllocationType flAllocationType,
MemoryProtection flProtect);
#endregion#region VirtualFree
[Flags()]
publicenum FreeType : uint
{
MEM_DECOMMIT = 0x4000,
MEM_RELEASE = 0x8000,
}
[DllImport(
"kernel32.dll",
CharSet = CharSet.Auto,
CallingConvention = CallingConvention.Winapi)]
[return: MarshalAs(UnmanagedType.Bool)]
internalstaticexternboolVirtualFree(
IntPtr lpAddress,
IntPtr dwSize,
FreeType dwFreeType);
#endregion#endregion#region Heap#region HeapCreate
[Flags()]
publicenum 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)]
internalstaticextern IntPtr HeapCreate(
HeapOptions flOptions,
IntPtr dwInitialSize,
IntPtr dwMaximumSize);
#endregion#region HeapDestroy
[DllImport(
"kernel32.dll",
CharSet = CharSet.Auto,
CallingConvention = CallingConvention.Winapi)]
[return: MarshalAs(UnmanagedType.Bool)]
internalstaticexternboolHeapDestroy(
IntPtr hHeap);
#endregion#region HeapAlloc
[Flags()]
publicenum 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)]
internalstaticexternunsafevoid* HeapAlloc(
HeapHandle hHeap,
HeapAllocFlags dwFlags,
IntPtr dwBytes);
#endregion#region HeapFree
[Flags()]
publicenum HeapFreeFlags : uint
{
Empty = 0x00000000,
HEAP_NO_SERIALIZE = 0x00000001,
}
[DllImport(
"kernel32.dll",
CharSet = CharSet.Auto,
CallingConvention = CallingConvention.Winapi)]
[return: MarshalAs(UnmanagedType.Bool)]
internalstaticexternunsafeboolHeapFree(
HeapHandle hHeap,
HeapFreeFlags dwFlags,
void* lpMem);
#endregion#endregion
}
#endregion#region Memory handles#region VirtualMemoryHandleinternalsealedclassVirtualMemoryHandle : SafeHandle
{
publicVirtualMemoryHandle(IntPtr handle, IntPtr size)
: base(handle, true)
{
Size = size;
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
overrideprotectedboolReleaseHandle()
{
return NativeMethods.VirtualFree(
handle,
Size,
NativeMethods.FreeType.MEM_RELEASE);
}
publicunsafevoid* GetPointer(out IntPtr sizeOfChunk)
{
return GetPointer(IntPtr.Zero, out sizeOfChunk);
}
publicunsafevoid* 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();
}
publicunsafevoid* GetPointer()
{
return GetPointer(IntPtr.Zero);
}
publicunsafevoid* GetPointer(IntPtr offset)
{
if (IsInvalid || (offset.ToInt64() > Size.ToInt64()))
{
return (void*)IntPtr.Zero;
}
return (byte*)handle + offset.ToInt64();
}
publicoverridebool IsInvalid
{
get { return handle == IntPtr.Zero; }
}
public IntPtr Size { get; privateset; }
}
#endregion#region HeapHandleinternalsealedclassHeapHandle : SafeHandle
{
publicHeapHandle(IntPtr handle)
: base(handle, true)
{
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
overrideprotectedboolReleaseHandle()
{
return NativeMethods.HeapDestroy(handle);
}
publicunsafevoid* Malloc(IntPtr size)
{
if (IsInvalid)
{
return (void*)IntPtr.Zero;
}
return NativeMethods.HeapAlloc(
this,
NativeMethods.HeapAllocFlags.Empty,
size);
}
publicunsafeboolFree(void* lpMem)
{
if (lpMem == null)
{
returnfalse;
}
return NativeMethods.HeapFree(
this,
NativeMethods.HeapFreeFlags.Empty,
lpMem);
}
publicoverridebool IsInvalid
{
get { return handle == IntPtr.Zero; }
}
}
#endregion#endregionclassProgram
{
staticvoidMain()
{
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 = stackallocint[(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();
}
}
}