.NET Memory Management and Garbage Collector

Introduction

Memory for all objects are allocated in the managed heap. “Managed” here means that all objects that are not longer used by the application are automatically destroyed.

The following happens on creating a new object:

  1. Size of the object is added to the recorded pointer for new object and if there is not enough memory for allocation, collect garbage and calculate pointer for new object.
  2. Memory for object is allocated and cleared, starting from recorder pointer for new object.
  3. The pointer for new object is increased on the allocated size.

In reality, a collection occurs when generation 0 is completely full.

There are three generations of the objects:

  • Zero generation: newly created objects.
  • First generation: objects that survive CG.Collect().
  • Second generation: objects that survive CG.Collect() multiple times.

Garbage collection for specific generation happens in one of these situations:

  • There is no memory in a heap (threshold reached) for new objects of this generation and size.
  • CG.Collect() is called with corresponding parameter.
  • System in low memory situation.

Each generation and large object heap has threshold (commutative size of all objects in particular heap). Reaching this threshold is a typical reason for starting garbage collector. These thresholds are dynamically tuned as the program runs.

Root - references to the heap, that should never be collected: static object pointers, local variables and parameters in thread stack, CPU registers. The list of active roots is maintained by JIT and CLR. This list is requested by GC while collecting the garbage.

Finalization

Then CG collects garbage, it runs Finalize method for objects that override it.

Some facts about finalizable objects:

  • They get promoted to older generations.
  • They take longer to allocate.
  • Finalize methods of several thousands of objects may be executed one after another and freeze your application for some time.
  • They increase lifetime of all objects they refer.
  • Should not rely on persistence of any other manager object in Finalize method.
  • The exact time and order of Finalize calls is unknown.
  • They may survive application shutdown unless RequestFinalizeOnShutdown is called.
  • They should correctly process all exceptions in Finalize because GC ignores them.
  • Their Finalize methods may be executed in another thread.

Finalize methods are executed in separate thread by GC finalizer. This thread sleeps while finalization ready queue is empty.

It is important that the Finalize() method can restore pointers to the object from some persistent object and it prevents GC from destroying it on next run. But the object no longer stays in the finalization queue so it will not be finalized when it becomes garbage next time. A workaround for this is to use GC.ReRegisterForFinalize(object) method in Finalize() to put object in finalization queue. Each call of ReRegisterForFinalize() creates new entry in finalization queue so on finalize it cause several calls of Finalize() for the same object.

Garbage collector pseudocode:

class GC
{
 
  private long _nextObjectPointer; 
  private System _system; 
  private Heap _sheap; // small object heap
  private Heap _lheap; // large object heap 
  private List<object> _finalizationQueue; // should be finalized
  private List<object> _freachableQueue; // finalization ready
 
  public GC(System system)
  {
     _system = system;
     _sheap = _system.GetMemory();
     _lheap = _system.GetMemory();
     _nextObjectPointer = 0;
  }
 
  public object New(Type type) 
  {
    if (type.Size < 85000) {
      // for small objects
      if (_nextObjectPointer + type.Size < _sheap.length)
        Collect();
      _sheap.AllocateAndClear(_nextObjectPointer, type.Size);
      var obj = type.Create(_nextObjectPointer);
      if (type.HasFinalize)
         _finalizationQueue.Add(obj)
      _nextObjectPointer += type.Size;
    }
    else
    {
      // TBD
    }
    return obj;
  }
 
  public void Collect() {
    // 1. Get roots and detect all objects, accessible from the 
    // roots. These objects are in use.
    // 2. Destroy other (unused) objects (except finalizable)
    // and shift used objects in the small object heap 
    // to the beginning of the heap, modifying
    // references to them, older generations are closer
    // to the beginning of the heap.
    // 3. Move unused finalizable object pointers from
    // _finalizationQueue to _freachableQueue.
    // 4. Update _nextObjectPointer
  }

  public void ReRegisterForFinalize(object obj)
  {
    _finalizationQueue.Add(obj);
  }

  public void SuppressFinalize(object obj)
  {
    // Set bit in object descriptor to prevent calling Finalize
  }
}

