One of the greatest advantages to using a managed language such as C# in the Common Language Runtime (CLR) environment is that you don’t have to worry about memory management as much as you did in languages such as C or C++.
That said, there are still memory-related topics that you need to understand as a C# developer. Furthermore, sometimes you just have to drop out of the world of managed code and get access to pointers. It’s ugly, and you should try to avoid it, but it’s there when you need it.
Solution: The GC
class contains many handy memory-related methods, including GetTotalMemory()
, which is the amount of memory the garbage collector thinks is allocated to your application. The number may not be exactly right because of objects that haven’t been garbage collected yet. However, this has the advantage of being able to tell you about how much memory a certain part of your program uses, rather than the entire process.
This same code prints the following:
Before allocations: 651,064
After allocations: 40,690,080
You can also ask the operating system to give you information about your process:
What each of those numbers mean is not necessarily intuitive, and you should consult a good operating system book, such as Windows Internals (Microsoft Press), to learn more about how virtual memory works.
You can also use performance counters to track this and a lot of other information about your application or its environment. You can access these interactively from perfmon.exe, or see the MSDN documentation for the PerformanceCounter
class to see how you can incorporate these into your own applications.
If for some reason you want to manage raw file handles, bitmaps, memory-mapped files, synchronization objects, or any other kernel object, you need to ensure that the resource is cleaned up. Each of these resources is represented by handles. To manipulate handles, you need to use native Windows functions via P/Invoke (see Chapter 26, “Interacting with the OS and Hardware”).
Solution: Here’s an example of a class that manages a file handle with help from the .NET classes:
When .NET detects a finalizer in a class, it ensures that it is called at some point in garbage collection.
The finalization phase of the collection process can be quite expensive, so it’s important to use finalizers only when necessary. They should almost never be used for managed resources—only unmanaged ones.
Many types of resources are limited (files, database connections, bitmaps, and so on) but have .NET classes to manage them. Like unmanaged resources, you still need to control their lifetimes, but because they are wrapped in managed code, finalizers are not appropriate.
Solution: You need to use the dispose pattern. Let’s look at a similar example as the last section, but with a managed resource. The IDisposable
interface defines a Dispose
method, but to use the pattern correctly you need to define a few more things yourself:
When using the dispose pattern, you call Dispose
yourself when you’re done with the resource:
This pattern is so common that there is a shortcut syntax:
You should not use the Dispose pattern with Windows Communication Framework objects. Unfortunately, they are not implemented according to this pattern. You can find more information on this problem by doing an Internet search for the numerous articles out there detailing this problem.
Whereas finalizers should not touch managed objects, Dispose
can (and should) clean up unmanaged resources. Here’s an example:
Here’s a little program to demonstrate what happens:
It gives the following output:
See the Dispose project in the included source code.
Solution: The short answer is, don’t. You are unlikely to outguess the garbage-collection system for efficiency. That said, if you really know what you’re doing and you’re going to measure the effects, here’s how to do it:
GC.Collect();
One reason why people force a collection is that they’re beginning a performance-sensitive operation that they don’t want interrupted by garbage collection. However, the garbage collection system in .NET 4 has undergone major revision from previous versions and now does more collections in the background, so the need to do forced collections should be lessened.
Solution: The problem is that just holding a reference to an object prevents it from being garbage collected. Enter WeakReference
.
This simple Cache class uses WeakReference
objects to store the actual values, allowing them to be garbage collected as needed.
Weak references should never be used for small items, and you should not rely on them as a crutch to solve memory problems for you. They are best used for items that can use a lot of memory, but are easily recreated as needed, such as in cache situations where it would be nice if the object were still around in memory, but you still want it to be garbage collected eventually.
This test program shows how it could be used:
Solution: To use pointers in your code, you have to do a few things:
1. Mark the code block as unsafe
.
2. Compile the project as unsafe
using the /unsafe
compiler switch (or in your project build settings).
3. Ensure that the target runtime environment has sufficient privilege to run unsafe code (it is a security risk after all).
Here’s a simple example:
You can create pointers to any non-reference type (that also contains only non-reference types):
Entire methods and classes can be marked as unsafe:
unsafe class MyUnsafeClass {...}
unsafe void MyUnsafeMethod() {...}
Solution: By using pointers, you can speed up array lookups by an order of magnitude, but at the price of code safety and guarantees.
This code shows that by using pointers you can gain direct access to memory, potentially overwriting data you didn’t mean to:
You can also use pointer arithmetic, just as you would in native languages:
Note that adding one to a pointer does not increase the memory address by 1 byte, but by 1 increment of the data type size, which in this example is an int
, or 4 bytes.
Most programs do not need to use any of this pointer stuff, and it is quite dangerous to do so, as evidenced by the “unsafe” status you have to grant the code, in addition to the increased permissions required to run programs that do this. If at all possible, try to create programs that do not rely on these techniques.
When the garbage collector runs, it moves objects around in memory in a compaction process. This requires fixing up all the memory addresses for all the moved objects. Obviously, this is not possible for objects the GC does not know about—such as in a native code interop situation.
Solution: “Fix” the memory in place.
You should keep memory fixed only as long as you absolutely need to. While memory is fixed in place, the GC is less efficient as it attempts to work around the unmovable block.
Solution: Use the Marshal.AllocHGlobal()
method and manually add some memory pressure:
When you allocate memory in this way, .NET knows nothing about it, as this program snippet shows:
The output is as follows:
Memory usage before unmanaged allocation: 665,456
Memory usage after unmanaged allocation: 706,416
The AddMemoryPressure
method notifies the CLR that it should take this into account when scheduling garbage collection, but that is all the involvement it has. When your unmanaged memory is freed, you should release this pressure.