# Chapter 21 The Managed Heap and Garbage Collection

# Managed Heap Basics

Every program uses resources of one sort or another, be they files, memory buffers, screen space, network connections, database resources, and so on. In fact, in an object-oriented environment, every type identifies some resource available for a program’s use. To use any of these resources requires memory to be allocated to represent the type. The following steps are required to access a resource:

  1. Allocate memory for the type that represents the resource (usually accomplished by using C#’s new operator).
  2. Initialize the memory to set the initial state of the resource and to make the resource usable. The type’s instance constructor is responsible for setting this initial state.
  3. Use the resource by accessing the type’s members (repeating as necessary).
  4. Tear down the state of a resource to clean up.
  5. Free the memory. The garbage collector is solely responsible for this step.

This seemingly simple paradigm has been one of the major sources of problems for programmers that must manually manage their memory; for example, native C++ developers. Programmers responsible for managing their own memory routinely forget to free memory, which causes a memory leak. In addition, these programmers frequently use memory after having released it, causing their program to experience memory corruption resulting in bugs and security holes. Furthermore, these two bugs are worse than most others because you can’t predict the consequences or the timing of them. For other bugs, when you see your application misbehaving, you just fix the line of code that is not working.

As long as you are writing verifiably type-safe code (avoiding C#’s unsafe keyword), then it is impossible for your application to experience memory corruption. It is still possible for your application to leak memory but it is not the default behavior. Memory leaks typically occur because your application is storing objects in a collection and never removes objects when they are no longer needed.

To simplify things even more, most types that developers use quite regularly do not require Step 4 (tear down the state of the resource to clean up). And so, the managed heap, in addition to abolishing the bugs I mentioned, also provides developers with a simple programming model: allocate and initialize a resource and use it as desired. For most types, there is no need to clean up the resource and the garbage collector will free the memory.

When consuming instances of types that require special cleanup, the programming model remains as simple as I’ve just described. However, sometimes, you want to clean up a resource as soon as possible, rather than waiting for a GC to kick in. In these classes, you can call one additional method (called Dispose) in order to clean up the resource on your schedule. On the other hand, implementing a type that requires special cleanup is quite involved. I describe the details of all this in the “Working with Types Requiring Special Cleanup” section later in this chapter. Typically, types that require special cleanup are those that wrap native resources like files, sockets, or database connections.

# Allocating Resources from the Managed Heap

The CLR requires that all objects be allocated from the managed heap. When a process is initialized, the CLR allocates a region of address space for the managed heap. The CLR also maintains a pointer, which I’ll call NextObjPtr. This pointer indicates where the next object is to be allocated within the heap. Initially, NextObjPtr is set to the base address of the address space region.

As region fills with non-garbage objects, the CLR allocates more regions and continues to do this until the whole process’s address space is full. So, your application’s memory is limited by the process’s virtual address space. In a 32-bit process, you can allocate close to 1.5 gigabytes (GB) and in a 64-bit process, you can allocate close to 8 terabytes.

C#’s new operator causes the CLR to perform the following steps:

  1. Calculate the number of bytes required for the type’s fields (and all the fields it inherits from its base types).
  2. Add the bytes required for an object’s overhead. Each object has two overhead fields: a type object pointer and a sync block index. For a 32-bit application, each of these fields requires 32 bits, adding 8 bytes to each object. For a 64-bit application, each field is 64 bits, adding 16 bytes to each object.
  3. The CLR then checks that the bytes required to allocate the object are available in the region. If there is enough free space in the managed heap, the object will fit, starting at the address pointed to by NextObjPtr, and these bytes are zeroed out. The type’s constructor is called (passing NextObjPtr for the this parameter), and the new operator returns a reference to the object. Just before the reference is returned, NextObjPtr is advanced past the object and now points to the address where the next object will be placed in the heap.

Figure 21-1 shows a managed heap consisting of three objects: A, B, and C. If another object were to be allocated, it would be placed where NextObjPtr points to (immediately after object C).

image-20221127172059249

For the managed heap, allocating an object simply means adding a value to a pointer—this is blazingly fast. In many applications, objects allocated around the same time tend to have strong relationships to each other and are frequently accessed around the same time. For example, it’s very common to allocate a FileStream object immediately before a BinaryWriter object is created. Then the application would use the BinaryWriter object, which internally uses the FileStream object. Because the managed heap allocates these objects next to each other in memory, you get excellent performance when accessing these objects due to locality of reference. Specifically, this means that your process’s working set is small, which means your application runs fast with less memory. It’s also likely that the objects your code is accessing can all reside in the CPU’s cache. The result is that your application will access these objects with phenomenal speed because the CPU will be able to perform most of its manipulations without having cache misses that would force slower access to RAM.

So far, it sounds like the managed heap provides excellent performance characteristics. However, what I have just described is assuming that memory is infinite and that the CLR can always allocate new objects at the end. However, memory is not infinite and so the CLR employs a technique known as garbage collection (GC) to “delete” objects in the heap that your application no longer requires access to.

# The Garbage Collection Algorithm

When an application calls the new operator to create an object, there might not be enough address space left in the region to allocate the object. If insufficient space exists, then the CLR performs a GC.

💡重要提示:前面的描述过于简单。事实上,垃圾回收时在第 0 代满的时候发生的。本章后面会解释 “代”。在此之前,先假设堆满就发生来及回收。

For managing the lifetime of objects, some systems use a reference counting algorithm. In fact, Microsoft’s own Component Object Model (COM) uses reference counting. With a reference counting system, each object on the heap maintains an internal field indicating how many “parts” of the program are currently using that object. As each “part” gets to a place in the code where it no longer requires access to an object, it decrements that object’s count field. When the count field reaches 0, the object deletes itself from memory. The big problem with many reference counting systems is that they do not handle circular references well. For example, in a GUI application, a window will hold a reference to a child UI element. And the child UI element will hold a reference to its parent window. These references prevent the two objects’ counters from reaching 0, so both objects will never be deleted even if the application itself no longer has a need for the window.

Due to this problem with reference counting garbage collector algorithms, the CLR uses a referencing tracking algorithm instead. The reference tracking algorithm cares only about reference type variables, because only these variables can refer to an object on the heap; value type variables contain the value type instance directly. Reference type variables can be used in many contexts: static and instance fields within a class or a method’s arguments or local variables. We refer to all reference type variables as roots.

When the CLR starts a GC, the CLR first suspends all threads in the process. This prevents threads from accessing objects and changing their state while the CLR examines them. Then, the CLR performs what is called the marking phase of the GC. First, it walks through all the objects in the heap setting a bit (contained in the sync block index field) to 0. This indicates that all objects should be deleted. Then, the CLR looks at all active roots to see which objects they refer to. This is what makes the CLR’s GC a reference tracking GC. If a root contains null, the CLR ignores the root and moves on to examine the next root.

Any root referring to an object on the heap causes the CLR to mark that object. Marking an object means that the CLR sets the bit in the object’s sync block index to 1. When an object is marked, the CLR examines the roots inside that object and marks the objects they refer to. If the CLR is about to mark an already-marked object, then it does not examine the object’s fields again. This prevents an infinite loop from occurring in the case where you have a circular reference.

Figure 21-2 shows a heap containing several objects. In this example, the application roots refer directly to objects A, C, D, and F. All of these objects are marked. When marking object D, the garbage collector notices that this object contains a field that refers to object H, causing object H to be marked as well. The marking phase continues until all the application roots have been examined.

Once complete, the heap contains some marked and some unmarked objects. The marked objects must survive the collection because there is at least one root that refers to the object; we say that the object is reachable because application code can reach (or access) the object by way of the variable that still refers to it. Unmarked objects are unreachable because there is no root existing in the application that would allow for the object to ever be accessed again.

image-20221127172335755

Now that the CLR knows which objects must survive and which objects can be deleted, it begins the GC’s compacting phase. During the compacting phase, the CLR shifts the memory consumed by the marked objects down in the heap, compacting all the surviving objects together so that they are contiguous in memory. This serves many benefits. First, all the surviving objects will be next to each other in memory; this restores locality of reference reducing your application’s working set size, thereby improving the performance of accessing these objects in the future. Second, the free space is all contiguous as well, so this region of address space can be freed, allowing other things to use it. Finally, compaction means that there are no address space fragmentation issues with the managed heap as is known to happen with native heaps.

When compacting memory, the CLR is moving objects around in memory. This is a problem because any root that referred to a surviving object now refers to where that object was in memory; not where the object has been relocated to. When the application’s threads eventually get resumed, they would access the old memory locations and corrupt memory. Clearly, this can’t be allowed and so, as part of the compacting phase, the CLR subtracts from each root the number of bytes that the object it referred to was shifted down in memory. This ensures that every root refers to the same object it did before; it’s just that the object is at a different location in memory.

After the heap memory is compacted, the managed heap’s NextObjPtr pointer is set to point to a location just after the last surviving object. This is where the next allocated object will be placed in memory. Figure 21-3 shows the managed heap after the compaction phase. After the compaction phase is complete, the CLR resumes all the application’s threads and they continue to access the objects as if the GC never happened at all.

image-20221127172415439

If the CLR is unable to reclaim any memory after a GC and if there is no address space left in the processes to allocate a new GC segment, then there is just no more memory available for this process. In this case, the new operator that attempted to allocate more memory ends up throwing an OutOfMemoryException. Your application can catch this and recover from it but most applications do not attempt to do so; instead, the exception becomes an unhandled exception, Windows terminates the process, and then Windows reclaims all the memory that the process was using.

As a programmer, notice how the two bugs described at the beginning of this chapter no longer exist. First, it’s not possible to leak objects because any object not accessible from your application’s roots will be collected at some point. Second, it’s not possible to corrupt memory by accessing an object that was freed because references can only refer to living objects, because this is what keeps the objects alive anyway.

💡重要提示:静态字段引用的对象一直存在,直到用于加载类型的 AppDomain 卸载为止。内存泄漏的一个常见原因就是让静态字段引用某个集合对象,然后不停地向集合添加数据项。静态字段使集合对象一直存活,而集合对象使所有数据项一直存活。因此,应尽量避免使用静态字段。

# Garbage Collections and Debugging

As soon as a root goes out of scope, the object it refers to is unreachable and subject to having its memory reclaimed by a GC; objects aren’t guaranteed to live throughout a method’s lifetime. This can have an interesting impact on your application. For example, examine the following code.

using System; 
using System.Threading; 
public static class Program { 
 public static void Main() { 
 // Create a Timer object that knows to call our TimerCallback 
 // method once every 2000 milliseconds. 
 Timer t = new Timer(TimerCallback, null, 0, 2000); 
 // Wait for the user to hit <Enter>. 
 Console.ReadLine(); 
 } 
 private static void TimerCallback(Object o) { 
 // Display the date/time when this method got called. 
 Console.WriteLine("In TimerCallback: " + DateTime.Now); 
 // Force a garbage collection to occur for this demo. 
 GC.Collect(); 
 } 
}

Compile this code from the command prompt without using any special compiler switches. When you run the resulting executable file, you’ll see that the TimerCallback method is called just once!

From examining the preceding code, you’d think that the TimerCallback method would get called once every 2,000 milliseconds. After all, a Timer object is created, and the variable t refers to this object. As long as the timer object exists, the timer should keep firing. But you’ll notice in the TimerCallback method that I force a garbage collection to occur by calling GC.Collect().

When the collection starts, it first assumes that all objects in the heap are unreachable (garbage); this includes the Timer object. Then, the collector examines the application’s roots and sees that Main doesn’t use the t variable after the initial assignment to it. Therefore, the application has no variable referring to the Timer object, and the garbage collection reclaims the memory for it; this stops the timer and explains why the TimerCallback method is called just once.

Let’s say that you’re using a debugger to step through Main, and a garbage collection just happens to occur just after t is assigned the address of the new Timer object. Then, let’s say that you try to view the object that t refers to by using the debugger’s Quick Watch window. What do you think will happen? The debugger can’t show you the object because it was just garbage collected. This behavior would be considered very unexpected and undesirable by most developers, so Microsoft has come up with a solution.

When you compile your assembly by using the C# compiler’s /debug switch, the compiler applies a System.Diagnostics.DebuggableAttribute with its DebuggingModes’ DisableOptimizations flag set into the resulting assembly. At run time, when compiling a method, the JIT compiler sees this flag set, and artificially extends the lifetime of all roots to the end of the method. For my example, the JIT compiler tricks itself into believing that the t variable in Main must live until the end of the method. So, if a garbage collection were to occur, the garbage collector now thinks that t is still a root and that the Timer object that t refers to will continue to be reachable. The Timer object will survive the collection, and the TimerCallback method will get called repeatedly until Console. ReadLine returns and Main exits.

To see this, just recompile the program from a command prompt, but this time, specify the C# compiler’s /debug switch. When you run the resulting executable file, you’ll now see that the TimerCallback method is called repeatedly! Note, the C# compiler’s /optimize+ compiler switch turns optimizations back on, so this compiler switch should not be specified when performing this experiment.

The JIT compiler does this to help you with JIT debugging. You may now start your application normally (without a debugger), and if the method is called, the JIT compiler will artificially extend the lifetime of the variables to the end of the method. Later, if you decide to attach a debugger to the process, you can put a breakpoint in a previously compiled method and examine the root variables.

So now you know how to build a program that works in a debug build but doesn’t work correctly when you make a release build! Because no developer wants a program that works only when debugging it, there should be something we can do to the program so that it works all of the time regardless of the type of build.

You could try modifying the Main method to the following.

public static void Main() { 
 // Create a Timer object that knows to call our TimerCallback 
 // method once every 2000 milliseconds. 
 Timer t = new Timer(TimerCallback, null, 0, 2000); 
 // Wait for the user to hit <Enter>. 
 Console.ReadLine(); 
 // Refer to t after ReadLine (this gets optimized away) 
 t = null; 
}

However, if you compile this (without the /debug+ switch) and run the resulting executable file, you’ll see that the TimerCallback method is still called just once. The problem here is that the JIT compiler is an optimizing compiler, and setting a local variable or parameter variable to null is the same as not referencing the variable at all. In other words, the JIT compiler optimizes the t = null; line out of the code completely, and therefore, the program still does not work as we desire. The correct way to modify the Main method is as follows.

public static void Main() { 
 // Create a Timer object that knows to call our TimerCallback 
 // method once every 2000 milliseconds. 
 Timer t = new Timer(TimerCallback, null, 0, 2000); 
 // Wait for the user to hit <Enter>. 
 Console.ReadLine(); 
 // Refer to t after ReadLine (t will survive GCs until Dispose returns) 
 t.Dispose(); 
}

Now, if you compile this code (without the /debug+ switch) and run the resulting executable file, you’ll see that the TimerCallback method is called multiple times, and the program is fixed. What’s happening here is that the object t is required to stay alive so that the Dispose instance method can be called on it. (The value in t needs to be passed as the this argument to Dispose.) It’s ironic: by explicitly indicating where you want the timer to be disposed, it must remain alive up to that point.

💡注意:读完本节的内容后,不必担心对象被过早回收这个问题。这里讨论之所以使用 Timer 类,是因为它具有其他类不具有的特殊行为。 Timer 类的特点 (和问题) 在于,堆中存在的一个 Timer 对象会造成别的事情的发生:一个线程池程序期调用一个方法。其他任何类型都不具有这个行为。例如,内存中存在的一个 String 对象不会造成别的事情的发生;字符串就那么 “傻傻地呆在那里”。所以,我用 Timer 展示根的工作原理以及对象生存期与调试器的关系,讨论的重点并不是如何保持对象的存活。所有非 Timer 的对象都会根据应用程序对的需要而自动存活。

💡小结:CLR 要求所有对象都从托管堆分配。进程初始化时,CLR 划出一个地址空间区域作为托管堆。CLR 还要维护一个指针,该指针指向下一个对象在堆中的分配位置。在一开始的时候,该指针设为地址空间区域的基地址。一个区域被非垃圾对象填满后,CLR 会分配更多的区域。这个过程一直重复,直到整个进程地址空间都被填满。所以,你的应用程序的内存受进程的虚拟地址空间的限制。32 位进程最多能分配 1.5GB,64 位进程最多能分配 8TB。对于托管堆,分配对象只需在指针上加一个值 —— 速度相当快。在许多应用程序中,差不多同时分配的对象彼此间有较强的联系,而且经常差不多在同一时间访问。例如,经常在分配一个 BinaryWriter 对象之前分配一个 FileStream 对象。然后,应用程序使用 BinaryWriter 对象,而后者在内部使用 FileStream 对象。由于托管堆在内存中连续分配这些对象,所以会因为引用的 “局部化”(locality)而获得性性能上的提升。具体地说,这意味着进程的工作集会非常小,应用程序只需使用很少的内存,从而提高了速度。还意味着代码使用的对象可以全部驻留在 CPU 的缓存中。结果是应用程序能以惊人的速度访问这些对象,因为 CPU 在执行大多数操作时,不会因为 “缓存未命中”(cache miss) 而被迫访问较慢的 RAM。至于对象生存期的管理,有的系统采用的是某种引用计数算法。事实上,Microsoft 自己的 “组件对象模型”(Component Object Model, COM) 用的就是引用计数。在这种系统中,堆上的每个对象都维护着一个内存字段来统计程序中多少 "部分" 正在使用对象。随着每一 “部分” 到达代码中某个不再需要对象的地方,就递减对象的计数字段。计数字段变成 0,对象就可以从内存中删除了。许多引用计数系统最大的问题是处理不好循环引用。鉴于引用计数垃圾回收器算法存在的问题,CLR 改为使用一种引用跟踪算法。引用跟踪算法只关心引用类型的变量,因为只有这种变量才能引用堆上的对象;值类型变量直接包含值类型实例。引用类型变量可在许多场合使用,包括类的静态和实例字段,或者方法的参数和局部变量。我们将所有引用类型的变量都称为根。CLR 开始 GC 时,首先暂停进程中的所有线程。这样可以防止线程在 CLR 检查期间访问对象并更改其状态。然后,CLR 进入 GC 的 标记阶段。在这个阶段,CLR 遍历堆中的所有对象,将同步块索引字段中的一位设为 0。这表明所有对象都应删除。然后,CLR 检查所有活动根。查看它们引用了哪些对象。这正是 CLR 的 GC 称为引用跟踪 GC 的原因。如果一个根包含 null, CLR 忽略这个根并继续检查下个根。任何根如果引用了堆上的对象,CLR 都会标记那个对象,也就是将该对象的同步块索引中对的位设为 1。一个对象被标记后, CLR 会检查那个对象中的根,标记它们引用的对象。如果发现对象已经标记,就不重新检查对象的字段。这就避免了因为循环引用而产生死循环。检查完毕后,堆中的对象要么已标记,要么未标记。已标记的对象不能被垃圾回收,因为至少有一个根在引用它。我们说这种对象的可达 (reachable) 的,因为应用程序代码可通过仍在引用它的变量抵达 (或访问) 它。未标记的对象是不可达 (unreachable) 的,因为应用程序中不存在使对象能被再次访问的根。CLR 知道哪些对象可以幸存,哪些可以删除后,就进入 GC 的压缩 (compact) 阶段。压缩意味着托管堆解决了本机 (原生) 堆的控件碎片化问题。如果 CLR 在一次 GC 之后回收不了内存,而且进程中没有空间来分配新的 GC 区域,就说明该进程的内存已耗尽。此时,试图分配更多内存的 new 操作符会抛出 OutOfMemoryException 。应用程序可捕捉该异常并从中恢复。但大多数应用程序都不会这么做;相反,异常会成为未处理异常,Windows 将终止进程并回收进程使用的全部内存。

# Generations: Improving Performance

The CLR’s GC is a generational garbage collector (also known as an ephemeral garbage collector, although I don’t use the latter term in this book). A generational GC makes the following assumptions about your code:

  • The newer an object is, the shorter its lifetime will be.

  • The older an object is, the longer its lifetime will be.

  • Collecting a portion of the heap is faster than collecting the whole heap.

Numerous studies have demonstrated the validity of these assumptions for a very large set of existing applications, and these assumptions have influenced how the garbage collector is implemented. In this section, I’ll describe how generations work.

When initialized, the managed heap contains no objects. Objects added to the heap are said to be in generation 0. Stated simply, objects in generation 0 are newly constructed objects that the garbage collector has never examined. Figure 21-4 shows a newly started application with five objects allocated (A through E). After a while, objects C and E become unreachable.

image-20221127172823035

When the CLR initializes, it selects a budget size (in kilobytes) for generation 0. So if allocating a new object causes generation 0 to surpass its budget, a garbage collection must start. Let’s say that objects A through E fill all of generation 0. When object F is allocated, a garbage collection must start. The garbage collector will determine that objects C and E are garbage and will compact object D, causing it to be adjacent to object B. The objects that survive the garbage collection (objects A, B, and D) are said to be in generation 1. Objects in generation 1 have been examined by the garbage collector once. The heap now looks like Figure 21-5.

image-20221127172849992

After a garbage collection, generation 0 contains no objects. As always, new objects will be allocated in generation 0. Figure 21-6 shows the application running and allocating objects F through K. In addition, while the application was running, objects B, H, and J became unreachable and should have their memory reclaimed at some point.

image-20221127172919254

Now let’s say that attempting to allocate object L would put generation 0 over its budget. Because generation 0 has reached its budget, a garbage collection must start. When starting a garbage collection, the garbage collector must decide which generations to examine. Earlier, I said that when the CLR initializes, it selects a budget for generation 0. Well, it also selects a budget for generation 1.

When starting a garbage collection, the garbage collector also sees how much memory is occupied by generation 1. In this case, generation 1 occupies much less than its budget, so the garbage collector examines only the objects in generation 0. Look again at the assumptions that the generational garbage collector makes. The first assumption is that newly created objects have a short lifetime. So generation 0 is likely to have a lot of garbage in it, and collecting generation 0 will therefore reclaim a lot of memory. The garbage collector will just ignore the objects in generation 1, which will speed up the garbage collection process.

Obviously, ignoring the objects in generation 1 improves the performance of the garbage collector. However, the garbage collector improves performance more because it doesn’t traverse every object in the managed heap. If a root or an object refers to an object in an old generation, the garbage collector can ignore any of the older objects’ inner references, decreasing the amount of time required to build the graph of reachable objects. Of course, it’s possible that an old object’s field refers to a new object. To ensure that the updated fields of these old objects are examined, the garbage collector uses a mechanism internal to the JIT compiler that sets a bit when an object’s reference field changes. This support lets the garbage collector know which old objects (if any) have been written to because the last collection. Only old objects that have had fields change need to be examined to see whether they refer to any new object in generation 0.

💡注意:Microsoft 的性能测试表明,对第 0 代执行一次垃圾回收,所花的时间不超过 1 毫秒。Microsoft 的目标是使垃圾回收所花的时间不超过一次普通的内存页面错误 (page fault) 的时间。

A generational garbage collector also assumes that objects that have lived a long time will continue to live. So it’s likely that the objects in generation 1 will continue to be reachable from the application. Therefore, if the garbage collector were to examine the objects in generation 1, it probably wouldn’t find a lot of garbage. As a result, it wouldn’t be able to reclaim much memory. So it is likely that collecting generation 1 is a waste of time. If any garbage happens to be in generation 1, it just stays there. The heap now looks like Figure 21-7.

image-20221127173038594

As you can see, all of the generation 0 objects that survived the collection are now part of generation 1. Because the garbage collector didn’t examine generation 1, object B didn’t have its memory reclaimed even though it was unreachable at the time of the last garbage collection. Again, after a collection, generation 0 contains no objects and is where new objects will be placed. In fact, let’s say that the application continues running and allocates objects L through O. And while running, the application stops using objects G, L, and M, making them all unreachable. The heap now looks like Figure 21-8.

image-20221127173101096

Let’s say that allocating object P causes generation 0 to exceed its budget, causing a garbage collection to occur. Because the memory occupied by all of the objects in generation 1 is less than its budget, the garbage collector again decides to collect only generation 0, ignoring the unreachable objects in generation 1 (objects B and G). After the collection, the heap looks like Figure 21-9.

image-20221127173127959

In Figure 21-9, you see that generation 1 keeps growing slowly. In fact, let’s say that generation 1 has now grown to the point in which all of the objects in it occupy its full budget. At this point, the application continues running (because a garbage collection just finished) and starts allocating objects P through S, which fill generation 0 up to its budget. The heap now looks like Figure 21-10.

image-20221127173152883

When the application attempts to allocate object T, generation 0 is full, and a garbage collection must start. This time, however, the garbage collector sees that the objects in generation 1 are occupying so much memory that generation 1’s budget has been reached. Over the several generation 0 collections, it’s likely that a number of objects in generation 1 have become unreachable (as in our example). So this time, the garbage collector decides to examine all of the objects in generation 1 and generation 0. After both generations have been garbage collected, the heap now looks like Figure 21-11.

image-20221127173218007

As before, any objects that were in generation 0 that survived the garbage collection are now in generation 1; any objects that were in generation 1 that survived the collection are now in generation 2. As always, generation 0 is empty immediately after a garbage collection and is where new objects will be allocated. Objects in generation 2 are objects that the garbage collector has examined two or more times. There might have been several collections, but the objects in generation 1 are examined only when generation 1 reaches its budget, which usually requires several garbage collections of generation 0.

The managed heap supports only three generations: generation 0, generation 1, and generation 2; there is no generation 3.3 When the CLR initializes, it selects budgets for all three generations. However, the CLR’s garbage collector is a self-tuning collector. This means that the garbage collector learns about your application’s behavior whenever it performs a garbage collection. For example, if your application constructs a lot of objects and uses them for a very short period of time, it’s possible that garbage collecting generation 0 will reclaim a lot of memory. In fact, it’s possible that the memory for all objects in generation 0 can be reclaimed.

If the garbage collector sees that there are very few surviving objects after collecting generation 0, it might decide to reduce the budget of generation 0. This reduction in the allotted space will mean that garbage collections occur more frequently but will require less work for the garbage collector, so your process’s working set will be small. In fact, if all objects in generation 0 are garbage, a garbage collection doesn’t have to compact any memory; it can simply set NextObjPtr back to the beginning of generation 0, and then the garbage collection is performed. Wow, this is a fast way to reclaim memory!

💡注意:如果应用程序的一些线程大多数时候都在栈顶闲置,垃圾回收器工作起来就尤其 “得心应手”。在这种情况下,线程有事做就会被唤醒,创建一组短期存活的对象,返回,然后继续睡眠。许多应用程序都是这样建构的。例如,GUI 应用程序大多数时候都让 GUI 线程处在一个消息循环中。用户偶尔产生一些输入 (触摸、鼠标或键盘事件),线程被唤醒,处理输入并回到消息泵。然后,为了处理输入而创建的大多数对象都会成为垃圾。

类似地,对于服务器应用程序,线程在池里呆着,等着客户端请求进入。有客户端请求进入后,线程创建新对象,代表客户端执行工作。结果发回客户端后,线程将回到线程池,创建的所有对象现在都成了垃圾。

On the other hand, if the garbage collector collects generation 0 and sees that there are a lot of surviving objects, not a lot of memory was reclaimed in the garbage collection. In this case, the garbage collector will grow generation 0’s budget. Now, fewer collections will occur, but when they do, a lot more memory should be reclaimed. By the way, if insufficient memory has been reclaimed after a collection, the garbage collector will perform a full collection before throwing an OutOfMemoryException

Throughout this discussion, I’ve been talking about how the garbage collector dynamically modifies generation 0’s budget after every collection. But the garbage collector also modifies the budgets of generation 1 and generation 2 by using similar heuristics. When these generations are garbage collected, the garbage collector again sees how much memory is reclaimed and how many objects survived. Based on the garbage collector’s findings, it might grow or shrink the thresholds of these generations as well to improve the overall performance of the application. The end result is that the garbage collector fine-tunes itself automatically based on the memory load required by your application—this is very cool!

The following GCNotification class raises an event whenever a generation 0 or generation 2 collection occurs. With these events, you could have the computer beep whenever a collection occurs or you calculate how much time passes between collections, how much memory is allocated between collections, and more. With this class, you could easily instrument your application to get a better understanding of how your application uses memory.

public static class GCNotification {
 private static Action<Int32> s_gcDone = null; // The event's field
 public static event Action<Int32> GCDone {
 add {
 // If there were no registered delegates before, start reporting notifications now
 if (s_gcDone == null) { new GenObject(0); new GenObject(2); }
 s_gcDone += value;
 }
 remove { s_gcDone -= value; }
 }
 private sealed class GenObject {
 private Int32 m_generation;
 public GenObject(Int32 generation) { m_generation = generation; }
 ~GenObject() { // This is the Finalize method
 // If this object is in the generation we want (or higher), 
 // notify the delegates that a GC just completed
 if (GC.GetGeneration(this) >= m_generation) {
 Action<Int32> temp = Volatile.Read(ref s_gcDone);
 if (temp != null) temp(m_generation);
 }
 // Keep reporting notifications if there is at least one delegate registered,
 // the AppDomain isn't unloading, and the process isn’t shutting down
 if ((s_gcDone != null) 
 && !AppDomain.CurrentDomain.IsFinalizingForUnload() 
 && !Environment.HasShutdownStarted) {
 // For Gen 0, create a new object; for Gen 2, resurrect the object 
 // & let the GC call Finalize again the next time Gen 2 is GC'd
 if (m_generation == 0) new GenObject(0);
 else GC.ReRegisterForFinalize(this);
 } else { /* Let the objects go away */ }
 }
 }
}

# Garbage Collection Triggers

As you know, the CLR triggers a GC when it detects that generation 0 has filled its budget. This is the most common trigger of a GC; however, there are additional GC triggers as listed here:

  • Code explicitly calls System.GC’s static Collect method Code can explicitly request that the CLR perform a collection. Although Microsoft strongly discourages such requests, at times it might make sense for an application to force a collection. I discuss this more in the “Forcing Garbage Collections” section later in this chapter.

  • Windows is reporting low memory conditions The CLR internally uses the Win32 CreateMemoryResourceNotification and QueryMemoryResourceNotification functions to monitor system memory overall. If Windows reports low memory, the CLR will force a garbage collection in an effort to free up dead objects to reduce the size of a process’s working set.

  • The CLR is unloading an AppDomain When an AppDomain unloads, the CLR considers nothing in the AppDomain to be a root, and a garbage collection consisting of all generations is performed. I’ll discuss AppDomains in Chapter 22, “CLR Hosting and AppDomains.”

  • The CLR is shutting down The CLR shuts down when a process terminates normally (as opposed to an external shutdown via Task Manager, for example). During this shutdown, the CLR considers nothing in the process to be a root; it allows objects a chance to clean up but the CLR does not attempt to compact or free memory because the whole process is terminating, and Windows will reclaim all of the processes’ memory.

# Large Objects

There is one more performance improvement you might want to be aware of. The CLR considers each single object to be either a small object or a large object. So far, in this chapter, I’ve been focusing on small objects. Today, a large object is 85,000 bytes or more in size.4 The CLR treats large objects slightly differently than how it treats small objects:

  • Large objects are not allocated within the same address space as small objects; they are allocated elsewhere within the process’ address space.

  • Today, the GC doesn’t compact large objects because of the time it would require to move them in memory. For this reason, address space fragmentation can occur between large objects within the process leading to an OutOfMemoryException being thrown. In a future version of the CLR, large objects may participate in compaction.

  • Large objects are immediately considered to be part of generation 2; they are never in generation 0 or 1. So, you should create large objects only for resources that you need to keep alive for a long time. Allocating short-lived large objects will cause generation 2 to be collected more frequently, hurting performance. Usually large objects are large strings (like XML or JSON) or byte arrays that you use for I/O operations, such as reading bytes from a file or network into a buffer so you can process it.

For the most part, large objects are transparent to you; you can simply ignore that they exist and that they get special treatment until you run into some unexplained situation in your program (like why you’re getting address space fragmentation).

# Garbage Collection Modes

When the CLR starts, it selects a GC mode, and this mode cannot change during the lifetime of the process. There are two basic GC modes:

  • Workstation This mode fine-tunes the garbage collector for client-side applications. It is optimized to provide for low-latency GCs in order to minimize the time an application’s threads are suspended so as not to frustrate the end user. In this mode, the GC assumes that other applications are running on the machine and does not hog CPU resources.

  • Server This mode fine-tunes the garbage collector for server-side applications. It is optimized for throughput and resource utilization. In this mode, the GC assumes no other applications (client or server) are running on the machine, and it assumes that all the CPUs on the machine are available to assist with completing the GC. This GC mode causes the managed heap to be split into several sections, one per CPU. When a garbage collection is initiated, the garbage collector dedicates one special thread per CPU; each thread collects its own section in parallel with the other threads. Parallel collections work well for server applications in which the worker threads tend to exhibit uniform behavior. This feature requires the application to be running on a computer with multiple CPUs so that the threads can truly be working simultaneously to attain a performance improvement.

By default, applications run with the Workstation GC mode. A server application (such as ASP.NET or Microsoft SQL Server) that hosts the CLR can request the CLR to load the Server GC. However, if the server application is running on a uniprocessor machine, then the CLR will always use Workstation GC mode. A stand-alone application can tell the CLR to use the Server GC mode by creating a configuration file (as discussed in Chapter 2, “Building, Packaging, Deploying, and Administering Applications and Types,” and Chapter 3, “Shared Assemblies and Strongly Named Assemblies”) that contains a gcServer element for the application. Here’s an example of a configuration file.

<configuration> 
 <runtime> 
 <gcServer enabled="true"/> 
 </runtime> 
</configuration>

When an application is running, it can ask the CLR if it is running in the Server GC mode by querying the GCSettings class’s IsServerGC read-only Boolean property.

using System; 
using System.Runtime; // GCSettings is in this namespace 
public static class Program { 
 public static void Main() { 
 Console.WriteLine("Application is running with server GC=" + GCSettings.IsServerGC); 
 } 
}

In addition to the two modes, the GC can run in two sub-modes: concurrent (the default) or nonconcurrent. In concurrent mode, the GC has an additional background thread that marks objects concurrently while the application runs. When a thread allocates an object that pushes generation 0 over its budget, the GC first suspends all threads and then determines which generations to collect. If the garbage collector needs to collect generation 0 or 1, it proceeds as normal. However, if generation 2 needs collecting, the size of generation 0 will be increased beyond its budget to allocate the new object, and then the application’s threads are resumed.

While the application’s threads are running, the garbage collector has a normal priority background thread that finds unreachable objects. Once found, the garbage collector suspends all threads again and decides whether to compact memory. If the garbage collector decides to compact memory, memory is compacted, root references are fixed up, and the application’s threads are resumed. This garbage collection takes less time than usual because the set of unreachable objects has already been built. However, the garbage collector might decide not to compact memory; in fact, the garbage collector favors this approach. If you have a lot of free memory, the garbage collector won’t compact the heap; this improves performance but grows your application’s working set. When using the concurrent garbage collector, you’ll typically find that your application is consuming more memory than it would with the non-concurrent garbage collector.

You can tell the CLR not to use the concurrent collector by creating a configuration file for the application that contains a gcConcurrent element. Here’s an example of a configuration file.

<configuration> 
 <runtime> 
 <gcConcurrent enabled="false"/> 
 </runtime> 
</configuration>

The GC mode is configured for a process and it cannot change while the process runs. However, your application can have some control over the garbage collection by using the GCSettings class’s GCLatencyMode property. This read/write property can be set to any of the values in the GCLatencyMode enumerated type, as shown in Table 21-1.

The LowLatency mode requires some additional explanation. Typically, you would set this mode, perform a short-term, time-sensitive operation, and then set the mode back to either Batch or Interactive. While the mode is set to LowLatency, the GC will really avoid doing any generation 2 collections because these could take a long time. Of course, if you call GC.Collect(), then generation 2 still gets collected. Also, the GC will perform a generation 2 collection if Windows tells the CLR that system memory is low (see the “Garbage Collection Triggers” section earlier in this chapter).

image-20221127173821580

Under LowLatency mode, it is more likely that your application could get an OutOfMemoryException thrown. Therefore, stay in this mode for as short a time as possible, avoid allocating many objects, avoid allocating large objects, and set the mode back to Batch or Interactive by using a constrained execution region (CER), as discussed in Chapter 20, “Exceptions and State Management.” Also, remember that the latency mode is a process-wide setting and threads may be running concurrently. These other threads could even change this setting while another thread is using it, so you may want to update some kind of counter (manipulated via Interlocked methods) when you have multiple threads manipulating this setting. Here is some code showing how to use the LowLatency mode.

private static void LowLatencyDemo() {
 GCLatencyMode oldMode = GCSettings.LatencyMode;
 System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions();
 try {
 GCSettings.LatencyMode = GCLatencyMode.LowLatency;
 // Run your code here...
 }
 finally {
 GCSettings.LatencyMode = oldMode;
 }
}

# Forcing Garbage Collections

The System.GC type allows your application some direct control over the garbage collector. For starters, you can query the maximum generation supported by the managed heap by reading the GC.MaxGeneration property; this property always returns 2.

You can also force the garbage collector to perform a collection by calling GC class’s Collect method, optionally passing in a generation to collect up to, a GCCollectionMode, and a Boolean ndicating whether you want to perform a blocking (non-current) or background (concurrent) collection. Here is the signature of the most complex overload of the Collect method.

void Collect(Int32 generation, GCCollectionMode mode, Boolean blocking);

The GCCollectionMode type is an enum whose values are described in Table 21-2.

image-20221127173946803

Under most circumstances, you should avoid calling any of the Collect methods; it’s best just to let the garbage collector run on its own accord and fine-tune its generation budgets based on actual application behavior. However, if you’re writing a console user interface (CUI) or GUI application, your application code owns the process and the CLR in that process. For these application types, you might want to suggest a garbage collection to occur at certain times using a GCCollectionMode of Optimized. Normally, modes of Default and Forced are used for debugging, testing, and looking for memory leaks.

For example, you might consider calling the Collect method if some non-recurring event has just occurred that has likely caused a lot of old objects to die. The reason that calling Collect in such a circumstance may not be so bad is that the GC’s predictions of the future based on the past are not likely to be accurate for non-recurring events. For example, it might make sense for your application to force a full GC of all generations after your application initializes or after the user saves a data file. Because calling Collect causes the generation budgets to adjust, do not call Collect to try to improve your application’s response time; call it to reduce your process’s working set.

For some applications (especially server applications that tend to keep a lot of objects in memory), the time required for the GC to do a full collection that includes generation 2 can be excessive. In fact, if the collection takes a very long time to complete, then client requests might time out. To help these kinds of applications, the GC class offers a RegisterForFullGCNotification method. Using this method and some additional helper methods (WaitForFullGCApproach, WaitForFullGCComplete, and CancelFullGCNotification), an application can now be notified when the garbage collector is getting close to performing a full collection. The application can then call GC.Collect to force a collection at a more opportune time, or the application could communicate with another server to better load balance the client requests. For more information, examine these methods and the “Garbage Collection Notifications” topic in the Microsoft .NET Framework SDK documentation. Note that you should always call the WaitForFullGCApproach and WaitForFullGCComplete methods in pairs because the CLR handles them as pairs internally.

# Monitoring Your Application’s Memory Usage

Within a process, there are a few methods that you can call to monitor the garbage collector. Specifically, the GC class offers the following static methods, which you can call to see how many collections have occurred of a specific generation or how much memory is currently being used by objects in the managed heap.

Int32 CollectionCount(Int32 generation);
Int64 GetTotalMemory(Boolean forceFullCollection);

To profile a particular code block, I have frequently written code to call these methods before and after the code block and then calculate the difference. This gives me a very good indication of how my code block has affected my process’s working set and indicates how many garbage collections occurred while executing the code block. If the numbers are high, I know to spend more time tuning the algorithms in my code block.

You can also see how much memory is being used by individual AppDomains as opposed to the whole process. For more information about this, see the “AppDomain Monitoring” section in Chapter 22.

When you install the .NET Framework, it installs a set of performance counters that offer a lot of real-time statistics about the CLR’s operations. These statistics are visible via the PerfMon.exe tool or the System Monitor ActiveX control that ships with Windows. The easiest way to access the System Monitor control is to run PerfMon.exe and click the + toolbar button, which causes the Add Counters dialog box shown in Figure 21-12 to appear.

image-20221129100443706

FIGURE 21-12 PerfMon.exe showing the .NET CLR Memory counters.

To monitor the CLR’s garbage collector, select the .NET CLR Memory performance object. Then select a specific application from the instance list box. Finally, select the set of counters that you’re interested in monitoring, click Add, and then click OK. At this point, the System Monitor will graph the selected real-time statistics. For an explanation of a particular counter, select the desired counter and then select the Show Description check box.

Another great tool for analyzing the memory and performance of your application is PerfView. This tool can collect Event Tracing for Windows (ETW) logs and process them. The best way to acquire this tool is for you to search the web for PerfView. Finally, you should look into using the SOS Debugging Extension (SOS.dll), which can often offer great assistance when debugging memory problems and other CLR problems. For memory-related actions, the SOS Debugging Extension allows you to see how much memory is allocated within the process to the managed heap, displays all objects registered for finalization in the finalization queue, displays the entries in the GCHandle table per AppDomain or for the entire process, shows the roots that are keeping an object alive in the heap, and more.

💡小结:CLR 的 GC 是基于代的垃圾回收器 (generational garbage collector),它对你的代码做出了以下几点假设:对象越新,生存期越短;对象越老,生存期越长;回收堆的一部分,速度快于回收整个堆。托管堆在初始化时不包含对象。添加到堆的对象称为第 0 代对象。简单地说,第 0 代对象就是那些新构造的对象,垃圾回收器从未检查过它们。CLR 初始化时为第 0 代对象选择一个预算容量 (以 KB 为单位)。如果分配一个新对象造成第 0 代超过预算,就必须启动一次垃圾回收。开始一次垃圾回收时,垃圾回收器还会检查第 1 代占用了多少内存。如果第 1 代占用的内存远少于预算,所以垃圾回收器只检查第 0 代中的对象。回顾一下基于代的垃圾回收器做出的假设。第一个假设是越新的对象活得越短。因此,第 0 代包含更多垃圾的可能性很大,能回收更多的内存。由于忽略了第 1 代中的对象,所以加快了垃圾回收速度。显然,忽略第 1 代中的对象能提升垃圾回收器的性能。但对性能有更大提振作用的是现在不必遍历托管堆中的每个对象。如果根或对象引用了老一代的某个对象,垃圾回收器就可以忽略老对象内部的所有引用,能在更短的时间内构造好可达对象图 (graph of reachable object)。当然,老对象的字段也有可能引用新对象。为了确保对老对象的已更新字段进行检查,垃圾回收器利用了 JIT 编译器内部的一个机制。这个机制在对象的引用字段发生变化时,会设置一个对应的位标志。这样,垃圾回收器就知道自上一次垃圾回收以来,哪些老对象 (如果有的话) 已被写入。只有字段发生变化的老对象才需检查是否引用了第 0 代中的任何新对象。当垃圾回收器发现第 1 代占用了太多内存,以至于用完了预算。在前几次对第 0 代进行回收时,第 1 代可能已经有许多对象变得不可达。所以这时垃圾回收器就会决定检查第 1 代和第 0 代中的所有对象。托管堆只支持三代:第 0 代、第 1 代和第 2 代。没有第 3 代。CLR 初始化时,会为每一代选择预算。然而,CLR 的垃圾回收器是自调节的。这意味着垃圾回收器会在执行垃圾回收的过程中了解应用程序的行为。如果垃圾回收器发现在回收 0 代后存活下来的对象很少,就可能减少第 0 代的预算。已分配空间的减少意味着垃圾回收将更频繁地发生,但垃圾回收器每次做的事情也减少了,这减小了进程的工作集。另一方面,如果垃圾回收器回收了第 0 代,发现还有很多对象存活,没有多少内存被回收,就会增大第 0 代的预算。现在,垃圾回收的次数将减少,但每次进行垃圾回收时,回收的内存要多得多。顺便说一句,如果没有回收到足够的内存,垃圾回收器会执行一次完整回收。如果还是不够,就抛出 OutOfMemoryException 异常。到目前为止,只是讨论了每次垃圾回收后如何动态代用第 0 代的预算。但垃圾回收器还会用类似的启发式算法调整第 1 代和第 2 代的预算。这些代码垃圾回收时,垃圾回收器会检查有多少内存被回收,以及有多少对象幸存。基于这些结果,垃圾回收器可能增大或减小这些代的预算,从而提升应用程序的总体性能。最终的结果是,垃圾回收器会根据应用程序要求的内存负载来自动优化。前面说过,CLR 在检测第 0 代超过预算时触发一次 GC。这是 GC 最常见的触发条件,在其他一些条件下也会触发:代码显式调用 System.GC 的静态 Collect 方法;Windows 报告低内存情况;CLR 正在卸载 AppDomain;CLR 正在关闭。还有另一个性能提升举措值得注意。CLR 将对象分为大对象和小对象。目前认为 85000 字节或更大的对象是大对象。CLR 以不同方式对待大小对象。大对象不是在小对象的地址空间分配,而是在进程地址空间的其他地方分配。前版本的 GC 不压缩大对象,因为在内存中移动它们代价过高。但这可能在进程中的大对象之间造成地址空间的碎片化,以至于抛出 OutOfMemoryException 。大对象总是第 2 代,绝不可能是第 0 代或 第 1 代。所以只能为需要长时间存活的资源创建大对象。分配短时间存活的大对象。分配短时间存活的大对象会导致第 2 代被更频繁地回收,会损害性能。大对象一般是大字符串 (比如 XML 或 JSON) 或者用于 I/O 操作的字节数组 (比如从文件或网络将字节读入缓冲区以便处理)。CLR 启动时会选择一个 GC 模式,进程终止前该模式不会改变。有两个基本 GC 模式。它们分别是工作站模式和服务器模式。应用程序默认以 “工作站” GC 模式运行。寄宿了 CLR 的服务器应用程序 (比如 ASP.NET 或 Microsoft SQL Server) 可请求 CLR 加载 “服务器” GC。但如果服务器应用程序在单处理器计算机上运行,CLR 将总是使用 “工作站” GC 模式。除了这两种主要模式,GC 还支持两种子模式:并发 (默认) 或非并发。在并发方式中,垃圾回收器有一个额外的后台线程,它能在应用程序运行时并发标记对象。应用程序线程运行时,垃圾回收器运行一个普通优先级的后台线程来查找不可达对象。找到之后,垃圾回收器再次挂起所有线程,判断是否要压缩 (移动) 内存。如决定压缩,内存会被压缩,根引用会被修正,应用程序线程恢复运行。这一次垃圾回收花费的时间比平常少,因为不可达对象集合已构造好了。但垃圾回收器也可能决定不压缩内存;事实上,垃圾回收器更倾向于选择不压缩。可用内存多,垃圾回收器便不会压缩堆;这有利于增强性能,但会增大应用程序的工作集。使用并发垃圾回收器,应用程序消耗的内存通常比使用并发垃圾回收器多。GC 模式是针对进程配置的,进程运行期间不能更改。但是,你的应用程序可以使用 GCSettings 类的 GCLatencyMode 属性对垃圾回收进行某种程度的控制。在模式设为 LowLatency 期间,垃圾回收器会全力避免任何第 2 代回收,因为那样花费的时间较多。当然,调用 GC.Collect() 仍会回收第 2 代。在 LowLatency 模式中,应用程序抛出 OutOfMemoryException 的机率会大一些。所以,出于该模式的时间应尽量短,避免分配太多对象,避免分配大对象,并用一个约束执行区域 (CER) 将模式设回 BatchInteractive 。另外注意,延迟模式是进程级的设置,而可能有多个线程并发运行。大多时候都要避免调用任何 Collect 方法:最好让垃圾回收器自己斟酌执行,让它根据应用程序的行为调整各个代的预算。但假如刚才发生了某个非重复性的事件,并导致大量旧对象死亡,就可考虑手动调用一次 Collect 方法。由于是非重复性事件,垃圾回收器基于历史的预测可能变得不准确。所以,这时调用 Collect 方法是合适的。

# Working with Types Requiring Special Cleanup

At this point, you should have a basic understanding of garbage collection and the managed heap, including how the garbage collector reclaims an object’s memory. Fortunately for us, most types need only memory to operate. However, some types require more than just memory to be useful; some types require the use of a native resource in addition to memory.

The System.IO.FileStream type, for example, needs to open a file (a native resource) and store the file’s handle. Then the type’s Read and Write methods use this handle to manipulate the file. Similarly, the System.Threading.Mutex type opens a Windows mutex kernel object (a native resource) and stores its handle, using it when the Mutex’s methods are called.

If a type wrapping a native resource gets GC’d, the GC will reclaim the memory used by the object in the managed heap; but the native resource, which the GC doesn’t know anything about, will be leaked. This is clearly not desirable, so the CLR offers a mechanism called finalization. Finalization allows an object to execute some code after the object has been determined to be garbage but before the object’s memory is reclaimed from the managed heap. All types that wrap a native resource— such as a file, network connection, socket, or mutex—support finalization. When the CLR determines that one of these objects is no longer reachable, the object gets to finalize itself, releasing the native resource it wraps, and then, later, the GC will reclaim the object from the managed heap.

System.Object, the base class of everything, defines a protected and virtual method called Finalize. When the garbage collector determines that an object is garbage, it calls the object’s Finalize method (if it is overridden). Microsoft’s C# team felt that Finalize methods were a special kind of method requiring special syntax in the programming language (similar to how C# requires special syntax to define a constructor). So, in C#, you must define a Finalize method by placing a tilde symbol (~) in front of the class name, as shown in the following code sample.

internal sealed class SomeType { 
 // This is the Finalize method 
 ~SomeType() { 
 // The code here is inside the Finalize method 
 } 
}

If you were to compile this code and examine the resulting assembly with ILDasm.exe, you’d see that the C# compiler did, in fact, emit a protected override method named Finalize into the module’s metadata. If you examined the Finalize method’s IL code, you’d also see that the code inside the method’s body is emitted into a try block, and that a call to base.Finalize is emitted into a finally block.

💡重要提示 如果熟悉 C++,会发现 C# Finalize 方法的特殊语法非常类似于 C++ 析构器。事实上,在 C# 编程语言规范的早期版本中,真的是将该方法称为析构器。但 Finalize 方法的工作方式和 C++ 析构器完全不同,这会使从一种语言迁移到另一种语言的开发人员产生混淆。

问题在于,这些开发人员可能错误地以为使用 C# 析构器语法意味着类型的实例会被确定性析构,就像在 C++ 中那样。但 CLR 不支持确定性析构,而作为面向 CLR 的语言,C# 也无法提供这种机制。

Finalize methods are called at the completion of a garbage collection on objects that the GC has determined to be garbage. This means that the memory for these objects cannot be reclaimed right away because the Finalize method might execute code that accesses a field. Because a finalizable object must survive the collection, it gets promoted to another generation, forcing the object to live much longer than it should. This is not ideal in terms of memory consumption and is why you should avoid finalization when possible. To make matters worse, when finalizable objects get promoted, any object referred to by its fields also get promoted because they must continue to live too. So, try to avoid defining finalizable objects with reference type fields.

Furthermore, be aware of the fact that you have no control over when the Finalize method will execute. Finalize methods run when a garbage collection occurs, which may happen when your application requests more memory. Also, the CLR doesn’t make any guarantees as to the order in which Finalize methods are called. So, you should avoid writing a Finalize method that accesses other objects whose type defines a Finalize method; those other objects could have been finalized already. However, it is perfectly OK to access value type instances or reference type objects that do not define a Finalize method. You also need to be careful when calling static methods because these methods can internally access objects that have been finalized, causing the behavior of the static method to become unpredictable.

The CLR uses a special, high-priority dedicated thread to call Finalize methods to avoid some deadlock scenarios that could occur otherwise.5 If a Finalize method blocks (for example, enters an infinite loop or waits for an object that is never signaled), this special thread can’t call any more Finalize methods. This is a very bad situation because the application will never be able to reclaim the memory occupied by the finalizable objects—the application will leak memory as long as it runs. If a Finalize method throws an unhandled exception, then the process terminates; there is no way to catch this exception.

So, as you can see, there are a lot of caveats related to Finalize methods and they must be used with caution. Specifically, they are designed for releasing native resources. To simplify working with them, it is highly recommended that developers avoid overriding Object’s Finalize method; instead, use helper classes that Microsoft now provides in the Framework Class Library (FCL). The helper classes override Finalize and add some special CLR magic I’ll talk about as we go on. You will then derive your own classes from the helper classes and inherit the CLR magic.

If you are creating a managed type that wraps a native resource, you should first derive a class from a special base class called System.Runtime.InteropServices.SafeHandle, which looks like the following (I’ve added comments in the methods to indicate what they do).

public abstract class SafeHandle : CriticalFinalizerObject, IDisposable { 
 // This is the handle to the native resource 
 protected IntPtr handle; 
 protected SafeHandle(IntPtr invalidHandleValue, Boolean ownsHandle) { 
 this.handle = invalidHandleValue; 
 // If ownsHandle is true, then the native resource is closed when 
 // this SafeHandle-derived object is collected 
 } 
 protected void SetHandle(IntPtr handle) { 
 this.handle = handle; 
 } 
 // You can explicitly release the resource by calling Dispose
 // This is the IDisposable interface’s Dispose method 
 public void Dispose() { Dispose(true); } 
 // The default Dispose implementation (shown here) is exactly what you want.
 // Overriding this method is strongly discouraged. 
 protected virtual void Dispose(Boolean disposing) { 
 // The default implementation ignores the disposing argument.
 // If resource already released, return 
 // If ownsHandle is false, return 
 // Set flag indicating that this resource has been released 
 // Call virtual ReleaseHandle method 
 // Call GC.SuppressFinalize(this) to prevent Finalize from being called 
 // If ReleaseHandle returned true, return 
 // If we get here, fire ReleaseHandleFailed Managed Debugging Assistant (MDA) 
 } 
 // The default Finalize implementation (shown here) is exactly what you want.
 // Overriding this method is very strongly discouraged. 
 ~SafeHandle() { Dispose(false); } 
 // A derived class overrides this method to implement the code that releases the resource 
 protected abstract Boolean ReleaseHandle(); 
 public void SetHandleAsInvalid() { 
 // Set flag indicating that this resource has been released 
 // Call GC.SuppressFinalize(this) to prevent Finalize from being called 
 } 
 public Boolean IsClosed { 
 get { 
 // Returns flag indicating whether resource was released 
 } 
 } 
 public abstract Boolean IsInvalid { 
 // A derived class overrides this property.
 // The implementation should return true if the handle's value doesn't
 // represent a resource (this usually means that the handle is 0 or -1)
 get;
 } 
 // These three methods have to do with security and reference counting; 
 // I'll talk about them at the end of this section
 public void DangerousAddRef(ref Boolean success) {...} 
 public IntPtr DangerousGetHandle() {...} 
 public void DangerousRelease() {...} 
}

The first thing to notice about the SafeHandle class is that it is derived from CriticalFinalizerObject, which is defined in the System.Runtime.ConstrainedExecution namespace. The CLR treats this class and classes derived from it in a very special manner. In particular, the CLR endows this class with three cool features:

  • The first time an object of any CriticalFinalizerObject-derived type is constructed, the CLR immediately JIT-compiles all of the Finalize methods in the inheritance hierarchy. Compiling these methods upon object construction guarantees that the native resource will be released when the object is determined to be garbage. Without this eager compiling of the Finalize method, it would be possible to allocate the native resource and use it, but not to get rid of it. Under low memory conditions, the CLR might not be able to find enough memory to compile the Finalize method, which would prevent it from executing, causing the native resource to leak. Or the resource might not be freed if the Finalize method contained code that referred to a type in another assembly, and the CLR failed to locate this other assembly.

  • The CLR calls the Finalize method of CriticalFinalizerObject-derived types after calling the Finalize methods of non–CriticalFinalizerObject-derived types. This ensures that managed resource classes that have a Finalize method can access CriticalFinalizerObject-derived objects within their Finalize methods successfully. For example, the FileStream class’s Finalize method can flush data from a memory buffer to an underlying disk with confidence that the disk file has not been closed yet.

  • The CLR calls the Finalize method of CriticalFinalizerObject-derived types if an AppDomain is rudely aborted by a host application (such as SQL Server or ASP.NET). This also is part of ensuring that the native resource is released even in a case in which a host application no longer trusts the managed code running inside of it.

The second thing to notice about SafeHandle is that the class is abstract; it is expected that another class will be derived from SafeHandle, and this class will provide a constructor that invokes the protected constructor, the abstract method ReleaseHandle, and the abstract IsInvalid property get accessor method.

Most native resources are manipulated with handles (32-bit values on a 32-bit system and 64-bit values on a 64-bit system). So the SafeHandle class defines a protected IntPtr field called handle. In Windows, most handles are invalid if they have a value of 0 or -1. The Microsoft.Win32.SafeHandles namespace contains another helper class called SafeHandleZeroOrMinusOneIsInvalid, which looks like this.

public abstract class SafeHandleZeroOrMinusOneIsInvalid : SafeHandle { 
 protected SafeHandleZeroOrMinusOneIsInvalid(Boolean ownsHandle) 
 : base(IntPtr.Zero, ownsHandle) { 
 } 
 public override Boolean IsInvalid { 
 get { 
 if (base.handle == IntPtr.Zero) return true; 
 if (base.handle == (IntPtr) (-1)) return true; 
 return false; 
 } 
 } 
}

Again, you’ll notice that the SafeHandleZeroOrMinusOneIsInvalid class is abstract, and therefore, another class must be derived from this one to override the protected constructor and the abstract method ReleaseHandle. The .NET Framework provides just a few public classes derived from SafeHandleZeroOrMinusOneIsInvalid, including SafeFileHandle, SafeRegistryHandle, SafeWaitHandle, and SafeMemoryMappedViewHandle. Here is what the SafeFileHandle class looks like.

public sealed class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid { 
 public SafeFileHandle(IntPtr preexistingHandle, Boolean ownsHandle) 
 : base(ownsHandle) { 
 base.SetHandle(preexistingHandle); 
 } 
 protected override Boolean ReleaseHandle() { 
 // Tell Windows that we want the native resource closed. 
 return Win32Native.CloseHandle(base.handle); 
 } 
}

The SafeWaitHandle class is implemented similarly to the SafeFileHandle class just shown. The only reason why there are different classes with similar implementations is to achieve type safety; the compiler won’t let you use a file handle as an argument to a method that expects a wait handle, and vice versa. The SafeRegistryHandle class’s ReleaseHandle method calls the Win32 RegCloseKey function.

It would be nice if the .NET Framework included additional classes that wrap various native resources. For example, one could imagine classes such as SafeProcessHandle, SafeThreadHandle, SafeTokenHandle, SafeLibraryHandle (its ReleaseHandle method would call the Win32 FreeLibrary function), SafeLocalAllocHandle (its ReleaseHandle method would call the Win32 LocalFree function), and so on.

All of the classes just listed (and more) actually do ship with the Framework Class Library (FCL). However, these classes are not publicly exposed; they are all internal to the assemblies that define them. Microsoft didn’t expose these classes publicly because they didn’t want to document them and do full testing of them. However, if you need any of these classes for your own work, I’d recommend that you use a tool such as ILDasm.exe or some IL decompiler tool to extract the code for these classes and integrate that code into your own project’s source code. All of these classes are trivial to implement, and writing them yourself from scratch would also be quite easy.

The SafeHandle-derived classes are extremely useful because they ensure that the native resource is freed when a GC occurs. In addition to what we’ve already discussed, SafeHandle offers two more capabilities. First, the CLR gives SafeHandle-derived types special treatment when used in scenarios in which you are interoperating with native code. For example, let’s examine the following code.

using System; 
using System.Runtime.InteropServices; 
using Microsoft.Win32.SafeHandles; 
internal static class SomeType { 
 [DllImport("Kernel32", CharSet=CharSet.Unicode, EntryPoint="CreateEvent")] 
 // This prototype is not robust 
 private static extern IntPtr CreateEventBad(
 IntPtr pSecurityAttributes, Boolean manualReset, Boolean initialState, String name); 
 // This prototype is robust 
 [DllImport("Kernel32", CharSet=CharSet.Unicode, EntryPoint="CreateEvent")] 
 private static extern SafeWaitHandle CreateEventGood(
 IntPtr pSecurityAttributes, Boolean manualReset, Boolean initialState, String name); 
 public static void SomeMethod() { 
 IntPtr handle = CreateEventBad(IntPtr.Zero, false, false, null); 
 SafeWaitHandle swh = CreateEventGood(IntPtr.Zero, false, false, null); 
 } 
}

You’ll notice that the CreateEventBad method is prototyped as returning an IntPtr, which will return the handle back to managed code; however, interoperating with native code this way is not robust. You see, after CreateEventBad is called (which creates the native event resource), it is possible that a ThreadAbortException could be thrown prior to the handle being assigned to the handle variable. In the rare cases when this would happen, the managed code would leak the native resource. The only way to get the event closed is to terminate the whole process.

The SafeHandle class fixes this potential resource leak. Notice that the CreateEventGood method is prototyped as returning a SafeWaitHandle (instead of an IntPtr). When CreateEventGood is called, the CLR calls the Win32 CreateEvent function. As the CreateEvent function returns to managed code, the CLR knows that SafeWaitHandle is derived from SafeHandle, causing the CLR to automatically construct an instance of the SafeWaitHandle class on the managed heap, passing in the handle value returned from CreateEvent. The constructing of the SafeWaitHandle object and the assignment of the handle happen in native code now, which cannot be interrupted by a ThreadAbortException. Now, it is impossible for managed code to leak this native resource. Eventually, the SafeWaitHandle object will be garbage collected and its Finalize method will be called, ensuring that the resource is released.

One last feature of SafeHandle-derived classes is that they prevent someone from trying to exploit a potential security hole. The problem is that one thread could be trying to use a native resource while another thread tries to free the resource. This could manifest itself as a handle-recycling exploit. The SafeHandle class prevents this security vulnerability by using reference counting. Internally, the SafeHandle class defines a private field that maintains a count. When a SafeHandle-derived object is set to a valid handle, the count is set to 1. Whenever a SafeHandle-derived object is passed as an argument to a native method, the CLR knows to automatically increment the counter. Likewise, when the native method returns to managed code, the CLR knows to decrement the counter. For example, you would prototype the Win32 SetEvent function as follows.

[DllImport("Kernel32", ExactSpelling=true)] 
private static extern Boolean SetEvent(SafeWaitHandle swh);

Now when you call this method passing in a reference to a SafeWaitHandle object, the CLR will increment the counter just before the call and decrement the counter just after the call. Of course, the manipulation of the counter is performed in a thread-safe fashion. How does this improve security? Well, if another thread tries to release the native resource wrapped by the SafeHandle object, the CLR knows that it cannot actually release it because the resource is being used by a native function. When the native function returns, the counter is decremented to 0, and the resource will be released.

If you are writing or calling code to manipulate a handle as an IntPtr, you can access it out of a SafeHandle object, but you should manipulate the reference counting explicitly. You accomplish this via SafeHandle’s DangerousAddRef and DangerousRelease methods. You gain access to the raw handle via the DangerousGetHandle method.

I would be remiss if I didn’t mention that the System.Runtime.InteropServices namespace also defines a CriticalHandle class. This class works exactly as the SafeHandle class in all ways except that it does not offer the reference-counting feature. The CriticalHandle class and the classes derived from it sacrifice security for better performance when you use it (because counters don’t get manipulated). As does SafeHandle, the CriticalHandle class has two types derived from it: CriticalHandleMinusOneIsInvalid and CriticalHandleZeroOrMinusOneIsInvalid. Because Microsoft favors a more secure system over a faster system, the class library includes no types derived from either of these two classes. For your own work, I would recommend that you use CriticalHandle-derived types only if performance is an issue. If you can justify reducing security, you can switch to a CriticalHandle-derived type.

# Using a Type That Wraps a Native Resource

Now that you know how to define a SafeHandle-derived class that wraps a native resource, let’s take a look at how a developer uses it. Let’s start by talking about the common System.IO.FileStream class. The FileStream class offers the ability to open a file, read bytes from the file, write bytes to the file, and close the file. When a FileStream object is constructed, the Win32 CreateFile function is called, the returned handle is saved in a SafeFileHandle object, and a reference to this object is maintained via a private field in the FileStream object. The FileStream class also offers several additional properties (such as Length, Position, CanRead) and methods (such as Read, Write, Flush).

Let’s say that you want to write some code that creates a temporary file, writes some bytes to the file, and then deletes the file. You might start writing the code like this.

using System; 
using System.IO; 
public static class Program { 
 public static void Main() { 
 // Create the bytes to write to the temporary file. 
 Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 }; 
 // Create the temporary file. 
 FileStream fs = new FileStream("Temp.dat", FileMode.Create); 
 // Write the bytes to the temporary file. 
 fs.Write(bytesToWrite, 0, bytesToWrite.Length); 
 // Delete the temporary file. 
 File.Delete("Temp.dat"); // Throws an IOException 
 } 
}

Unfortunately, if you build and run this code, it might work, but most likely it won’t. The problem is that the call to File’s static Delete method requests that Windows delete a file while it is still open. So Delete throws a System.IO.IOException exception with the following string message: The process cannot access the file "Temp.dat" because it is being used by another process.

Be aware that in some cases, the file might actually be deleted! If another thread somehow caused a garbage collection to start after the call to Write and before the call to Delete, the FileStream’s SafeFileHandle field would have its Finalize method called, which would close the file and allow Delete to work. The likelihood of this situation is extremely rare, however, and therefore the previous code will fail more than 99 percent of the time.

Classes that allow the consumer to control the lifetime of native resources it wraps implement the IDisposable interface, which looks like this.

public interface IDisposable { 
 void Dispose();
}

💡重要提示:如果类定义的一个字段的类型实现了 dispose 模式,那么类本身也应实现 dispose 模式,那么类本身也应实现 dispose 模式。 Dispose 方法应 dispose 字段引用的对象。这就允许类的使用者在类上调用 Dispose 来释放对象自身使用的资源。

Fortunately, the FileStream class implements the IDisposable interface and its implementation internally calls Dispose on the FileStream object’s private SafeFileHandle field. Now, we can modify our code to explicitly close the file when we want to as opposed to waiting for some GC to happen in the future. Here’s the corrected source code.

using System; 
using System.IO; 
public static class Program { 
 public static void Main() { 
 // Create the bytes to write to the temporary file. 
 Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 }; 
 // Create the temporary file. 
 FileStream fs = new FileStream("Temp.dat", FileMode.Create); 
 // Write the bytes to the temporary file. 
 fs.Write(bytesToWrite, 0, bytesToWrite.Length); 
 // Explicitly close the file when finished writing to it. 
 fs.Dispose(); 
 // Delete the temporary file. 
 File.Delete("Temp.dat"); // This always works now. 
 } 
}