Close and Dispose

Object should not be used after Dispose(), but object can be opened again after Close(). For example, large byte array in heap can be only disposed, but a file handle or a database connection can be opened and closed multiple times.

If Dispose() or Close() makes finalization by CG unnecessary, they should notify it about by calling CG.SuppressFinalize(object).

However, it is not safe to rely on persistent address of object in LOH, because this behavior may be changed. So the objects should be pinned before modifying by unmanaged code.

Large Object Heap

CG places object that are greater than or equal to 85000 bytes into large object heap and classifies them as second generation objects right after instantiation. While collecting the garbage, CG does not compact (and defragment free space) LOH, it only mark space, occupied by the destroyed objects, as free.

It is important, that LOH may be become too fragmented, take a lot of system memory, and cause OutOfMemory error while trying to create a new object even when it contains enough free space.

Best practices

Avoid big arrays of objects (several thousands), especially when they stays in memory for a long time and have cross-references. Try to implement similar logic with arrays of value type (int, char, struct, etc).

Do not allocate large objects (arrays or collections) frequently, use the object pool instead.

Finalization is supplementary to the Open/Close or Dispose patterns. Do not use only finalization for freeing resources.

There are a few properties in .NET framework that returns new objects each time when they are accessed. For example, System.Drawing.FontFamily.Families creates a lot of IDisposable FontFamily objects. Avoid multiple calls to this property in your application.

If COM components are not released, the Finalize() method of finalization-ready objects are not executed, application memory is constantly increased, or GC.WaitForPendingFinalizers() takes a lot of time then may be your STA thread does not pumps messages (as described in KB828988) and you need to add the following code to pump them:

Thread.CurrentThread.Join(100)

The STA thread should never perform unbounded non-pumping operations, such as calling Console.ReadLine(). Instead, the STA thread must have a MTA thread perform the operation and then wait for the operation to finish. // KB828988

Analyzing application’s memory

Note: Bitmap class stores bitmap raw data in private memory, not managed heap.

Performance counters

There are several memory and garbage collector performance counters, observable by performance monitor (perfmon.exe), that may help you identify problems in the application.

  • .NET CLR Memory/# Gen 2 collections - how many times CG collects second-generation objects (from program start).
  • .NET CLR Memory/Large Object Heap size - LOH size in bytes, updated on each second-generation objects garbage collection.

VMMap

VMMap is a small utility for analyzing application memory: image, private, shareable, mapped files, heap, managed heap, stack, system, and free memory. Shows differences between two application memory states.

Start application, then run VMMap and open you application’s process in VMMap - it should show the current state of the memory. Most likely, you will be interested in two big memory sections: private (memory used for application data) and managed heap (memory used for storing CLR data, including one 32MB and several 16MB memory blocks used by GC for storing objects).

You can refresh the snapshot by pressing F5. In “Show Changes” mode (click “Options” -> “Show Changes” or press Ctrl+D) VMMap shows differences between previous and current snapshots when you refreshing it.

dotTrace

dotTrace is a memory profiler from JetBrains. Supports recording allocations, comparing two memory snapshots, detecting new objects, tracking objects to the roots.

The typical memory profiling session consists of the following steps:

  • Build application.
  • Start dotTrace, click File -> Profile Application, edit “Executable path:” field and select “Memory profiling” in profiling settings.
  • In the moment when you want to start the profiling, click “Record Allocations” and “Mark Memory”.
  • In the moment when you want to stop the profiling, click “Get Snapshot”.
  • In the snapshot window you can click “Show New Objects” to view the objects that were created before marking memory and getting snapshot.
  • Select any of the types and open it in a new window.
  • In the new window click “Merged Shortest” in “Root Paths:” section to view the objects that keep the investigated object in memory.

WinDbg

WinDbg.exe, main executable of the Debugging Tools for Windows package, can analyze CLR heaps with SoS extension. For debugging x86 applications on x64 you need to download and install debugging tools for x86.

After attaching debugger to the process, you can load SoS extension by executing the following command:

