--- C # stack VS stack (Part Four ),
Preface
In Part Three, the first article in this series, this section describes the differences between the value type and the reference type in Copy, how to clone the reference type, and how to use the ICloneable interface.
This article is the fourth part of the article. It mainly describes the principle and precautions of memory recovery and how to improve GC efficiency.
Note: I have limited my understanding in English and technical experience. If you have any mistakes in this article, please kindly advise.
Directory
C # stack comparison (Part One)
C # stack comparison (Part Two)
C # stack comparison (Part Three)
C #Stack comparison (Part Four)
Graphical Representation
Let's take a look at it from the GC perspective. If we are responsible for taking out the trash, we need to do it efficiently. Obviously, we need to determine what is spam and what is not (this is troublesome for those who are unwilling to continue ).
In order to decide what to leave, we first assume that all useless things are in the garbage bin (such as newspapers in the corner, garbage bins in the attic, and everything in the toilet ). Imagine that we are living with two "friends": Joseph Ivan-Thomas (JIT) and Cindy lulin-Richmond (Cindy Lorraine Richmond, CLR ). Joseph and Cindy record the memory usage and provide us with feedback records. The initial feedback list is called the "root" list, because we will start with it. We will keep a master list to depict a chart that shows the position of everything in the room. Anything we need to make things work will be added to this list (just as we won't put the remote control far away when watching TV, when we play the computer, we will put the keyboard and display in the "List ).
Note: In the author's article, JIT and CLR are short for concept names in the form of first letter names. This article introduces the following concepts:
This is also how GC determines whether to recycle or not. GC receives an object reference from the JIT compiler and CLR root list, and recursively searches for the object reference, so that we can create a graph to describe the objects we should save.
Composition of the root list:
● Global/static pointer. In static variables, this ensures that our objects are not recycled by keeping references.
● The pointer is on the stack (thread stack. We don't want to throw anything that the thread needs to continue executing.
● CPU registration pointer. Any pointer from a CPU to a memory address on the hosting stack will be protected (do not discard these pointers ).
In, Object1, Objetc3, and Object5 are referenced by the root list in the hosting heap, and Object1 and Object5 are directly referenced (pointer pointing, object3 is found in recursive search. If we compare this example with the TV remote control example, we will find that Object1 is a TV and Object3 is a remote control. When these are all graphical (graphical display of the reference relationship), we will proceed to the next step to compacting ).
Suppression
Now we have drawn a graph of the objects we want to retain, and we can put the "Reserved objects" together.
Note: The gray Box is a non-referenced object. we can remove the object and reorganize the managed heap so that the referenced object can be "nearly ", in order to keep the managed heap space neat.
Fortunately, in our lives, we may not need to tidy up the room when we place other things. Since Object2 is not referenced, GC moves down Object3 and fixes the Object1 pointer.
Note: The "fixed Object3" pointer here is because the memory address of Object3 changes after moving, so we also need to update the pointer address pointing to Object3, in principle, the pointer only knows an address value and does not know which one is Object3.
What I did not mention in the original article is that the GRAPH pointer to the Object also needs to update the address value. Of course, this is not the main concern. The above is my opinion.
Next, GC moves Object5 down, for example:
Now we have sorted out the hosting heap. We only need a note and place it at the top of the managed heap we just pressed to make Claire (in fact, Cindy seems that the author remembers his girlfriend's name J, CLR) knows where to place new objects, as shown in:
Understanding the nature of GC can help us better understand the situation of moving memory objects that may be very inefficient. As you have seen, it makes sense to reduce the size of the objects we need to move, because we have produced smaller object copies, this will improve GC efficiency as a whole.
Note: what may be involved here is the management of LOH large object heap in the memory. Generally, we will "design" the memory data distribution based on our business scenarios, in this way, you can better manage large objects and some memory fragment objects that are frequently created and deleted. The second advantage is to help us understand how GC recycles junk data, what operations are there after collection, and what impact these operations have, and how GC manages garbage according to "Generation.
What will happen outside the managed heap?
As a garbage collector, one problem is how to deal with the items in the car when we clean the house. The premise is that we need to clean everything when we clean it up. That is to say, what should I do if the laptop is in the room and the battery is in the car?
Note: according to the context, the author wants to express that garbage in the house will be discarded sooner or later (managed resources ), in most cases, garbage in cars may not be thrown away due to drive-by negligence (similar to unmanaged resources), and we are a perfect person to catch up, it must be clear about all the garbage (including in the CAR). What should we do?
In reality, GC needs to execute code to clean up unmanaged resources, such as file handles, database connections, and network connections. The most likely way to handle these problems is to use the final function (finalizer is called the destructor. Here we use the C ++ expression, which is essentially the same ).
Note: destructor are not only available in C ++, but still available in C # code, in more cases, we will inherit from the code and implement the IDisposeable interface to allow the GC to call the Dispose () method to reclaim resources (for more information, see the standard Dispose mode ), the Terminator is executed after Dispose and ensures that Class garbage collection is also executed when the caller does not call Dispose. In many cases, Using (var a = new Class () is used ()) when the syntax sugar occurs, the program will automatically execute the Class Dispose method. If the Dispose method is not called and there are still unmanaged resources, this will cause Memory leakage (Memory Leak ).
class Sample{ ~Sample() { // FINALIZER: CLEAN UP HERE }}
During object creation, all objects with Terminator are added to the End queue. Let's assume that Object1, Object4, and Object5 have termination functions and are in the termination queue. Let's see what happened. When the objects Object2 and Object4 are no longer referenced by the program, they are ready for garbage collection, such:
The object Object2 is recycled normally. However, when we recycle the object Object4, GC knows that it is in the final queue and instead of directly recycling resources, it moves Object4 (pointer) to a new queue named Freachable.
A special thread will manage the Freachable queue. When the Object4 Terminator is executed, it will be removed from the Freachable queue so that Object4 is ready to be recycled, such:
Therefore, Object4 will be recycled at the next GC.
Adding a terminator to a class will add additional work to GC, so this will be a very expensive operation and will increase the negative performance impact on garbage collection. You can use the terminator only when you are sure to do so. Otherwise, you must be very cautious.
A positive approach is to reclaim unmanaged resources. As you may think, it is best to explicitly close the connection and use the IDisposeable interface instead of writing the terminator manually.
IDisposeable Interface
The class implementing the IDisposeable interface has a cleanup method Dispose () (this method is the only thing the IDisposeable interface does ). So we use this interface to replace the Terminator:
public class ResourceUser{ ~ResourceUser() // THIS IS A FINALIZER { // DO CLEANUP HERE }}
The code after reconstruction using the IDisposable interface is as follows:
public class ResourceUser : IDisposable{ #region IDisposable Members public void Dispose() { // CLEAN UP HERE!!! } #endregion}
The IDisposeable interface is integrated into the Using Keyword, And the Dispose method is called at the end of Using. Objects in Using will be out of scope because they are essentially considered to have been removed (recycled) and are waiting for GC to be recycled.
public static void DoSomething(){ ResourceUser rec = new ResourceUser(); using (rec) { // DO SOMETHING } // DISPOSE CALLED HERE // DON'T ACCESS rec HERE}
I like to use the Using syntax sugar, because it intuitively makes more sense and rec temporary variables do not make sense outside the using block. Therefore, the using (ResourceUser rec = new ResourceUser () mode is more suitable for actual needs and values.
Note: The author emphasizes the scope of the rec variable. If it is only inside the Using block, it must be in parentheses after the Using.
Using is used to implement the IDisposeable interface class, so that we can replace the methods that need to write the terminator to generate GC energy consumption.
Static variables: Be careful!
class Counter{ private static int s_Number = 0; public static int GetNextNumber() { int newNumber = s_Number; // DO SOME STUFF s_Number = newNumber + 1; return newNumber; }}
If both threads call the GetNextNumber method at the same time and before S_Number is increased, they will return the same result! There is only one way to ensure that the results meet expectations, that is, only one thread can enter the code. As a best practice, you will try to Lock a small program, because the thread cannot wait in the queue to wait for the Lock method to be executed, even if it may be inefficient.
class Counter{ private static int s_Number = 0; public static int GetNextNumber() { lock (typeof(Counter)) { int newNumber = s_Number; // DO SOME STUFF newNumber += 1; s_Number = newNumber; return newNumber; } }}
Note: 1. in essence, Lock is the locking method of thread semaphores. In the original article, some people pointed out that lock (typeof (Counter) was a question. Although the author did not reply, the author did make this mistake, "We will never lock type Typeof (Anything) or lock (this)", use private readonly static object syncLock = new Object (); lock (syncLock ){...} In this way, only conclusions are not demonstrated in code. If you want to know more, you can search for them online.
2.The code lock before C #4 may be compiled:
Object tmp = listLock; System. threading. monitor. enter (tmp); try {// TODO: Do something stuff. system. threading. thread. sleep (1000);} finally {System. threading. monitor. exit (tmp );}Before C #4
Imagine this situation: if the first thread unexpectedly exits after the execution of Enter (tmp), that is, it does not execute Exit (tmp ), the second thread will always be blocked in the Enter field and wait for others to release resources. This is a typical deadlock case.
The overload of Monitor. Enter is added to the C #4 and later frameworks, which will solve possible deadlock problems for us to a certain extent:
Bool acquired = false; object tmp = listLock; try {# region Description // Summary: // Attempts to acquire an exclusive lock on the specified object, and atomically // sets a value that indicates whether the lock was taken. ////// Parameters: /// obj: // The object on which to acquire the lock. ///// lockTaken: // The result of the attempt to acquire the lock, passed by reference. the input // must be false. the output is true if the lock is acquired; otherwise, the // output is false. the output is set even if an exception occurs during the // attempt to acquire the lock. //// // Exceptions: /// System. argumentException: // The input to lockTaken is true. /// // System. argumentNullException: // The obj parameter is null. // [TargetedPatchingOptOut ("Performance critical to inline upload SS NGen image boundaries")] // public static void TryEnter (object obj, ref bool lockTaken); # endregion System. threading. monitor. enter (tmp, ref acquired); // TODO: Do something stuff. system. threading. thread. sleep (1000);} finally {if (acquired) {System. threading. monitor. exit (tmp );}}C #4 and later
--The above content is from the "deep understanding of C # Edition2"
Static variables: Caution 2
The second thing we should pay attention to is the reference of static variables. Remember, the objects indexed by the "root" list are not recycled. Here is the ugliest example:
class Olympics{ public static Collection<Runner> TryoutRunners;} class Runner{ private string _fileName; private FileStream _fStream; public void GetStats() { FileInfo fInfo = new FileInfo(_fileName); _fStream = _fileName.OpenRead(); }}
Since the Runner set in the Olympus class is static, It is not released by GC (also referenced by the "root" list), but you may also notice that, every time we call the GetStats method, it opens a file. And because it is neither closed nor recycled, we will face a catastrophic disaster. Imagine we have 0.1 million Runner registered for the Olympics. We will end with many unrecoverable objects. Ouch! We are talking about low performance issues!
Singleton Mode
One way to save resources for an object is to keep only one object globally in the application. We will use the GoF Singleton mode.
Using the tool class (static Utility Class or XXXHelper class) to keep a singleton in memory is a resource-saving trick. The best practice is the singleton mode. We should be careful to use static variables because they are really "global variables" and cause us headaches and encounter many strange behaviors in multi-threaded programs that change the thread state. If we use the singleton mode, we should think clearly.
public class SingltonPattern{ private static Earth _instance = new Earth(); private SingltonPattern() { } public static Earth GetInstance() { return _instance; }}
We have defined private constructors, so the SingltonPattern class cannot be instantiated externally. We can only get the instance through the static method GetInstance. This will be thread-safe because CLR protects static variables. This example is the most elegant way I have seen in the singleton mode.
Note: The original readers have questioned this Singleton mode. The Singleton mode of the author here is more in line with the singleton principle.
Summary
Let's summarize the methods that can improve GC efficiency:
1.Clean up. Do not keep the resource on! Close all opened connections and clear all unhosted objects as much as possible. When using unmanaged resources, one principle is to initialize objects as late as possible and release resources as soon as possible.
2.Do not over-use references. Make rational use of the referenced object. Remember, if our object is still alive, we should set the object to null. One of my tips when setting null values is to use the NullObject mode to avoid exceptions caused by null references. When GC starts to recycle, the less referenced objects exist, the more favorable the performance.
3.Simple Terminator. For GC, The Terminator consumes a lot of resources. We only need to use the terminator in a very definite way. If we can replace the Terminator with IDisposeable, it will be very efficient, because we can recycle resources at one time rather than twice.
4.Put the object and its sub-objects together. It is easy for GC to copy big data instead of data fragments. When we declare an object, try to make it as close as possible to all internal object declarations.
Friends who read Part 4 willThe last "egg"