Now, when File’s Delete method is called, Windows sees that the file isn’t open and successfully deletes it.

Keep in mind that calling Dispose is not required to guarantee native resource cleanup. Native resource cleanup will always happen eventually; calling Dispose lets you control when that cleanup happens. Also, calling Dispose does not delete the managed object from the managed heap. The only way to reclaim memory in the managed heap is for a garbage collection to kick in. This means you can still call methods on the managed object even after you dispose of any native resources it may have been using.

The following code calls the Write method after the file is closed, attempting to write more bytes to the file. Obviously, the bytes can’t be written, and when the code executes, the second call to the Write method throws a System.ObjectDisposedException exception with the following string message: Cannot access a closed file.

using System; 
using System.IO; 
public static class Program { 
 public static void Main() { 
 // Create the bytes to write to the temporary file. 
 Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 }; 
 // Create the temporary file. 
 FileStream fs = new FileStream("Temp.dat", FileMode.Create); 
 // Write the bytes to the temporary file. 
 fs.Write(bytesToWrite, 0, bytesToWrite.Length); 
 // Explicitly close the file when finished writing to it. 
 fs.Dispose(); 
 // Try to write to the file after closing it. 
 fs.Write(bytesToWrite, 0, bytesToWrite.Length); // Throws ObjectDisposedException
 // Delete the temporary file. 
 File.Delete("Temp.dat"); 
 } 
}