.loadby sos mscorwks

Then you can try to run SoS extension commands to analyze the heaps. A few examples:

Information about CLR GC managed heaps:

> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x106c0f1c
generation 1 starts at 0x1069d24c
generation 2 starts at 0x02c81000
ephemeral segment allocation context: none
 segment    begin allocated     size
02c80000 02c81000  03c69680 0x00fe8680(16680576)
08810000 08811000  09807954 0x00ff6954(16738644)
0d170000 0d171000  0d8f24dc 0x007814dc(7869660)
10250000 10251000  10850f28 0x005fff28(6291240)
Large object heap starts at 0x03c81000
 segment    begin allocated     size
03c80000 03c81000  04196e28 0x00515e28(5332520)
Total Size  0x3276200(52912640)
------------------------------
GC Heap Size  0x3276200(52912640)

Memory consumption by type stating from 02c80000 and ending at 03c69680:

> !dumpheap -stat 02c81000 03c69680
total 281387 objects
Statistics:
      MT    Count    TotalSize Class Name
6c9c06a0        1           12 System.Drawing.SizeConverter
6c9c05c8        1           12 System.Drawing.PointConverter
...
0a1b1060        4          352 Some.Page
...
67d7fe10    37203      2083368 System.Reflection.RuntimeMethodInfo
67d80b54    54102      6761880 System.String
Total 281387 objects

Memory consumption by type in LOH:

> !dumpheap -stat 03c81000 04196e28
total 63 objects
Statistics:
      MT    Count    TotalSize Class Name
67d54324       30        55464 System.Object[]
67d835c4        1        90144 System.Byte[]
67d80b54        2       240576 System.String
00781e90       30      4946336      Free
Total 63 objects

Memory fragmentation in LOH:

> !dumpheap -type Free -stat 03c81000 04196e28
total 30 objects
Statistics:
      MT    Count    TotalSize Class Name
00781e90       30      4946336      Free
Total 30 objects

Get addresses of all objects with 0a1b1060 method table within address range:

> !dumpheap -mt 0a1b1060 0d171000  0de68160
 Address       MT     Size
0d610308 0a1b1060       88     
0d8875ac 0a1b1060       88     
0dc258b0 0a1b1060       88     
0ddada50 0a1b1060       88     
total 4 objects
Statistics:
      MT    Count    TotalSize Class Name
0a1b1060        4          352 Some.Page
Total 4 objects

Trace references from object to GC root that holds it:

!gcroot 0d8875ac
Scan Thread 0 OSTHread 950
...
Scan Thread 14 OSTHread db8
DOMAIN(00773328):HANDLE(Pinned):b13ec:Root:03c83250(System.Object[])->
03a9dfa8(Some.Storage)->
03a9f0cc(Storage.Links)->
0dde1444(System.EventHandler`1[[Some.LinkRuleAddedEventArgs, Some]])->
0ddb20b4(System.Object[])->
0d8b8590(System.EventHandler`1[[Some.LinkRuleAddedEventArgs, Some]])->
0d8b838c(Some.Info)->
0d8b8224(Some.Icon)->
...
0d8875ac(Some.Container)

Displays information about object at specified address:

