An Overview of the .Net Compact Framework Garbage Collector
Developers frequently ask for more information about how the garbage collector works in the Compact Framework. In this blog entry, I’ll address the questions we are most often asked about the collector. I’ll start by describing the overall model for collections in the Compact Framework. I’ll then point out a few of the key differences between how collections are done in the Compact Framework and the full .Net Framework. If you have questions I don’t answer, feel free to send me mail or comment on this entry and I’ll work to get them addressed in the future.
Let’s start by looking at the collection model used by the Compact Framework. In this section, I’ll describe the conditions that cause a collection to occur and the specific steps taken each time the collector runs.
A collection is initiated when either:
· 1MB of objects have been allocated,
· An application is moved to the background,
· A failure to allocate memory occurs
· An application calls GC.Collect (more on this later).
When doing a collection, the Compact CLR performs the following steps:
1. Brings all threads to a “safe point”. Before a collection can occur, the CLR must ensure that all threads are in a “known” state with respect to the GC heap. That is, no threads are in a state in which they can alter the GC heap in any way once a collection begins. To do this, the CLR brings all threads to a state where they are not executing any managed code, nor are they running in the unmanaged portion of the CLR. Once they reach a safe point, threads are not allowed to proceed until the collection has finished (except for the thread that actually does the collection, that is).
2. Marks all objects that descend from live roots. Once all threads have reached a safe point, the objects in the GC heap are examined. Those that descend from live roots, such as local variables in the current scope, statics and so on are marked. Note that there is not a special thread that exists solely to do garbage collections. The thread that was running when the GC was triggered is the thread that does the collection.
3. Frees all unmarked objects and populates the finalization queue. The memory for all unmarked objects that don’t have finalizers is freed. Those unmarked objects that do have finalizers are placed on a list known as the finalization queue.
4. Occasionally, compacts the GC heap. If the CLR determines that the GC heap is sufficiently fragmented, the heap is compacted. The heuristic used to determine when a compaction should occur is currently based on the percentage of fragmentation in the heap, but different heuristics may be used in future releases.
5. “Pitches” Jitted code in some scenarios. The Compact Framework CLR maintains jitted code in an in-memory cache. Instead of recompiling a method each time it is called, the native code for that method is simply retrieved from the cache. When a failure to allocate memory occurs, or when an application is moved to the background, the contents of the code cache are freed, or “pitched”, to make more memory available to the system.
6. Runs the finalizers for all objects on the finalization queue. After the unused objects are freed and the optional compacting and code pitching occurs, the collection is considered done and all threads are allowed to resume. In the background, the CLR uses a dedicated thread to run the finalizers for all objects on the finalization queue. Those objects are then freed the next time a collection occurs.
How is the NetCF collector different from the collector in the Full .Net Framework?
The Compact Framework targets significantly different scenarios than the full .Net Framework does. While the Compact Framework is tuned specifically for use on devices with limited resources, the full .Net Framework supports scenarios ranging from client side UI applications to server side applications such as SQL Server and ASP.Net that may run on machines with multiple processors. As such, it’s natural to find significant differences between the Compact Framework’s garbage collector and the garbage collector provided by the full .Net Framework. The primary differences between the two collectors are:
Different basic collection model. As we’ve seen, the .Net CF has a standard mark and sweep collection model augmented with occasional compaction and code pitching. While the high level goal (to reclaim memory allocated to objects that are no longer used) is the same, the basic collection model on the full .Net Framework is substantially different. In particular, the full .Net Framework uses a “generational” model whereby objects that survive collections are promoted to higher generations that are collected less frequently. In this way, the collector on the full .Net Framework tunes itself such that less time is spent analyzing objects that aren’t likely to be collected anyway. There are other differences as well. For example, the full .Net Framework handles unusually large objects differently than normal objects and the events that cause a GC to occur are different. Chapter 19 in Jeff Richter’s “Applied .Net Framework Programming” book provides a thorough, yet easily readable description of how garbage collection works on the full .Net Framework.
Fewer GC “configurations”. Because the full .Net Framework supports such a wide variety of application scenarios, its garbage collector is able to run in different “modes” specifically tuned by scenario. In particular, the collection strategy used by the full .Net Framework is different depending on whether you are running an interactive application on a work station vs. a server application with high throughput requirements on a multi processor machine. You’ve probably heard these referred to as “workstation” vs. “server” garbage collection. Also, the workstation flavor of garbage collection has a concurrent mode in which the collector uses threads differently in an attempt to keep the application’s user interface as responsive as possible. The Compact Framework supports none of these different GC modes. Because the range of application scenarios is smaller, and the collector itself must be smaller, a single collection model is used regardless of the type of device your application is running on.
Code pitching. As described, the Compact Framework’s collector frees code it has previously jitted in order to free memory in certain scenarios. The collector on the full .Net Framework has no such “code pitching” capability – the amount of jitted code kept in memory is free to grow without bounds.
Other Frequently Asked Questions
I’ve tried to answer many of the commonly asked GC questions in this blog. We’ve covered questions like “what causes a GC?”, “what happens during a GC?”, and “How is the .Net CF collector different from the one in the full .Net Framework”? In this section, I’ve added answers to a few more common questions. If you still have questions unanswered, feel free to send them my way and I’ll research them and post the results.
What are the ramifications of calling GC.Collect()?
You’ve probably heard the old wisdom “Yes, there is an API you can use to force a garbage collection, but don’t ever call it – the garbage collector can do a better job on its own”. Here’s the thinking behind that statement:
Garbage collection is a relatively expensive operation. As we’ve seen, a collection involves “suspending” threads, traversing object graphs, moving blocks of memory around and so on. Each time you force a collection, you’re forcing the Compact CLR to, at a minimum, get all threads to a state in which a collection can start, and scan the object graph looking for unused objects. Clearly, calling GC.Collect() repeatedly will have a seriously negative performance impact. The Compact Framework’s collector will kick in automatically after 1MB of objects have been created. As such, collections do happen occasionally - they don’t only happen when memory is exhausted. Furthermore, when a call to GC.Collect() returns, you can’t be guaranteed that all object finalizers have finished running. As a result, you cannot force a collection in hopes of deterministically finalizing a particular object (use the Dispose pattern to deterministically free resources associated with an object). That said, it is possible that you may have a scenario in which you’ll benefit by initiating collections yourself. If you believe you have such a scenario, feel free to try it, but be aware of the implications and measure performance carefully to make sure you’re not doing more harm than good.
Can I prevent a GC from occurring?
There are no APIs you can use to prevent a GC from occurring. As we’ve seen, the time spent in garbage collection is a function of the number of objects that have been allocated. As such, really the only way to “prevent” a collection is to keep the number of objects you allocate to a minimum. When you consider this, however, be sure to remember that there are subtle scenarios in which allocations may be happening on your behalf that you may not be aware of. The best example of this is boxing. Boxing a value type necessarily creates a reference type in the GC heap, so even though you aren’t explicitly calling new(), you’re still allocating objects. It might be tempting to think that a call to GC.Collect() can be used to “time” collections, but as described above, such calls are likely to negatively impact performance instead of helping consistently.
This posting is provided "AS IS" with no warranties, and confers no rights.
Some of the information contained within this post may be in relation to beta software. Any and all details are subject to change.