Note that no memory corruption occurs here because the memory for the FileStream object still exists in the managed heap; it’s just that the object can’t successfully execute its methods after it is explicitly disposed.

💡重要提示:定义实现 IDisposable 接口的类型时,在它的所有方法和属性中,一定要在对象被显式清理之后抛出一个 System.ObjectDisposedException 。而 Dispose 方法永远不要抛出 ObjectDisposedException ;被多次调用就直接返回。

💡重要提示:我一般不赞成在代码中显式调用 Dispose 。理由是 CLR 的垃圾回收器已经写得非常好,应该放心地把工作交给它。垃圾回收器知道一个对象何时不再由应用程序代码访问,而且只有到那时才会回收对象。而当应用程序代码调用 Dispose 时,实际是在信誓旦旦地说它知道应用程序在什么时候不需要一个对象。但许多应用程序都不可能准确知道一个对象在什么时候不需要。
例如,假定在方法 A 的代码中构造了一个新对象,然后将对该对象的引用传给方法 B。方法 B 可能将对该对象的引用保存到某个内部字段变量 (一个根) 中。但方法 A 并不知道这个情况,它当然可以调用 Dispose 。但在此之后,其他代码可能试图访问该对象,造成抛出一个 ObjectDisposedException 。建议只有在确定必须清理资源 (例如删除打开的文件) 时才调用 Dispose
有可能多个线程同时调用一个对象的 Dispose 。但 Dispose 的设计规范指出 Dispose 不一定要线程安全。原因是代码只有在确定没有别的线程使用对象时才应调用 Dispose