> !dumpobj 0d8b8590
Name: System.EventHandler`1[[Some.LinkRuleAddedEventArgs, Some]]
MethodTable: 06820a7c
EEClass: 67b13518
Size: 32(0x20) bytes
 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
67d80770  40000ff        4        System.Object  0 instance 0d8b838c _target
67d7ffc8  4000100        8 ...ection.MethodBase  0 instance 00000000 _methodBase
67d8341c  4000101        c        System.IntPtr  1 instance  a1a8298 _methodPtr
67d8341c  4000102       10        System.IntPtr  1 instance        0 _methodPtrAux
67d80770  400010c       14        System.Object  0 instance 00000000 _invocationList
67d8341c  400010d       18        System.IntPtr  1 instance        0 _invocationCount

Convert decimal number to hexadecimal:

> ?0n256
Evaluate expression: 256 = 00000100

Print callstaks of all threads:

> ~*kb

   0  Id: 1668.15dc Suspend: 1 Teb: fffde000 Unfrozen
RetAddr  : Args to Child                       : Call Site
7798bc03 : f440c360 f4cd55c8 00000002 00000000 : ntdll!NtWaitForMultipleObjects+0xa
f460f595 : 02313bd0 00000000 00000000 f48ca8bb : KERNEL32!FlsSetValue+0x7b3
f4609f49 : 00000001 0032dff8 02313bd0 f45f7380 : mscorwks!InitializeFusion+0xbbc9
  ...
# 15  Id: 1668.1b3c Suspend: 1 Teb: fffae000 Unfrozen
RetAddr  : Args to Child                       : Call Site
77b70038 : 00000000 00000000 00000000 00000000 : ntdll!DbgBreakPoint
7798be3d : 00000000 00000000 00000000 00000000 : ntdll!DbgUiRemoteBreakin+0x38
77ac6a51 : 00000000 00000000 00000000 00000000 : KERNEL32!BaseThreadInitThunk+0xd
00000000 : 00000000 00000000 00000000 00000000 : ntdll!RtlUserThreadStart+0x21

List threads:

> !threads -special
ThreadCount: 12
UnstartedThread: 0
BackgroundThread: 7
PendingThread: 0
DeadThread: 3
Hosted Runtime: no
                                      PreEmptive                         Lock
       ID OSID ThreadOBJ    State GC       GC Alloc Context  Domain   Count APT Exception
   0    1 15dc 02313bd0   201a220 Enabled  00000000:00000000 0230b9a0     0 MTA
   2    2 13a4 023b3b40      b220 Enabled  00000000:00000000 0230b9a0     0 MTA (Finalizer)
   5    3 151c 1c270460       220 Enabled  00000000:00000000 0230b9a0     0 Ukn
XXXX    4    0 1c28ac30      9820 Enabled  00000000:00000000 0230b9a0     0 MTA
XXXX    5    0 1c2a50c0      9820 Enabled  00000000:00000000 0230b9a0     0 MTA
   8    6 196c 1c2af380      b020 Enabled  00000000:00000000 0230b9a0     0 MTA
   9    7 1918 1c28a2b0      b220 Enabled  00000000:00000000 0230b9a0     0 MTA
  11    8 1908 1c2dac90      7020 Disabled 00000000:00000000 0230b9a0     2 STA (GC)
  12    9 1858 02310020   180b220 Enabled  00000000:00000000 0230b9a0     0 MTA (Threadpool Worker)
  13    a 12a8 1c2bca50   200b220 Enabled  00000000:00000000 0230b9a0     0 MTA
XXXX    b    0 1c2f4400      9820 Enabled  00000000:00000000 0230b9a0     0 Ukn
XXXX    c 1998 1c2fcee0  80010220 Enabled  00000000:00000000 0230b9a0     0 Ukn

       OSID     Special thread type
    1   1450    DbgHelper 
    2   13a4    Finalizer 
   11   1908    SuspendEE 
   12   1858    ThreadpoolWorker 
   14   1994    Gate 

EE is “Execution Engine”, LockThreadStore - SetGCInProgress - SuspendEE - … perform the GC … - SetGCDone - RestartEE - UnlockThreadStore (link).

Some other useful WinDbg commands:

  • > ~<thread-index>k - display callstack
  • > ~<thread-index>e!clrstack - display CLR stack
  • > .logopen /t d:\windbg.log - save all text from WinDbg session window to the file

WeakReferences

You can check that objects are collected by the garbage collector by creating weak reference to them and checking its IsAlive property after GC.Collect().

// some code here that creates objects
var wr = new WeakReference(obj);
obj = null;
GC.Collect();
Assert.IsFalse(wr.IsAlive);

Glossary

  • Committed memory - part of the virtual memory that application allocated.
  • Memory page - minimal memory fragment used for allocation, similar to file system cluster.
  • Virtual memory - all memory provided by operating system for the application.
  • Working set - memory pages of the committed memory that is loaded by OS into RAM because application is trying to access these pages.

References