In the Release compilation mode, whether the event will cause memory leakage.
Question: Memory leakage in uncommon events
Some friends may also use events frequently, but they seldom release event hooks, and the program has never heard of problems such as memory leakage. Fortunately, in some cases, there will be no problems, and projects created many years ago have been running well, including me. Even so, I still cannot be lucky, we have to figure out how this kind of mysterious memory leak happened. Today we can do an experiment to verify it again.
Yes. To verify this problem, I once suspected that my code was wrong. I even wrote the code in the book (online) as an example, and I could not reproduce the issue that caused Memory leakage. Is the textbook wrong?
First, let's look at my code and prepare two classes, one for initiating an event and one for processing an event:
class A { public event EventHandler ToDoSomething ; public A() { } public void RaiseEvent() { ToDoSomething(this, new EventArgs()); } public void DelEvent() { ToDoSomething = null; } public void Print(string msg) { Console.WriteLine("A:{0}", msg); } } class B { byte[] data = null; public B(int size) { data = new byte[size]; for (int i = 0; i < size ; i++) data[i] = 0; } public void PrintA(object sender, EventArgs e) { ((A)sender).Print("sender:"+ sender.GetType ()); } }
Then, write the following method in the main program:
static void TestInitEvent(A a) { var b = new B(100 * 1024 * 1024); a.ToDoSomething += b.PrintA; }
Here, we will Initialize an instance object B of M B and hook the event ToDoSomething of object a to the PrintA method of object B. Normally, B is a local variable inside the method and is inaccessible outside the method. However, because the method of object B is linked to the event of object a of the method parameter, therefore, the lifecycle of object B is not completed, which can be initiated by object a later. The PrintA method of object B is called and confirmed.
PS: A friend asked me why I didn't write the code to cancel the hook here. I used it here, and the actual project code is generally not written like this.
To monitor how much memory is consumed in the current test, prepare a getWorkingSet method. The Code is as follows:
Static void getWorkingSet () {using (var process = Process. getCurrentProcess () {Console. writeLine ("--------- current process name: {0} -----------", process. processName); using (var p1 = new PerformanceCounter ("Process", "Working Set-Private", process. processName) using (var p2 = new PerformanceCounter ("Process", "Working Set", process. processName) {Console. writeLine (process. id); // note divided by the number of CPUs in the Console. writeLine ("{0} {1: N} KB", "Working Set (process class)", process. workingSet64/1024); Console. writeLine ("{0} {1: N} KB", "Working Set", process. workingSet64/1024); // process. privateMemorySize64 private working set is not very accurate, about 9 M more Console. writeLine ("{0} {1: N} KB", "private working set", p1.NextValue ()/1024); // p1.NextValue () // Logger ("{0 }; memory (dedicated working set) {1: N}; PID: {2}; program name: {3} ", // DateTime. now, p1.NextValue ()/1024, process. id. toString (), process. processName) ;}} Console. writeLine ("--------------------------------------------------------"); Console. writeLine ();}
Next, start to write the following test code in the main program:
GetWorkingSet (); A a = new A (); TestInitEvent (a); Console. writeLine ("1, press any key to start garbage collection"); Console. readKey (); GC. collect (); getWorkingSet ();
View the screen output:
--------- Current process name: leleapplication1.vshost ----------- 4848 Working Set (process class) 25,260.00 KB Working Set 25,260.00 KB private working set 8,612.00 KB--------------------------------------------------------1, press any key to start garbage collection --------- current process name: consoleApplication1.vshost ----------- 4848 Working Set (process class) 135,236.00 KB Working Set 135,236.00 KB private working set 111,256.00 KB
After the program starts running, the memory usage is much better. The current program is in the IDE debugging state, and then we run the test program directly without debugging (Release). Then we can check the result again:
--------- Current process name: leleapplication1 ----------- 7056 Working Set (process class) 10,344.00 KB Working Set 10,344.00 KB private working set 7,036.00 KB--------------------------------------------------------1, press any key to start garbage collection --------- current process name: consoleApplication1 ----------- 7056 Working Set (process class) 121,460.00 KB Working Set 121,460.00 KB private working set 109,668.00 KB --------------------------------------------------------
We can see that the memory still cannot be recycled in the Release compilation mode.
Analysis of the above test program, we only hook an event in a separate method, and the event has not been executed, and then start garbage collection, but the results show that the collection was not successful. This is in line with the situation described in our textbooks: After the object event hook, if the hook is not removed, it may cause memory leakage.
At the same time, the above results also show that the linked object B is not recycled, which can initiate an event to test whether object B can continue to process the event initiated by object, continue with the above main program code:
Console. WriteLine ("2, press any key to initiate an event on the primary object"); Console. ReadKey (); a. RaiseEvent (); // getWorkingSet () cannot be recycled normally here ();
Result:
2. Press any key to initiate the event A: sender: ConsoleApplication1.A --------- current process name: leleapplication1 ----------- 7056 Working Set (process class) 121,576.00 KB Working Set 121,576.00 KB private working set 109,672.00 KB Working Set --------------------------------------------------------
This indicates that although object B is out of the range of the TestInitEvent method,But it remains alive, Printed A sentence: A: sender: ConsoleApplication1.A
Is it possible that GC can be successfully recycled several times?
Let's continue to call GC in the main program. Try again:
Console. writeLine ("3, press any key to start garbage collection, and then initiate an event again"); Console. readKey (); GC. collect ();. raiseEvent (); // getWorkingSet () cannot be recycled normally in the memory ();
Result:
3. Press any key to start garbage collection, and then initiate event A: sender: leleapplication1.a --------- current process name: leleapplication1 ----------- 7056 Working Set (process class) 14,424.00 KB Working Set 14,424.00 KB private working set 2,972.00 KB worker --------------------------------------------------------
Sure enough, the memory is recycled!
However, note that after the GC execution is successful, the method a. RaiseEvent () for initiating the event is still called and the execution is successful,Object B is still aliveEvent hooks are still valid, but a large amount of useless memory is recycled.
Note: The result of the above Code is that when I write a blog, I write it again and test it. If it is executed continuously, this is not the case, the above Code cannot recycle the memory successfully.
This shows that the GC memory recovery time is indeed uncertain.
Proceed. We will deregister the event and release the event hook. Then, let's look at the result:
Console. writeLine ("4, press any key to start logging out, and then recycle again"); Console. readKey ();. delEvent (); GC. collect (); Console. writeLine ("5, garbage collection completed"); getWorkingSet ();
Result:
4. Press any key to log out of the event, and then recycle the event again. 5. The garbage collection is completed. --------- current process name: leleapplication1 ----------- 7056 Working Set (process class) 15,252.00 KB Working Set 15,252.00 KB private working set 3,196.00 KB running --------------------------------------------------------
The memory has not changed significantly, indicating that the memory has been recycled successfully.
To verify the previous guesses, let's re-run the program and execute it continuously (in the Release mode) to see the execution results:
--------- Current process name: leleapplication1 ----------- 4280 Working Set (process class) 10,364.00 KB Working Set 10,364.00 KB private working set 7,040.00 KB--------------------------------------------------------1, press any key to start garbage collection --------- current process name: consoleApplication1 ----------- 4280 Working Set (process class) 121,456.00 KB Working Set 121,456.00 KB private working set 109,668.00 KB--------------------------------------------------------2, press any key, the main object initiates event A: sender: leleapplication1.a --------- current process name: consoleApplication1 ----------- 4280 Working Set (process class) 121,572.00 KB Working Set 121,572.00 KB private working set 109,672.00 KB--------------------------------------------------------3, press any key to start garbage collection, then initiate event A: sender: ConsoleApplication1.A --------- current process name: consoleApplication1 ----------- 4280 Working Set (process class) 121,628.00 KB Working Set 121,628.00 KB private working set 109,672.00 KB--------------------------------------------------------4, press any key to start logging out of the event, then garbage collection 5 again, garbage collection completed --------- current process name: consoleApplication1 ----------- 4280 Working Set (process class) 19,228.00 KB Working Set 19,228.00 KB private working set 7,272.00 KB --------------------------------------------------------View Code
This confirms the previous instructions,The timing for GC to actually recycle memory is uncertain.
Compiler Optimization
Streamline the previous test code, initialize only the event object, and then perform GC collection. Let's take a look at the result:
GetWorkingSet (); A a = new A (); TestInitEvent (a); getWorkingSet (); Console. writeLine ("4, press any key to start logging out, and then recycle again"); Console. readKey ();. delEvent (); GC. collect (); Console. writeLine ("5, garbage collection completed"); getWorkingSet (); Console. readKey ();
Result:
--------- Current process name: leleapplication1 ----------- 6576 Working Set (process class) 10,344.00 KB Working Set 10,344.00 KB private working set 7,240.00 KB worker current process name: leleapplication1 ----------- 6576 Working Set (process class) 121,500.00 KB Working Set 121,500.00 KB private working set 110,292.00 KB--------------------------------------------------------4, press any key to start logging off the event, and then recycle again 5, garbage collection completed --------- current process name: leleapplication1 --------- 6576 Working Set (process class) 19,788.00 KB Working Set 19,788.00 KB private working set 7,900.00 KB --------------------------------------------------------
As expected, the memory will return to normal after GC.
Slightly modify the above Code and just comment out the code before GC: a. DelEvent ();
GetWorkingSet (); A a = new A (); TestInitEvent (a); getWorkingSet (); Console. writeLine ("4, press any key to start logging out, and then recycle again"); Console. readKey (); //. delEvent (); GC. collect (); Console. writeLine ("5, garbage collection completed"); getWorkingSet (); Console. readKey ();
Check the result again:
--------- Current process name: leleapplication1 ----------- 4424 Working Set (process class) 10,308.00 KB Working Set 10,308.00 KB private working set 7,040.00 KB worker current process name: leleapplication1 ----------- 4424 Working Set (process class) 121,256.00 KB Working Set 121,256.00 KB private working set 7,592.00 KB--------------------------------------------------------4, press any key to start logging off the event, and then recycle again 5, garbage collection completed --------- current process name: leleapplication1 --------- 4424 Working Set (process class) 19,436.00 KB Working Set 19,436.00 KB private working set 7,600.00 KB --------------------------------------------------------
Stunned: no memory usage occurred!
It seems that there is only one possibility:
Object a has no Code such as Operation events before GC recycles the memory. Therefore, it is clear that the event code before object a is no longer valid. The related object B can be found in TestInitEvent (); immediately after the method is called, the current test result is displayed.
If the Release compilation mode is not optimized, let's take a look at the results of IDE debugging or Debug compilation mode running (the previous code is not modified ):
--------- Current process name: leleapplication1.vshost ----------- 8260 Working Set (process class) 25,148.00 KB Working Set 25,148.00 KB private working set 9,816.00 KB worker current process name: leleapplication1.vshost ----------- 8260 Working Set (process class) 136,048.00 KB Working Set 136,048.00 KB private working set 112,888.00 KB--------------------------------------------------------4, press any key to start logging off the event, and then recycle again 5, garbage collection completed --------- current process name: leleapplication1.vshost --------- 8260 Working Set (process class) 136,692.00 KB Working Set 136,692.00 KB private working set 112,892.00 KB --------------------------------------------------------
This time, although GC garbage collection is still called, the effect is not immediately achieved, and the memory is still more than 100 MB.
Finally, we immediately release the event hook after initiating the event hook, and then look at the results in Debug mode. Therefore, we only need to modify the following code:
static void TestInitEvent(A a) { var b = new B(100 * 1024 * 1024); a.ToDoSomething += b.PrintA; // a.ToDoSomething -= b.PrintA; }
View the execution result in Debug mode:
--------- Current process name: leleapplication1.vshost ----------- 8652 Working Set (process class) 26,344.00 KB Working Set 26,344.00 KB private working set 9,452.00 KB worker current process name: leleapplication1.vshost ----------- 8652 Working Set (process class) 135,628.00 KB Working Set 135,628.00 KB private working set 10,008.00 KB--------------------------------------------------------4, press any key to start logging off the event, and then recycle again 5, garbage collection completed --------- current process name: leleapplication1.vshost --------- 8652 Working Set (process class) 33,768.00 KB Working Set 33,768.00 KB private working set 10,008.00 KB --------------------------------------------------------
As expected, the memory usage has not increased, so calling GC to recycle memory makes no sense at this time.
Q: Do I have to cancel the event hook?
Not necessarily. If the lifecycle of the object initiating the event is short, it is not a static object, not a singleton object. When the lifecycle of the object ends, GC can recycle the object. However, this object can be recycled successfully only after multiple generations, and it is uncertain when it will be executed each time. The longer the recycled algebra is, the longer the time it will be recycled.
Therefore, if the object that initiates the event is not the root object, but is attached to another object with a long lifecycle, and the event hook is not released, the objects that process the event cannot be released, therefore, memory leakage occurs.
To avoid potential memory leakage, we should develop a good habit of immediately removing event hooks without using events!
Do I need to write GC in the program code to reclaim the memory?
Not necessarily, unless you are very clear about when to recycle the memory and are sure that GC can work effectively at this time, such as in the example tested in this article, it will not be effective to call GC, it may also cause side effects, such as suspending the Service Processing of the entire application.
Summary
When using events, if you do not remove the event hook after use, memory leakage may occur,
The timing of GC memory recovery is indeed uncertain. Therefore, GC is not a life-saving tool. The best practice is to immediately remove the event hook when an event is used up.
If you forget this, do not forget to use the Release compilation mode when releasing the program!