The previous code examples show how to explicitly call a type’s Dispose method. If you decide to call Dispose explicitly, I highly recommend that you place the call in an exception-handling finally block. This way, the cleanup code is guaranteed to execute. So it would be better to write the previous code example as follows.

using System; 
using System.IO; 
public static class Program { 
 public static void Main() { 
 // Create the bytes to write to the temporary file. 
 Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 }; 
 // Create the temporary file. 
 FileStream fs = new FileStream("Temp.dat", FileMode.Create); 
 try { 
 // Write the bytes to the temporary file. 
 fs.Write(bytesToWrite, 0, bytesToWrite.Length); 
 } 
 finally { 
 // Explicitly close the file when finished writing to it. 
 if (fs != null) fs.Dispose(); 
 } 
 // Delete the temporary file. 
 File.Delete("Temp.dat"); 
 } 
}

Adding the exception-handling code is the right thing to do, and you must have the diligence to do it. Fortunately, the C# language provides a using statement, which offers a simplified syntax that produces code identical to the code just shown. Here’s how the preceding code would be rewritten using C#’s using statement.

using System; 
using System.IO; 
public static class Program { 
 public static void Main() { 
 // Create the bytes to write to the temporary file. 
 Byte[] bytesToWrite = new Byte[] { 1, 2, 3, 4, 5 }; 
 // Create the temporary file. 
 using (FileStream fs = new FileStream("Temp.dat", FileMode.Create)) { 
 // Write the bytes to the temporary file. 
 fs.Write(bytesToWrite, 0, bytesToWrite.Length); 
 } 
 // Delete the temporary file. 
 File.Delete("Temp.dat"); 
 } 
}

