This is the second part of the .NET Internals Cookbook series. For your convenience you can find other parts in the table of contents in Part 0 – Table of contents
Table of Contents
7. How to compact the LOH?
You need to set GCSettings.LargeObjectHeapCompactionMode property to CompactOnce. It will be compacted when the GC will be cleaning the LOH for the next time. By default compaction is disabled.
8. What objects are allocated on the LOH?
Any object having at least 85000 bytes (*NOT* 85 kilobytes). Also, for 32-bit applications, any array of doubles with at least 1000 elements. This is because LOH is always aligned on an 8-byte boundary. Accessing doubles is faster if they are properly aligned so big arrays are stored in LOH. In 64-bit applications this trick for doubles does not happen.
To see this in action, use this code:
1 2 3 4 5 6 7 8 9 10 |
using System; public class Program { public static void Main() { Console.WriteLine(GC.GetGeneration(new double[999])); Console.WriteLine(GC.GetGeneration(new double[1000])); } } |
If you compile it in 32-bit, you should get array of doubles allocated in generation 2 (all LOH objects are in that generation). If you try it here you will see that it doesn’t happen in 64-bit application.
9. How to ask the CLR to not throw out-of-band exceptions?
You need to use Constrained Executed Regions. Those are special blocks of code where you have multiple limitations (you cannot allocate memory, do boxing etc) but CLR tries to not stop your code from executing in its entirety.
Just keep in mind that the CER is in catch and finally blocks, not in the try. Typical trick is to have code like this:
1 2 3 4 5 6 7 8 |
try { // Nothing } finally { // All the code } |
Some tools may indicate that finally block here is not needed but it is not true. CER doesn’t mark try block.
So how does it work? It analyzes the code and tries to make sure that it works as expected. See this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
using System; using System.Runtime.CompilerServices; using System.Runtime.ConstrainedExecution; using System.Threading; namespace ConstrainedExecutedRegion { class Program { static void Main(string[] args) { var cerThread = new Thread(() => { Console.WriteLine("Not even executed!"); RuntimeHelpers.PrepareConstrainedRegions(); try { } finally { Console.WriteLine("In finally: " + DateTime.Now); OutOfMemory(); } }) { Name = "CER thread" }; cerThread.Start(); cerThread.Join(); } [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] unsafe static void OutOfMemory() { Big big; } } unsafe struct Big { public fixed byte Bytes[int.MaxValue]; } } |
If you comment out either RuntimeHelpers.PrepareConstrainedRegions();
or [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
you get this output:
1 2 3 4 5 |
Not even executed! In finally: 2/18/2019 9:58:03 AM Unhandled Exception: OutOfMemoryException. Press any key to continue . . . |
The method is executed and we get OOM. But if you have all lines in place you get
1 2 |
Unhandled Exception: OutOfMemoryException. Press any key to continue . . . |
The method was not even executed! That’s because CLR tried to prepare the method beforehand and noticed that there is a problem ahead. Interesting thing is when actually CER checks are executed — it happens when the code is JIT compiled. You can see in debugger that the method is neither executed nor JITted and the exception is thrown.
However, CER is not always required, If you want to avoid being aborted by Thread.Abort
you can just run everything in finally.
10. How to resurrect an object? How to make it being cleaned up again?
If you override an object’s Finalize
method you can execute some code when the object is not reachable anymore. At the same time, you can restore strong reference to that object which effectively resurrects it. To make it cleaned up again you need to register it again for finalization using GC.ReRegisterForFinalize method. See this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
using System; namespace ExceptionTests { public class Program { public static Foo StrongReference = null; public static void Main() { Foo foo = new Foo(); foo = null; Console.WriteLine("Created, cleaning up."); GC.Collect(); GC.WaitForPendingFinalizers(); StrongReference = null; Console.WriteLine("After cleaning. Running GC again."); GC.Collect(); GC.WaitForPendingFinalizers(); } } public class Foo { bool wasResurrected = false; ~Foo() { if (!wasResurrected) { Program.StrongReference = this; GC.ReRegisterForFinalize(this); wasResurrected = true; Console.WriteLine("Resurrected!"); } else { Console.WriteLine("Finalizing again"); } } } } |
The output is:
1 2 3 4 |
Created, cleaning up. Resurrected! After cleaning. Running GC again. Finalizing again |
So you can see that the object was indeed resurrected and then cleaned up again.
11. What is VM_HOARDING?
Imagine a situation when you frequently allocate objects which die quickly. You may need to ask OS to allocate some memory for you, then you run the code and finally you clean up. Since now the memory is not used anymore, you return it to the operating system. If this pattern repeats frequently, you may spend a lot of time on calling OS because of inefficient memory usage. This is somewhat similar to trashing when you spend much more time managing the memory instead of doing something useful.
.NET 2 added a feature to not release segments of memory which are not needed anymore. Instead, CLR decommits them and puts in the wait list. This solves the problem because when you repeat the allocation pattern you don’t need to ask OS to give you some memory because you already have it. You just need to commit it as keep using.
See more in this blog post.
12. How to turn the GC off?
You can use GC.TryStartNoGCRegion.
You need to provide an amount of memory you want to use. First, .NET checks if it has enough memory and if not then it allocates some more to fulfill the request. Next, every time the GC would like to run the collection it checks whether you didn’t allocate more than promised and whether you are still in No GC region. If so — it doesn’t run the collection. However, if you allocate more than you specified it may ignore your request and do the cleaning. You can be notified when this happens as shown by Luca Bolognese.
To see more read Matt Warren’s fantastic explanation.