In the using statement, you initialize an object and save its reference in a variable. Then you access the variable via code contained inside using’s braces. When you compile this code, the compiler automatically emits the try and finally blocks. Inside the finally block, the compiler emits code to cast the object to an IDisposable and calls the Dispose method. Obviously, the compiler allows the using statement to be used only with types that implement the IDisposable interface.

💡注意:C# 的 using 语句支持初始化多个变量,只要这些变量的类型相同。另外, using 语句还允许只使用一个已初始化的变量。欲知详情,请参见文档中的 “ using 语句” 主题。

# An Interesting Dependency Issue

The System.IO.FileStream type allows the user to open a file for reading and writing. To improve performance, the type’s implementation makes use of a memory buffer. Only when the buffer fills does the type flush the contents of the buffer to the file. A FileStream supports the writing of bytes only. If you want to write characters and strings, you can use a System.IO.StreamWriter, as is demonstrated in the following code.

FileStream fs = new FileStream("DataFile.dat", FileMode.Create); 
StreamWriter sw = new StreamWriter(fs); 
sw.Write("Hi there"); 
// The following call to Dispose is what you should do. 
sw.Dispose();
// NOTE: StreamWriter.Dispose closes the FileStream; 
// the FileStream doesn't have to be explicitly closed.

Notice that the StreamWriter’s constructor takes a reference to a Stream object as a parameter, allowing a reference to a FileStream object to be passed as an argument. Internally, the StreamWriter object saves the Stream’s reference. When you write to a StreamWriter object, it internally buffers the data in its own memory buffer. When the buffer is full, the StreamWriter object writes the data to the Stream.

When you’re finished writing data via the StreamWriter object, you should call Dispose. (Because the StreamWriter type implements the IDisposable interface, you can also use it with C#’s using statement.) This causes the StreamWriter object to flush its data to the Stream object and close the Stream object.

💡注意:不需要在 FileStream 对象上显式调用 Dispose ,因为 StreamWriter 会帮你调用。但如果非要显式调用 DisposeFileStream 会发现对象已经清理过了,所以方法什么都不做而直接返回。

What do you think would happen if there were no code to explicitly call Dispose? Well, at some point, the garbage collector would correctly detect that the objects were garbage and finalize them. But the garbage collector doesn’t guarantee the order in which objects are finalized. So if the FileStream object were finalized first, it would close the file. Then when the StreamWriter object was finalized, it would attempt to write data to the closed file, throwing an exception. If, on the other hand, the StreamWriter object were finalized first, the data would be safely written to the file.

How was Microsoft to solve this problem? Making the garbage collector finalize objects in a specific order would have been impossible because objects could contain references to each other, and there would be no way for the garbage collector to correctly guess the order in which to finalize these objects. Here is Microsoft’s solution: the StreamWriter type does not support finalization, and therefore it never flushes data in its buffer to the underlying FileStream object. This means that if you forget to explicitly call Dispose on the StreamWriter object, data is guaranteed to be lost. Microsoft expects developers to see this consistent loss of data and fix the code by inserting an explicit call to Dispose.

💡注意:.NET Framework 支持托管调试助手 (Managed Debugging Assistant,MDA) 功能。启用一个 MDA 后,.NET Framework 就会查找特定种类的常见编程错误,并激活对应的 MDA。在调试器中,激活一个 MDA 感觉就像是抛出一个异常。有一个 MDA 可以检测 StreamWriter 对象没有显式 dispose 就作为垃圾被回收的情况。为了在 Visual Studio 中启用这个 MDA,请打开你的项目,选择 “调试 “|” 异常”。在 “异常” 对话框中,展开 “Managed Debugging Assistants” 节点并滚动到底部,找到名为 StreamWriterBufferedDataLost 的 MDA,勾选 “引发” 即可让 Visual Studio 调试器在 StreamWriter 对象的数据丢失时停止。

# Other GC Features for Use with Native Resources

Sometimes, a native resource consumes a lot of memory, but the managed object wrapping that resource occupies very little memory. The quintessential example of this is the bitmap. A bitmap can occupy several megabytes of native memory, but the managed object is tiny because it contains only an HBITMAP (a 4-byte or 8-byte value). From the CLR’s perspective, a process could allocate hundreds of bitmaps (using little managed memory) before performing a collection. But if the process is manipulating many bitmaps, the process’s memory consumption will grow at a phenomenal rate. To fix this situation, the GC class offers the following two static methods.

public static void AddMemoryPressure(Int64 bytesAllocated); 
public static void RemoveMemoryPressure(Int64 bytesAllocated);

A class that wraps a potentially large native resource should use these methods to give the garbage collector a hint as to how much memory is really being consumed. Internally, the garbage collector monitors this pressure, and when it gets high, a garbage collection is forced.

There are some native resources that are fixed in number. For example, Windows formerly had a restriction that it could create only five device contexts. There had also been a restriction on the number of files that an application could open. Again, from the CLR’s perspective, a process could allocate hundreds of objects (that use little memory) before performing a collection. But if the number of these native resources is limited, attempting to use more than are available will typically result in exceptions being thrown.

To fix this situation, the System.Runtime.InteropServices namespace offers the HandleCollector class.

public sealed class HandleCollector { 
 public HandleCollector(String name, Int32 initialThreshold); 
 public HandleCollector(String name, Int32 initialThreshold, Int32 maximumThreshold); 
 public void Add(); 
 public void Remove(); 
 public Int32 Count { get; } 
 public Int32 InitialThreshold { get; } 
 public Int32 MaximumThreshold { get; } 
 public String Name { get; } 
}

A class that wraps a native resource that has a limited quantity available should use an instance of this class to give the garbage collector a hint as to how many instances of the resource are really being consumed. Internally, this class object monitors the count, and when it gets high, a garbage collection is forced.

💡注意:在内部, GC.AddMemoryPressureHandleCollector.Add 方法会调用 GC.Collect ,在第 0 代超过预算前强制进行 GC。一般都强烈反对强制开始一次垃圾回收,因为它会对应用程序性能造成负面影响。但是,类之所以调用这些方法,是为了保证应用程序性能造成负面影响。但是,类之所以调用这些方法,是为了保证应用程序能用上有限的本机资源。本地资源用光了,应用程序就会失败。对于大多数应用程序,性能遭受一些损失总胜于完全无法运行。

Here is some code that demonstrates the use and effect of the memory pressure methods and the HandleCollector class.

using System; 
using System.Runtime.InteropServices; 
public static class Program { 
 public static void Main() { 
 MemoryPressureDemo(0); // 0 causes infrequent GCs
 MemoryPressureDemo(10 * 1024 * 1024); // 10MB causes frequent GCs
 HandleCollectorDemo(); 
}
 private static void MemoryPressureDemo(Int32 size) { 
 Console.WriteLine(); 
 Console.WriteLine("MemoryPressureDemo, size={0}", size); 
 // Create a bunch of objects specifying their logical size 
 for (Int32 count = 0; count < 15; count++) { 
 new BigNativeResource(size); 
 }
 // For demo purposes, force everything to be cleaned-up 
 GC.Collect(); 
 } 
 private sealed class BigNativeResource { 
 private readonly Int32 m_size; 
 public BigNativeResource(Int32 size) { 
 m_size = size; 
 // Make the GC think the object is physically bigger 
 if (m_size > 0) GC.AddMemoryPressure(m_size); 
 Console.WriteLine("BigNativeResource create."); 
 } 
 ~BigNativeResource() { 
 // Make the GC think the object released more memory 
 if (m_size > 0) GC.RemoveMemoryPressure(m_size); 
 Console.WriteLine("BigNativeResource destroy."); 
 } 
 } 
 private static void HandleCollectorDemo() { 
 Console.WriteLine(); 
 Console.WriteLine("HandleCollectorDemo"); 
 for (Int32 count = 0; count < 10; count++) new LimitedResource(); 
 // For demo purposes, force everything to be cleaned-up 
 GC.Collect(); 
 } 
 private sealed class LimitedResource { 
 // Create a HandleCollector telling it that collections should 
 // occur when two or more of these objects exist in the heap 
 private static readonly HandleCollector s_hc = new HandleCollector("LimitedResource", 2); 
 public LimitedResource() { 
 // Tell the HandleCollector a LimitedResource has been added to the heap 
 s_hc.Add(); 
 Console.WriteLine("LimitedResource create. Count={0}", s_hc.Count); 
 } 
 ~LimitedResource() { 
 // Tell the HandleCollector a LimitedResource has been removed from the heap 
 s_hc.Remove(); 
 Console.WriteLine("LimitedResource destroy. Count={0}", s_hc.Count); 
 } 
 } 
}

If you compile and run the preceding code, your output will be similar to the following output.

MemoryPressureDemo, size=0
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
MemoryPressureDemo, size=10485760
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource create.
BigNativeResource create.
BigNativeResource create.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
BigNativeResource destroy.
HandleCollectorDemo
LimitedResource create. Count=1
LimitedResource create. Count=2
LimitedResource create. Count=3
LimitedResource destroy. Count=3
LimitedResource destroy. Count=2
LimitedResource destroy. Count=1
LimitedResource create. Count=1
LimitedResource create. Count=2
LimitedResource create. Count=3
LimitedResource destroy. Count=2
LimitedResource create. Count=3
LimitedResource destroy. Count=3
LimitedResource destroy. Count=2
LimitedResource destroy. Count=1
LimitedResource create. Count=1
LimitedResource create. Count=2
LimitedResource create. Count=3
LimitedResource destroy. Count=2
LimitedResource destroy. Count=1
LimitedResource destroy. Count=0

# Finalization Internals

On the surface, finalization seems pretty straightforward: you create an object and its Finalize method is called when it is collected. But after you dig in, finalization is more complicated than this.

When an application creates a new object, the new operator allocates the memory from the heap. If the object’s type defines a Finalize method, a pointer to the object is placed on the finalization list just before the type’s instance constructor is called. The finalization list is an internal data structure controlled by the garbage collector. Each entry in the list points to an object that should have its Finalize method called before the object’s memory can be reclaimed.

Figure 21-13 shows a heap containing several objects. Some of these objects are reachable from application roots, and some are not. When objects C, E, F, I, and J were created, the system detected that these objects’ types defined a Finalize method and so added references to these objects to the finalization list.

image-20221127180339865

💡注意:虽然 System.Object 定义了 Finalize 方法,但 CLR 知道忽略它。也就是说,构造类型的实例时,如果该类型的 Finalize 方法是从 System.Object 继承的,就不认为这个对象是 ” 可终结 “ 的。类型必须重写 ObjectFinalize 方法,这个类型及其派生类型的对象才被认为是” 可终结 “的。

When a garbage collection occurs, objects B, E, G, H, I, and J are determined to be garbage. The garbage collector scans the finalization list looking for references to these objects. When a reference is found, the reference is removed from the finalization list and appended to the freachable queue. The freachable queue (pronounced “F-reachable”) is another of the garbage collector’s internal data structures. Each reference in the freachable queue identifies an object that is ready to have its Finalize method called. After the collection, the managed heap looks like Figure 21-14.

image-20221127180434281

In this figure, you see that the memory occupied by objects B, G, and H has been reclaimed because these objects didn’t have a Finalize method. However, the memory occupied by objects E, I, and J couldn’t be reclaimed because their Finalize methods haven’t been called yet.

A special high-priority CLR thread is dedicated to calling Finalize methods. A dedicated thread is used to avoid potential thread synchronization situations that could arise if one of the application’s normal-priority threads were used instead. When the freachable queue is empty (the usual case), this thread sleeps. But when entries appear, this thread wakes, removes each entry from the queue, and then calls each object’s Finalize method. Because of the way this thread works, you shouldn’t execute any code in a Finalize method that makes any assumptions about the thread that’s executing the code. For example, avoid accessing thread-local storage in the Finalize method.

In the future, the CLR may use multiple finalizer threads. So you should avoid writing any code that assumes that Finalize methods will be called serially. With just one finalizer thread, there could be performance and scalability issues in the scenario in which you have multiple CPUs allocating finalizable objects but only one thread executing Finalize methods—the one thread might not be able to keep up with the allocations.

The interaction between the finalization list and the freachable queue is fascinating. First, I’ll tell you how the freachable queue got its name. Well, the “f” is obvious and stands for finalization; every entry in the freachable queue is a reference to an object in the managed heap that should have its Finalize method called. But the reachable part of the name means that the objects are reachable. To put it another way, the freachable queue is considered a root, just as static fields are roots. So a reference in the freachable queue keeps the object it refers to reachable and is not garbage.

In short, when an object isn’t reachable, the garbage collector considers the object to be garbage. Then when the garbage collector moves an object’s reference from the finalization list to the freachable queue, the object is no longer considered garbage and its memory can’t be reclaimed. When an object is garbage and then not garbage, we say that the object has been resurrected.

As freachable objects are marked, objects referred to by their reference type fields are also marked recursively; all these objects must get resurrected in order to survive the collection. At this point, the garbage collector has finished identifying garbage. Some of the objects identified as garbage have been resurrected. The garbage collector compacts the reclaimable memory, which promotes the resurrected object to an older generation (not ideal). And now, the special finalization thread empties the freachable queue, executing each object’s Finalize method.

The next time the garbage collector is invoked on the older generation, it will see that the finalized objects are truly garbage because the application’s roots don’t point to it and the freachable queue no longer points to it either. The memory for the object is simply reclaimed. The important point to get from all of this is that two garbage collections are required to reclaim memory used by objects that require finalization. In reality, more than two collections will be necessary because the objects get promoted to another generation. Figure 21-15 shows what the managed heap looks like after the second garbage collection.

image-20221127180542963

💡小结:包含本机资源的类型被 GC 时, GC 会回收对象在托管堆中使用的内存。但这样会造成本机资源 (GC 对它一无所知) 的泄露,这当然是不允许的。所以,CLR 提供了称为终结 (finalization) 的机制,允许对象在被判定为垃圾之后,但在对象内存被回收之前执行一些代码。任何包装了本机资源 (文件、网络连接、套接字、互斥体) 的类型都支持终结。CLR 判定一个对象不可达时,对象将终结它自己,释放它包装的本机资源。之后,GC 会从托管堆回收对象。终极基类 System.Object 定义了受保护的虚方法 Finalize 。垃圾回收器判定对象是垃圾后,会调用对象的 Finalize 方法 (如果重写)。被视为垃圾的对象在垃圾回收完毕后才调用 Finalize 方法,所以这些对象的内存不是马上被回收的,因为 Finalize 方法可能要执行访问字段的代码。可终结对象在回收时必须存活,造成它被提升到另一代,使对象活的比正常时间长。这增大了内存耗用,所以应尽可能避免终结。更糟的是,可终结对象被提升时,其字段引用的所有对象也会被提升,因为它们也必须继续存活。所以,要尽量避免为引用类型的字段定义可终结对象。另外要注意, Finalize 方法的执行时间是控制不了的。应用程序请求更多内存时才可能发生 GC,而只有 GC 完成后才运行 Finalize 。另外,CLR 不保证多个 Finalize 方法的调用顺序。所以,在 Finalize 方法中不要访问定义了 Finalize 方法的其他类型的对象;那些对象可能已经终结了。但可以安全地访问值类型的实例,或者访问没有定义 Finalize 方法的引用类型的对象。调用静态方法也要当心,这些方法可能在内部访问已终结的对象,导致静态方法的行为变得无法预测。CLR 用一个特殊的、高优先级的专用线程调用 Finalize 方法来避免死锁。如果 Finalize 方法抛出未处理的异常,则进程终止,没办法捕捉该异常。综上所述, Finalize 方法问题较多,使用须谨慎。记住它们是为释放本机资源而设计的。强烈建议不要重写 ObjectFinalize 方法。相反,使用 Microsoft 在 FCL 中提供的辅助类。创建封装了本机资源的托管类型时,应该先从 System.Runtime.InteropServices.SafeHandle 这个特殊基类派生出一个类。 SafeHandle 类有两点需要注意。其一,它派生自 CriticalFinalizerObject ;后者在 System.Runtime.ConstrainedExecution 命名空间定义。CLR 以特殊方式对待这个类及其派生类。首次构造任何 CriticalFinalizerObject 派生类型的对象时,CLR 立即对继承层次结构中的多有 Finalize 方法进行 JIT 编译。构造对象时就编译这些方法,可确保当对象被确定为垃圾之后,本机资源肯定会得以释放。不对 Finalize 方法进行提前编译,那么也许能分配并使用本机资源,但无法保证释放。内存紧张时,CLR 可能找不到足够的内存来编译 Finalize 方法,这会阻止 Finalize 方法的执行,造成本机资源泄露。另外,如果 Finalize 方法中的代码引用了另一个程序集中的类型,但 CLR 定位该程序集失败,那么资源将得不到释放。CLR 是在调用了非 CriticalFinalizerObject 派生类型的 Finalize 方法之后,才调用 CriticalFinalizerObject 派生类型的 Finalize 方法。这样,托管资源类就可以在它们的 Finalize 方法中成功地访问 CriticalFinalizerObject 派生类型的对象。如果 AppDomain 被一个宿主应用程序 (例如 Microsoft SQL Server 或者 Microsoft ASP.NET) 强行中断,CLR 将调用 CriticalFinalizerObject 派生类型的 Finalize 方法。宿主应用程序不再信任它内部运行的托管代码时,也利用这个功能确保本机资源得以释放。其二, SafeHandle 是抽象类,必须有另一个类从该类派生并重写受保护的构造器、抽象方法 ReleaseHandle 以及抽象属性 IsInvalidget 访问器方法。大多数本机资源都用句柄 (32 位系统是 32 位值,64 位系统是 64 位值) 进行操作。所以 SafeHandle 类定义了受保护 IntPtr 字段 handle 。在 Windows 中,大多数值为 0 或 -1 的句柄都是无效的。 SafeHandle 派生类非常有用,因为它们保证本机资源在垃圾回收时得以释放。除了前面讨论过的功能, SafeHandle 类还有两个功能值得注意。首先,与本机代码互操作时, SafeHandle 派生类将获得 CLR 的特殊对待。 SafeHandle 派生类最后一个值得注意的功能是防止有人利用潜在的安全漏洞。 SafeHandle 类内部定义了私有字段来维护一个计数器。一旦某个 SafeHandle 派生对象被设为有效句柄,计数器就被设为 1。每次将 SafeHandle 派生对象作为实参传给一个本机方法 (非托管方法),CLR 就会自动递增计数器。类似地,当本机方法返回到托管代码时,CLR 就会自动递增计数器。类似地,当本机方法返回到托管代码时,CLR 自动递减计数器。当然,对计数器的操作是以线程安全的方式进行的。那么安全性如何得以保障?当另一个线程试图释放 SafeHandle 对象包装的本机资源时,CLR 知道它实际上不能释放资源,因为该资源正在由一个本机 (非托管) 函数使用。本机函数返回后,计数器递减为 0, 资源才会得以释放。 System.Runtime.InteropServices 命名空间还定义了一个 CriticalHandle 类。该类除了不提供引用计数器功能,其他方面与 SafeHandle 类相同。 CriticalHandle 类及其派生类通过牺牲安全性来换取更好的性能 (因为不用操作计数器)。和 SafeHandle 相似, CriticalHandle 类也有自己的几个派生类型,其中包括 CriticalHandleMinusOneIsInvalidCriricalHandleZeroOrMinusOneIsInvalid 。由于 Microsoft 倾向于建立更安全而不是更快的系统,所以类库中没有提供从这两个类派生的类型。自己写程序时,建议只有在必须追求性能的时候才使用派生自 CriticalHandle 的类型。如果降低安全性不会有什么严重的后果,就选择从 CriticalHandle 派生的一个类型。类如果想允许使用者控制类所包装的本机资源的生存期,就必须实现 IDisposable 接口。注意,并非一定要调用 Dispose 才能保证本机资源得以清理。本机资源的清理最终总会发生,调用 Dispose 只是控制这个清理动作的发生时间。另外,调用 Dispose 不会将托管对象从托管堆删除。只有在垃圾回收之后,托管堆中的内存才会得以回收。这意味着即使 dispose 了托管对象过去用过的任何本机资源,也能在托管对象上调用方法。 using 语句初始化一个对象,并将它的引用保存到一个变量中。然后再 using 语句的大括号内访问该变量。编译这段代码时,编译器自动生成 try 块和 finally 块。在 finally 块中,编译器生成代码将变量转型为一个 IDisposable 并调用 Dispose 方法。但是很显然, using 语句只能用于那些实现了 IDisposable 接口的类型。 StreamWriter 类型不支持终结,所以永远不会将它的缓冲区中的数据 flush 到底层 FileStream 对象。这意味着如果忘记在 StreamWriter 对象上显式调用 Dispose ,数据肯定会丢失。Microsoft 希望开发人员注意到这个数据一直丢失的问题,并插入对 Dispose 的调用来修正代码。应用程序创建新对象时, new 操作符会从堆中分配内存。如果对象的类型定义了 Finalize 方法,那么在该类型的实例构造器被调用之前,会将指向该对象的指针放到一个终结列表 (finalization list) 中。终结列表是由垃圾回收器控制的一个内部数据结构。列表中的每一项都指向一个对象 ———— 回收该对象的内存前应调用它的 Finalize 方法。freachable 队列 (发音是 “F-reachable”) 也是垃圾回收器的一种内部数据结构。一个特殊的高优先级 CLR 线程专门调用 Finalize 方法。专用线程可避免潜在的线程同步问题。(使用应用程序的普通优先级线程就可能发生这个问题。) freachable 队列为空时 (这很常见),该线程将睡眠。但一旦队列中有记录项出现,线程就会被唤醒,将每一项都从 freachable 队列中移除,同时调用每个对象的 Finalize 方法。由于该线程的特殊工作方式, Finalize 中的代码不应该对执行代码做出任何假设。例如,不要在 Finalize 方法中访问线程的本地存储。CLR 未来可能使用多个终结器线程。所以,代码不应假设 Finalize 方法会被连续调用。终结列表和 freachable 队列之间的交互很有意思。首先,让我告诉你 freachable 队列这个名称的由来。“f” 明显代表 “终结”(finalization);freachable 队列中的每个记录项都是对托管堆中应用调用其 Finalize 方法的一个对象的引用。“reachable” 意味着对象是可达的。换言之,可将 freachable 队列看成是像静态字段那样的一个根。所以,freachable 队列中的引用使它指向的对象保持可达,不是垃圾。简单地说,当一个对象不可达时,垃圾回收器就把它视为垃圾。但是,当垃圾回收器将对象的引用从终结列表移至 freachable 队列时,对象不再被认为是垃圾,不能回收它的内存。对象被视为垃圾又变得不是垃圾,我们说对象被复活了。标记 freachable 对象时,将递归标记对象中的引用类型的字段所引用的对象;所有这些对象也必须复活以便在回收过程中存活。下次对老一代进行垃圾回收时,会发现已终结的对象成为真正的垃圾,因为没有应用程序的根指向它们,freachable 队列也不再指向它们。所以,这些对象的内存会直接回收。整个过程中,注意,可终结对象需要执行两次垃圾回收才能释放它们占用的内存。在实际应用中,由于对象可能被提升至另一代,所以可能要求不止进行两次垃圾回收。

# Monitoring and Controlling the Lifetime of Objects Manually

The CLR provides each AppDomain with a GC handle table. This table allows an application to monitor the lifetime of an object or manually control the lifetime of an object. When an AppDomain is created, the table is empty. Each entry on the table consists of a reference to an object on the managed heap and a flag indicating how you want to monitor or control the object. An application adds and removes entries from the table via the System.Runtime.InteropServices.GCHandle type, as follows.

// This type is defined in the System.Runtime.InteropServices namespace 
public struct GCHandle { 
 // Static methods that create an entry in the table 
 public static GCHandle Alloc(object value); 
 public static GCHandle Alloc(object value, GCHandleType type); 
 // Static methods that convert a GCHandle to an IntPtr 
 public static explicit operator IntPtr(GCHandle value); 
 public static IntPtr ToIntPtr(GCHandle value); 
 // Static methods that convert an IntPtr to a GCHandle 
 public static explicit operator GCHandle(IntPtr value); 
 public static GCHandle FromIntPtr(IntPtr value); 
 // Static methods that compare two GCHandles 
 public static Boolean operator ==(GCHandle a, GCHandle b); 
 public static Boolean operator !=(GCHandle a, GCHandle b); 
 // Instance method to free the entry in the table (index is set to 0) 
 public void Free(); 
 // Instance property to get/set the entry's object reference 
 public object Target { get; set; } 
 // Instance property that returns true if index is not 0 
 public Boolean IsAllocated { get; } 
 // For a pinned entry, this returns the address of the object 
 public IntPtr AddrOfPinnedObject(); 
 public override Int32 GetHashCode(); 
 public override Boolean Equals(object o); 
}

Basically, to control or monitor an object’s lifetime, you call GCHandle’s static Alloc method, passing a reference to the object that you want to monitor/control, and a GCHandleType, which is a flag indicating how you want to monitor/control the object. The GCHandleType type is an enumerated type defined as follows.

public enum GCHandleType { 
 Weak = 0, // Used for monitoring an object’s existence 
 WeakTrackResurrection = 1, // Used for monitoring an object’s existence 
 Normal = 2, // Used for controlling an object’s lifetime 
 Pinned = 3 // Used for controlling an object’s lifetime 
}

Now, here’s what each flag means:

  • Weak This flag allows you to monitor the lifetime of an object. Specifically, you can detect when the garbage collector has determined this object to be unreachable from application code. Note that the object’s Finalize method may or may not have executed yet and therefore, the object may still be in memory.

  • WeakTrackResurrection This flag allows you to monitor the lifetime of an object. Specifically, you can detect when the garbage collector has determined that this object is unreachable from application code. Note that the object’s Finalize method (if it exists) has definitely executed, and the object’s memory has been reclaimed.

  • Normal This flag allows you to control the lifetime of an object. Specifically, you are telling the garbage collector that this object must remain in memory even though there may be no roots in the application that refer to this object. When a garbage collection runs, the memory for this object can be compacted (moved). The Alloc method that doesn’t take a GCHandleType flag assumes that GCHandleType.Normal is specified.

  • Pinned This flag allows you to control the lifetime of an object. Specifically, you are telling the garbage collector that this object must remain in memory even though there might be no roots in the application that refer to this object. When a garbage collection runs, the memory for this object cannot be compacted. This is typically useful when you want to hand the address of the memory out to native code. The native code can write to this memory in the managed heap knowing that a GC will not move the object.

When you call GCHandle’s static Alloc method, it scans the AppDomain’s GC handle table, looking for an available entry where the reference of the object you passed to Alloc is stored, and a flag is set to whatever you passed for the GCHandleType argument. Then, Alloc returns a GCHandle nstance back to you. A GCHandle is a lightweight value type that contains a single instance field, an IntPtr, which refers to the index of the entry in the table. When you want to free this entry in the GC handle table, you take the GCHandle instance and call the Free method (which also invalidates the GCHandle instance by setting its IntPtr field to zero).

Here’s how the garbage collector uses the GC handle table. When a garbage collection occurs:

  1. The garbage collector marks all of the reachable objects (as described at the beginning of this chapter). Then, the garbage collector scans the GC handle table; all Normal or Pinned objects are considered roots, and these objects are marked as well (including any objects that these objects refer to via their fields).
  2. The garbage collector scans the GC handle table looking for all of the Weak entries. If a Weak entry refers to an object that isn’t marked, the reference identifies an unreachable object (garbage), and the entry has its reference value changed to null.
  3. The garbage collector scans the finalization list. If a reference in the list refers to an unmarked object, the reference identifies an unreachable object, and the reference is moved from the finalization list to the freachable queue. At this point, the object is marked because the object is now considered reachable.
  4. The garbage collector scans the GC handle table looking for all of the WeakTrackResurrection entries. If a WeakTrackResurrection entry refers to an object that isn’t marked (which now is an object referenced by an entry in the freachable queue), the reference identifies an unreachable object (garbage), and the entry has its reference value changed to null.
  5. The garbage collector compacts the memory, squeezing out the holes left by the unreachable objects. Pinned objects are not compacted (moved); the garbage collector will move other objects around them.

Now that you have an understanding of the mechanism, let’s take a look at when you’d use them. The easiest flags to understand are the Normal and Pinned flags, so let’s start with these two. Both of these flags are typically used when interoperating with native code.

The Normal flag is used when you need to hand a pointer to a managed object to native code because, at some point in the future, the native code is going to call back into managed code, passing it the pointer. You can’t actually pass a pointer to a managed object out to native code, because if a garbage collection occurs, the object could move in memory, invalidating the pointer. So to work around this, you would call GCHandle’s Alloc method, passing in a reference to the object and the Normal flag. Then you’d cast the returned GCHandle instance to an IntPtr and pass the IntPtr into the native code. When the native code calls back into managed code, the managed code would cast the passed IntPtr back to a GCHandle and then query the Target property to get the reference (or current address) of the managed object. When the native code no longer needs the reference, you’d call GCHandle’s Free method, which allows a future garbage collection to free the object (assuming no other root exists to this object).

Notice that in this scenario, the native code is not actually using the managed object itself; the native code wants a way just to reference the object. In some scenarios, the native code needs to actually use the managed object. In these scenarios, the managed object must be pinned. Pinning prevents the garbage collector from moving/compacting the object. A common example is when you want to pass a managed String object to a Win32 function. In this case, the String object must be pinned because you can’t pass the reference of a managed object to native code and then have the garbage collector move the object in memory. If the String object were moved, the native code would either be reading or writing to memory that no longer contained the String object’s characters—this will surely cause the application to run unpredictably.

When you use the CLR’s P/Invoke mechanism to call a method, the CLR pins the arguments for you automatically and unpins them when the native method returns. So, in most cases, you never have to use the GCHandle type to explicitly pin any managed objects yourself. You do have to use the GCHandle type explicitly when you need to pass the pointer to a managed object to native code; then the native function returns, but native code might still need to use the object later. The most common example of this is when performing asynchronous I/O operations.

Let’s say that you allocate a byte array that should be filled as data comes in from a socket. Then, you would call GCHandle’s Alloc method, passing in a reference to the array object and the Pinned flag. Then, using the returned GCHandle instance, you call the AddrOfPinnedObject method. This returns an IntPtr that is the actual address of the pinned object in the managed heap; you’d then pass this address into the native function, which will return back to managed code immediately. While the data is coming from the socket, this byte array buffer should not move in memory; preventing this buffer from moving is accomplished by using the Pinned flag. When the asynchronous I/O operation has completed, you’d call GCHandle’s Free method, which will allow a future garbage collection to move the buffer. Your managed code should still have a reference to the buffer so that you can access the data, and this reference will prevent a garbage collection from freeing the buffer from memory completely.

It is also worth mentioning that C# offers a fixed statement that effectively pins an object over a block of code. Here is some code that demonstrates its use.

unsafe public static void Go() {
 // Allocate a bunch of objects that immediately become garbage
 for (Int32 x = 0; x < 10000; x++) new Object();
 IntPtr originalMemoryAddress;
 Byte[] bytes = new Byte[1000]; // Allocate this array after the garbage objects
 // Get the address in memory of the Byte[]
 fixed (Byte* pbytes = bytes) { originalMemoryAddress = (IntPtr) pbytes; }
 // Force a collection; the garbage objects will go away & the Byte[] might be compacted
 GC.Collect(); 
 // Get the address in memory of the Byte[] now & compare it to the first address
 fixed (Byte* pbytes = bytes) {
 Console.WriteLine("The Byte[] did{0} move during the GC", 
 (originalMemoryAddress == (IntPtr) pbytes) ? " not" : null);
 }
}

Using C#’s fixed statement is more efficient that allocating a pinned GC handle. What happens is that the C# compiler emits a special “pinned” flag on the pbytes local variable. During a garbage collection, the GC examines the contents of this root, and if the root is not null, it knows not to move the object referred to by the variable during the compaction phase. The C# compiler emits IL to initialize the pbytes local variable to the address of the object at the start of a fixed block, and the compiler emits an IL instruction to set the pbytes local variable back to null at the end of the fixed block so that the variable doesn’t refer to any object, allowing the object to move when the next garbage collection occurs.

Now, let’s talk about the next two flags, Weak and WeakTrackResurrection. These two flags can be used in scenarios when interoperating with native code, but they can also be used in scenarios that use only managed code. The Weak flag lets you know when an object has been determined to be garbage but the object’s memory is not guaranteed to be reclaimed yet. The WeakTrackResurrection flag lets you know when an object’s memory has been reclaimed. Of the two flags, the Weak flag is much more commonly used than the WeakTrackResurrection flag. In fact, I’ve never seen anyone use the WeakTrackResurrection flag in a real application.

Let’s say that Object-A periodically calls a method on Object-B. However, the fact that Object-A has a reference to Object-B forbids Object-B from being garbage collected, and in some rare scenarios, this may not be desired; instead, we might want Object-A to call Object-B’s method if Object-B is still alive in the managed heap. To accomplish this scenario, Object-A would call GCHandle’s Alloc method, passing in the reference to Object-B and the Weak flag. Object-A would now just save the returned GCHandle instance instead of the reference to Object-B.

At this point, Object-B can be garbage collected if no other roots are keeping it alive. When Object-A wants to call Object-B’s method, it would query GCHandle’s read-only Target property. If this property returns a non-null value, then Object-B is still alive. Object-A’s code would then cast the returned reference to Object-B’s type and call the method. If the Target property returns null, then Object-B has been collected (but not necessarily finalized) and Object-A would not attempt to call the method. At this point, Object-A’s code would probably also call GCHandle’s Free method to relinquish the GCHandle instance.

Because working with the GCHandle type can be a bit cumbersome and because it requires elevated security to keep or pin an object in memory, the System namespace includes a WeakReference class to help you.

public sealed class WeakReference<T> : ISerializable where T : class {
 public WeakReference(T target);
 public WeakReference(T target, Boolean trackResurrection);
 public void SetTarget(T target);
 public Boolean TryGetTarget(out T target);
}

This class is really just an object-oriented wrapper around a GCHandle instance: logically, its constructor calls GCHandle’s Alloc, its TryGetTarget method queries GCHandle’s Target property, its SetTarget method sets GCHandle’s Target property, and its Finalize method (not shown in the preceding code, because it’s protected) calls GCHandle’s Free method. In addition, no special permissions are required for code to use the WeakReference class because the class supports only weak references; it doesn’t support the behavior provided by GCHandle instances allocated with a GCHandleType of Normal or Pinned. The downside of the WeakReference class is that an instance of it must be allocated on the heap. So the WeakReference class is a heavier-weight object than a GCHandle instance.

💡重要提示:开发人员刚开始学习弱引用时,会马上想到它们在缓存情形中的用处。例如,他们会想到构造包含大量数据的一组对象,并创建对这些对象的弱引用。程序需要数据时检查这些弱引用。程序需要数据时就检查这些弱引用,看看包含这些数据的对象是否依然 “健在”。对象还在,程序就直接使用对象;这样程序就会有较好的性能。但如果发生垃圾回收,包含数据的对象就会被销毁。而一旦需要重新创建数据,程序的性能就会受到影响。

这个技术的问题在于:垃圾回收不是在内存满或接近满时才发生的。相反,只要第 0 代满了,垃圾回收就会发生。多以,对象在内存中被抛弃的频率比预想的高得多,应用程序的性能将大打折扣。

弱引用在缓存情形中确实能得到高效应用。但构建良好的缓存算法来找到内存消耗与速度之间的平衡点十分复杂。简单地说,你希望缓存保持对自己的所有对象的强引用,内存吃紧就开始将强引用转换为弱引用。目前,CLR 没有提供一个能告诉应用程序内存吃紧的机制。但已经有人通过定时调用 Win32 GlobalMemoryStatusEx 函数并检查返回的 MEMORYSTATUSEX 结构的 dwMemoryLoad 成员成功做到了这一点。如果该成员报告大于 80 的值,内存空间就处于吃紧状态。然后可以开始将强引用转换为弱引用 ———— 可依据的算法包括:最近最少使用算法 (Least-Recently Used algorithm, LRU)、最频繁使用算法 (Most-Frequently Used algorithm, MFU) 以及某个时基算法 (time-base algorithm) 等。

Developers frequently want to associate a piece of data with another entity. For example, you can associate data with a thread or with an AppDomain. It is also possible to associate data with an individual object by using the System.Runtime.CompilerServices.ConditionalWeakTable class, which looks like this.

public sealed class ConditionalWeakTable<TKey, TValue> 
 where TKey : class where TValue : class {
 public ConditionalWeakTable();
 public void Add(TKey key, TValue value);
 public TValue GetValue(TKey key, CreateValueCallback<TKey, TValue> createValueCallback);
 public Boolean TryGetValue(TKey key, out TValue value);
 public TValue GetOrCreateValue(TKey key);
 public Boolean Remove(TKey key);
 public delegate TValue CreateValueCallback(TKey key); // Nested delegate definition
}

If you want to associate some arbitrary data with one or more objects, you would first create an instance of this class. Then, call the Add method, passing in a reference to some object for the key parameter and the data you want to associate with the object in the value parameter. If you attempt to add a reference to the same object more than once, the Add method throws an ArgumentException; to change the value associated with an object, you must remove the key and then add it back in with the new value. Note that this class is thread-safe so multiple threads can use it concurrently, although this means that the performance of the class is not stellar; you should test the performance of this class to see how well it works for your scenario.

Of course, a table object internally stores a WeakReference to the object passed in as the key; this ensures that the table doesn’t forcibly keep the object alive. But what makes the ConditionalWeakTable class so special is that it guarantees that the value remains in memory as long as the object identified by the key is in memory. So this is more than a normal WeakReference because if it were, the value could be garbage collected even though the key object continued to live. The ConditionalWeakTable class could be used to implement the dependency property mechanism used by XAML. It can also be used internally by dynamic languages to dynamically associate data with objects.

Here is some code that demonstrates the use of the ConditionalWeakTable class. It allows you to call the GCWatch extension method on any object passing in some String tag. Then it notifies you via the console window whenever that particular object gets garbage collected.

internal static class ConditionalWeakTableDemo {
 public static void Main() {
 Object o = new Object().GCWatch("My Object created at " + DateTime.Now);
 GC.Collect(); // We will not see the GC notification here
 GC.KeepAlive(o); // Make sure the object o refers to lives up to here
 o = null; // The object that o refers to can die now
 GC.Collect(); // We'll see the GC notification sometime after this line
 Console.ReadLine();
 }
}
internal static class GCWatcher {
 // NOTE: Be careful with Strings due to interning and MarshalByRefObject proxy objects
 private readonly static ConditionalWeakTable<Object, NotifyWhenGCd<String>> s_cwt =
 new ConditionalWeakTable<Object, NotifyWhenGCd<String>>();
 private sealed class NotifyWhenGCd<T> {
 private readonly T m_value;
 internal NotifyWhenGCd(T value) { m_value = value; }
 public override string ToString() { return m_value.ToString(); }
 ~NotifyWhenGCd() { Console.WriteLine("GC'd: " + m_value); }
 }
 public static T GCWatch<T>(this T @object, String tag) where T : class {
 s_cwt.Add(@object, new NotifyWhenGCd<String>(tag));
 return @object;
 }
}

💡小结:CLR 为每个 AppDomain 都提供了一个 GC 句柄表 (GC Handle table),允许应用程序监视或手动控制对象的生存期。这个表在 AppDomain 创建之初是空白的。表中每个记录项都包含以下两种信息:对托管堆中的一个对象的引用,以及指出如何监视或控制对象的标志 (flag)。简单地说,为了控制或监视对象的生存期,可调用 GCHandle 的静态 Alloc 方法并传递想控制 / 监视的对象的引用。还可传递一个 GCHandleType ,这可传递一个 GCHandleType ,这是一个标志,指定了你想如何控制 / 监视对象。 GCHandle 的静态 Alloc 方法在调用时会扫描 AppDomain 的 GC 句柄表,查找一个可用的记录项来存储传给 Alloc 的对象引用,并将标志设为你为 GCHandleType 实参传递的值。然后, Alloc 方法返回一个 GCHandle 实例。 GCHandle 是轻量级的值类型,其中包含一个实例字段 (一个 IntPtr 字段),它引用了句柄表中的记录项索引。要释放 GC 句柄表中的这个记录项时,可以获取 GCHandle 实例,并在这个实例上调用 Free 方法。 Free 方法将 IntPtr 字段设为 0,使实例变得无效。垃圾回收器如何使用 GC 句柄表。当垃圾回收发生时,垃圾回收器的行为如下。垃圾回收器标记所有可达的对象 (本章开始的时候已进行了描述)。然后,垃圾回收器扫描 GC 句柄表;所有 NormalPinned 对象都被看成是根,同时标记这些对象 (包括这些对象通过它们的字段引用的对象)。垃圾回收器扫描 GC 句柄表,查找所有 Weak 记录项。如果一个 Weak 记录项引用了未标记的对象,该引用标识的就是不可达对象 (垃圾),该记录项的引用值更改为 null 。垃圾回收器扫描终结列表。在列表中,对未标记对象的引用标识的是不可达对象,这些引用从终结列表移至 freachable 队列。这时对象会被标记,因为对象又变成可达了。垃圾回收器扫描 GC 句柄表,查找所有 WeakTrackResurrection 记录项。如果一个 WeakTrackResurrection 记录项引用了未标记的对象 (它现在是由 freachable 队列中的记录项引用的),该引用标识的就是不可达对象 (垃圾),该记录项的引用值更改为 null 。垃圾回收器堆内存进行压缩,填补不可达对象留下的内存 “空调”,这其实就是一个内存碎片整理的过程。 Pinned 对象不会压缩 (移动),垃圾回收器会移动它周围的其他对象。使用 CLR 的 P/Invoke 机制调用方法时,CLR 会自动帮你固定实参,并在本机方法返回时自动解除固定。所以,大多数时候都不必使用 GCHandle 类型来显式固定任何托管对象。只有在将托管对象的指针传给本机代码,然后本机函数返回,但本机代码将来仍需使用该对象时,才需要显式使用 GCHandle 类型。最常见的例子就是执行异步 I/O 操作。假定分配了一个字节数组,并准备在其中填充来自一个套接字的数据。这时应该调用 GCHandleAlloc 方法,传递对数组对象的引用以及 Pinned 标志。然后,在返回的 GCHandle 实例上调用 AddrOfPinnedObject 方法。这会返回一个 IntPtr ,它是已固定对象在托管堆中的实际地址。然后,就该地址传给本机函数,该函数立即返回至托管代码。数据从套接字传来时,该字节数组缓冲区在内存中不会移动;阻止移动是 Pinned 标志的功劳。异步 I/O 操作完毕后调用 GCHandleFree 方法,以后垃圾回收时就可以移动缓冲区了。托管代码应包含一个缓冲区引用,这使你能访问数据。正是由于这个引用的存在,所以才会阻止垃圾回收从内存中彻底释放该缓冲区。使用 C# 的 fixed 语句比分配一个固定 GC 句柄高效得多。这里发生的事情是,C# 编译器在局部变量上生成一个特殊的” 已固定 “标志。垃圾回收期间,GC 检查这个根的内容,如果根不为 null ,就知道在压缩阶段不要移动变量引用的对象。C# 编译器生成 IL 将局部变量初始化为 fixed 块起始处的对象的地址。在 fixed 块的尾部,编译器还会生成 IL 指令将局部变量设回 null ,使变量不引用任何对象。这样一来,下一次垃圾回收发生时,对象就可以移动了。 WeakWeakTrackResurrection 它们既可用于和本机代码的互操作,也可在只有托管代码的时候使用。 Weak 标志使你知道在什么时候一个对象被判定为垃圾,但这时对象的内存不一定被回收。 WeakTrackResurrection 标志使你知道在什么时候对象的内存已被回收。两个标志中 Weak 更常用。由于使用 GCHandle 类型有些繁琐,而且要求提升的安全性才能在内存中保持或固定对象,所以 System 命名空间提供了一个 WeakReference<T> 类来帮助你。这个类其实是包装了一个 GCHandle 实例的面向对象包装器 (wrapper):逻辑上说,它的构造器调用 GCHandleAlloc 方法, TryGetTarget 方法查询 GCHandleTarget 属性, SetTarget 方法设置 GCHandleTarget 属性,而 Finalize 方法 (这里未显示,因为是受保护方法) 则调用 GCHandleFree 方法。此外,代码无需特殊权限即可使用 WeakReference<T> 类,因为该类只支持弱引用;不支持 GCHandleType 设为 NormalPinnedGCHandle 实例的行为。 WeakReference<T> 类的缺点在于它的实例必须在堆上分配。所以, WeakReference 类比 GCHandle 实例更 “重”。开发人员经常需要将一些数据和另一个实体关联。例如,数据可以和一个线程或 AppDomain 关联。另外,可用 System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue> 类将数据和单独的对象关联。注意这个类是线程安全的,多个线程能同时使用它 (虽然这也意味着类的性能并不出众);应测试好这个类的性能,验证它在是否适合你的实际环境。当然,表对象在内部存储了对作为 key 传递的对象的弱引用 (一个 WeakReference 对象);这样可保证不会因为表的存在而造成对象 “被迫” 存活。但是, ConditionalWeakTable 类最特别的地方在于,它保证了只要 key 所标识的对象在内存中,值就肯定在内存中。这使其超越了一个普通的 WeakReference ,因为如果是普通的 WeakReference ,那么即使 key 对象保持存活,值就肯定在内存中。这使其超越了一个普通的 WeakReference ,因为如果是普通的 WeakReference , 那么即使 key 对象保持存活,值也可能被垃圾回收。 ConditionalWeakTable 类可用于实现 XAML 的依赖属性 (dependency property) 机制。动态语言也可在内部利用它将数据和对象动态关联。