Laden...

MemoryLeaks durch nicht abgehängte Events

Erstellt von Programmierhans vor 18 Jahren Letzter Beitrag vor 18 Jahren 8.744 Views
Programmierhans Themenstarter:in
4.221 Beiträge seit 2005
vor 18 Jahren
MemoryLeaks durch nicht abgehängte Events

Hallo Leute

In letzter Zeit sind diverse Threads zum Thema GC / Memory / Events / MemoryLeaks erstellt worden.

Dabei habe ich immer behauptet, dass ein nicht abgehängter Event ein MemoryLeak provoziert.... Diese Behauptung ist ...Edit: nur teilweise richtig.... EndEdit:

TestKlasse welche nur viel Memory braucht und einen Event anbietet.




	/// <summary>
	/// Zusammenfassung für MemoryFresser.
	/// </summary>
	public class MemoryFresser
	{
		private int _Id;
		private string[] _Strings=new string[1000];
		public MemoryFresser(int pID)
		{
			this._Id=pID;
			for (int i=0;i<1000;i++)
			{
				//fülle mal ein wenig Memory
				this._Strings[i]=new string('X',1024);
			}
		}

		public event EventHandler Test;
		private void OnTest()
		{
			if (this.Test!=null)
			{
				this.Test(this,EventArgs.Empty);
			}
		}
		
		~MemoryFresser()
		{
			System.Diagnostics.Debug.WriteLine(string.Format("Memory der ID {0} wird freigegeben",this._Id.ToString()));
		}
	}



Aufrufender Code:




		MemoryFresser _MemoryFresser;
		private void button1_Click(object sender, System.EventArgs e)
		{
			for (int i=0;i<100;i++)
			{
				this._MemoryFresser=new MemoryFresser(i);
				//obwohl dieser EventHandler nicht abgehängt wird werden die alten MemoryFresser vom GC entsorgt
				this._MemoryFresser.Test+=new EventHandler(mf_Test);
			}
		}

		private void mf_Test(object sender, EventArgs e)
		{
			System.Diagnostics.Debug.WriteLine("Test");
		}


Ich hab absichtlich kein IDisposable implementiert sondern nur einen Destructor (~) damit man sieht wann die Instanzen vom GC collected werden.

Dies will aber nicht heissen, dass man keine MemoryLeaks haben kann (es gibt genügend Beispiele wie man Leaks erzeugen kann)...

Aber somit ist bewiesen dass ein nicht abgehängter Event KEIN Leak provoziert...

Sorry für die bisher falsche Behauptung meinerseits (man hat halt nie ausgelernt)

Gruss
Programmierhans

Kommentare sind ausdrücklich erwünscht !!

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

4.506 Beiträge seit 2004
vor 18 Jahren

Hallo Programmierhans!

Was soll man dem noch hinzufügen? Ich kann hier nur sagen sauber getestet, übersichtlich dargestellt, ich hätte es auf jeden Fall schlechter hingekriegt 😉

Vielleicht so etwas wie:

Du musst ja Zeit haben... tststs (aber das kann ICH wohl nicht sagen 😉)

Ciao
Norman-Timo

A: “Wie ist denn das Wetter bei euch?”
B: “Caps Lock.”
A: “Hä?”
B: “Na ja, Shift ohne Ende!”

_
416 Beiträge seit 2005
vor 18 Jahren

Hallo,

du gehst allerdings mit einer falschen Annahme rein.

Dein MemoryFresser verweist auf deine aufrufende Klasse und nicht umgekehrt. D.h. es existieren keine Referenzen AUF deinen MemoryFresser.

Andersrum, d.h. wenn der MemoryFresser ein Event abboniert, sollte es anders aussehen.

cu, tb

4.506 Beiträge seit 2004
vor 18 Jahren

Hallo tb!

Du hast recht mit der Annahme, dass alle Referenzen VOM Fresser aus gehen, aber macht das wirklich einen Unterschied, denn das sollte normalerweise auch den Speicher unnötig fressen lassen.

Oder ich bin verkehrt, dann habe ich das Prinzip mit der Speicherbelegung noch nicht so geschnackelt...

Ciao
Norman-Timo

A: “Wie ist denn das Wetter bei euch?”
B: “Caps Lock.”
A: “Hä?”
B: “Na ja, Shift ohne Ende!”

Programmierhans Themenstarter:in
4.221 Beiträge seit 2005
vor 18 Jahren

Original von tb

Andersrum, d.h. wenn der MemoryFresser ein Event abboniert, sollte es anders aussehen.

Müsste man auch noch testen 🙂



	/// <summary>
	/// Zusammenfassung für MemoryFresser.
	/// </summary>
	public class MemoryFresser
	{
		private int _Id;
		private Form _Caller;
		private string[] _Strings=new string[1000];
		public MemoryFresser(int pID, Form pCaller)
		{
			this._Caller=pCaller;
			this._Caller.Closed+=new EventHandler(_Caller_Closed);
			this._Id=pID;
			for (int i=0;i<1000;i++)
			{
				//fülle mal ein wenig Memory
				this._Strings[i]=new string('X',1024);
			}
		}

		public event EventHandler Test;
		private void OnTest()
		{
			if (this.Test!=null)
			{
				this.Test(this,EventArgs.Empty);
			}
		}
		
		~MemoryFresser()
		{
			System.Diagnostics.Debug.WriteLine(string.Format("Memory der ID {0} wird freigegeben",this._Id.ToString()));
		}

		private void _Caller_Closed(object sender, EventArgs e)
		{
			System.Diagnostics.Debug.WriteLine("Closed");
		}
	}





		MemoryFresser _MemoryFresser;
		private void button1_Click(object sender, System.EventArgs e)
		{
			for (int i=0;i<100;i++)
			{
				this._MemoryFresser=new MemoryFresser(i,this);
				this._MemoryFresser.Test+=new EventHandler(mf_Test);
			}
		}

		private void mf_Test(object sender, EventArgs e)
		{
			System.Diagnostics.Debug.WriteLine("Test");
		}


@ tb

Und Du hast natürlich Recht .... jetzt Leakt das Teil 👍

@ norman_timo

So gut war die Testumgebung wohl doch nicht 🤔

Da ich eh meist Bibliotheken entwickle habe ich mir extrem angewöhnt eh immer alles was angehängt wurde auch wieder abzuhängen....

Gruss
Programmierhans

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

4.506 Beiträge seit 2004
vor 18 Jahren

Das versteh ich irgendwie jetzt doch nicht...

Kann das jemand näher erläutern was da passiert?

Ciao
Norman-Timo

A: “Wie ist denn das Wetter bei euch?”
B: “Caps Lock.”
A: “Hä?”
B: “Na ja, Shift ohne Ende!”

Programmierhans Themenstarter:in
4.221 Beiträge seit 2005
vor 18 Jahren

So und nun noch der Beweis, dass wirklich der Event dran schuld ist:




	/// <summary>
	/// Zusammenfassung für MemoryFresser.
	/// </summary>
	public class MemoryFresser
	{
		private int _Id;
		private Form _Caller;
		private string[] _Strings=new string[1000];
		public MemoryFresser(int pID, Form pCaller)
		{
			this._Caller=pCaller;
			this._Caller.Closed+=new EventHandler(_Caller_Closed);
			this._Id=pID;
			for (int i=0;i<1000;i++)
			{
				//fülle mal ein wenig Memory
				this._Strings[i]=new string('X',1024);
			}
		}

		public event EventHandler Test;
		private void OnTest()
		{
			if (this.Test!=null)
			{
				this.Test(this,EventArgs.Empty);
			}
		}
		
		public void DetachClosed()
		{
			this._Caller.Closed-=new EventHandler(_Caller_Closed);
		}

		~MemoryFresser()
		{
			System.Diagnostics.Debug.WriteLine(string.Format("Memory der ID {0} wird freigegeben",this._Id.ToString()));
		}

		private void _Caller_Closed(object sender, EventArgs e)
		{
			System.Diagnostics.Debug.WriteLine("Closed");
		}
	}





MemoryFresser _MemoryFresser;
		private void button1_Click(object sender, System.EventArgs e)
		{
			for (int i=0;i<100;i++)
			{
				if (this._MemoryFresser!=null)
				{
					this._MemoryFresser.DetachClosed();
				}
				this._MemoryFresser=new MemoryFresser(i,this);
				this._MemoryFresser.Test+=new EventHandler(mf_Test);
			}
		}

		private void mf_Test(object sender, EventArgs e)
		{
			System.Diagnostics.Debug.WriteLine("Test");
		}


Wenn vor dem überschreiben von this._MemoryFresser mit DetachClosed das abhängen des Closed des Callers gemacht wird, dann kann der GC schön brav collecten....

Gruss
Programmierhans

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

Programmierhans Themenstarter:in
4.221 Beiträge seit 2005
vor 18 Jahren

Original von norman_timo

Du musst ja Zeit haben... tststs (aber das kann ICH wohl nicht sagen 😉)

Zurzeit warte ich im einen Projekt darauf dass ich weitermachen kann (Telefon ausstehend) .... aber wenn der Anruf nicht bald kommt, dann wechsle ich auf ein anderes Projekt..... aber dann kommt der Anruf sicher sofort und ich hab zwei offene Baustellen (wobei das eine eine Bibliothek ist, welche ich nur sehr ungern in nicht betriebsfähigem Zustand habe)

Und Weiterbildung gehört nun mal auch dazu finde ich... und ich nutze jede freie Minute aus um mein Coding noch mehr zu verbessern... Und wie man sieht habe ich zwar "eigentlich" gewusst, dass nicht abgehängte Events MemoryLeaks provozieren können, habe aber nie ein Testprojekt angelegt um genau dies zu testen (vorallem in welche Richtung usw...)

Daher nochmal dank an tb für den Tritt gegen das Schienbein 😉

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

_
416 Beiträge seit 2005
vor 18 Jahren

Original von norman_timo
Das versteh ich irgendwie jetzt doch nicht...

Kann das jemand näher erläutern was da passiert?

Ciao
Norman-Timo

Also, erster Fall: der der Caller tragt sich beim Fresser ein. Dann gibt es eine Referenz vom Fresser auf den Caller, da wenn der Fresser das Event auslöst ja irgendwie die Methode des Callers aufgerufen werden muss (siehe Observer-Pattern) Nun sucht der der Garbage Collector aber nur nach Objekten AUF die keine Referenz existiert. Das ist ja auch logisch. Da auf sie keine Referenz existiert, kann sie niemand mehr benutzen. Dass von ihnen eine Referenz auf andre existiert ist ja egal, da dies niemand mehr sehen kann. Stell dir mal eine Stadt vor von welcher aus nur Einbahnstraßen nach draußen führen, aber keine rein. Die Stadt wär ja so ziemlich nutzlos, kann also weg.

Zweiter Fall: der Fresser abonniert ein Event des Callers. Jetzt brauch man nur eine ganz einfache Überlegung. Was wäre wenn der GC den Fresser löscht und plötzlich das Event des Callers ausgelöst wurde. Der Caller würde versuchen die Methode des Fressers aufzurufen, aber an dessen Stelle ist nur noch ein Speicherloch oder schlimmer schon ganz andere Daten. Das wäre eine Zugriffverletzung welche früher gern mit einen blauen Bildschirm belohnt wurde. Fazit: in Fall zwei wird der Fresser nicht gelöscht.

cu, tb

4.506 Beiträge seit 2004
vor 18 Jahren

Vielen Dank!

Ich hab mir eingebildet, dass in der Stadt mit lauter Einbahnstraßen auch Autos vom Himmel fallen können, aber das war wohl ein Denkfehler.

Ciao
Norman-Timo

A: “Wie ist denn das Wetter bei euch?”
B: “Caps Lock.”
A: “Hä?”
B: “Na ja, Shift ohne Ende!”

1.373 Beiträge seit 2004
vor 18 Jahren

Interessante Untersuchung, nur zumindest von der Terminologie her nicht ganz richtig. Bei dem von dir gezeigten Effekt handelt es sich nicht um ein Leak. Ich zitiere wikipedia:

Ein Speicherleck (englisch: memory leak) ist ein Fehler in einer Software. Ein Teil des von einem Programm benutzten Arbeitsspeichers wird nach Benutzung nicht wieder freigegeben und kann nicht wieder verwendet werden, solange das Programm nicht beendet wird.

Das bedeutet, dass ein Leak nur dann da ist, wenn der reservierte Speicher nicht wieder zurückgeholt werden kann, bzw. auf das "verlorene" Objekt nicht mehr zugegriffen werden kann. Das trifft in diesem Fall nicht zu. Durch Auslösen des Events wird das "verlorene" Objekt ja implizit verwendet. Und wer möchte, kann es sich auch explizit holen:


// angenommen, dies hier stünde in Form
...
Delegate[] delegates = Closed.GetInvocationList();
foreach(Delegate del in delegates){
  object target = del.Target; 
  //        ^-- da isser, unser MemoryFresser :)
}

Es geht also kein Speicher verloren, ergo: kein (Memory-)Leak.

Aber die wichtige Aussage deiner Untersuchung, dass man ein Objekt schnellstmöglich wieder bei abonnierten Events austragen sollte, kann ich vollkommen unterschreiben.

Das Verhalten des Garbagecollectors ist natürlich vollkommen korrekt. Es könnte durchaus sein, dass ein Objekt nur besteht, um durch ein event verwendet zu werden:


form.Closed = new MyObjekt().MyHandler;

... ist vollkommen gültiger (und ggf. sinnvoller) Code. In dieser Hinsicht ist es sinnvoll, Delegaten/Events nicht zu sehr als Blackbox zu sehen, sondern auch als ein Objekt, dass eine Referenz auf den Callee enthält, oder vielleicht wie eine Collection mit Callee-Referenzen. Wie auch immer - selbst aufräumen ist angesagt!

MfG VizOne

Programmierhans Themenstarter:in
4.221 Beiträge seit 2005
vor 18 Jahren

Absolut korrekt VizOne

MemoryLeak hin oder her .... einigen wir uns darauf dass unbeabsichtigt Speicher nicht freigegeben wird... das triffts dann wieder.

Früher war ich unentschlossen, heute bin ich mir da nicht mehr so sicher...

_
416 Beiträge seit 2005
vor 18 Jahren

Original von VizOne
Das bedeutet, dass ein Leak nur dann da ist, wenn der reservierte Speicher nicht wieder zurückgeholt werden kann, bzw. auf das "verlorene" Objekt nicht mehr zugegriffen werden kann. Das trifft in diesem Fall nicht zu. Durch Auslösen des Events wird das "verlorene" Objekt ja implizit verwendet. Und wer möchte, kann es sich auch explizit holen

Das is ja korrekt. Richtige MemoryLeaks gibts in .Net dank Garbage Collector ja nicht mehr (solange man keine Unsafeblöcke verwendet oder fehlerhafte unmanaged dlls einbindet)

Und gerade dass Eventbeispiel ist vielleicht von daher unpassend als dass das Event ja immer noch mal ausgelöst werden könnte. Aber manchmal passiert es halt auch mal schnell dass man irgendwo noch eine Referenz hat, die man wirklich nie mehr benutzt. Bestes Beispiel: lokale Variable in der Main-methode. Dass ist dann doch schon wieder ein klassisches Memoryleak. Denn sobald Application.Run aufgerufen wird kann niemand mehr auf diese Variable zugreifen. Und wenn sie auch nach zurückkehren von Application.Run nicht mehr benutzt wird, war dies eine reine Speicherverschwendung welche erst bei Programmende aufgelöst wird.
D.h. auch immer Referenzen auf null setzen wenn nicht mehr benötigt!

cu, tb

1.373 Beiträge seit 2004
vor 18 Jahren

Original von tb
Bestes Beispiel: lokale Variable in der Main-methode. Dass ist dann doch schon wieder ein klassisches Memoryleak. Denn sobald Application.Run aufgerufen wird kann niemand mehr auf diese Variable zugreifen. Und wenn sie auch nach zurückkehren von Application.Run nicht mehr benutzt wird, war dies eine reine Speicherverschwendung welche erst bei Programmende aufgelöst wird.
D.h. auch immer Referenzen auf null setzen wenn nicht mehr benötigt!

Das stimmt nicht. Der GC ist ja nicht doof. Er kann die lokale Variable löschen, sobald sie das letzte mal verwendet wurde. Das kann bereits lange for Application.Run() passieren, wenn sie danach nicht mehr verwendet wird.
Eine lokale Variable auf null zu setzen ist vollkommen sinnlos. Der GC braucht diesen Hinweis nicht, da er sie bereits vorher als "non-reachable" einstufen kann. Nicht umsonst gibt es GC.KeepAlive, um das Leben solcher Objekte zu verlängern.
Bei Membervariablen macht es hingegen durchaus Sinn, wenn die Member eher freigegeben werden können als das Elternobjekt. Sie sind Teil des Objektgraphen, und wenn dessen Wurzel erreichbar ist, sind auch die Member erreichbar.

MfG VizOne

_
416 Beiträge seit 2005
vor 18 Jahren

Original von VizOne
Der GC ist ja nicht doof.

Tja meiner anscheinend schon. Ich arbeite noch mit .Net 1.1.

Das ist mein Testcode

byte[] mem = new byte[100*1024*1024]; // belege 100MB speicher
for(int i=0; i<mem.Length; i++) mem[i]=(byte)(i%256); // nutze ihn auch
//mem=null;

Form frm = new Form(); 
Application.Run(frm);
 

So belegt das Programm 117MB Arbeitsspeicher. Und es rückt nix mehr davon raus (nicht mal auf den Kopf stellen nützt was)

Kommentiert man die mem=null; zeile wieder ein werden nach ca 30 sec, manchmal auch früher die 100MB freigegeben.

Ich habs ca. 3 mal mit beiden Versionen getestet, immer das gleiche ergebnis.

cu, tb

P
939 Beiträge seit 2003
vor 18 Jahren

Das kann ich mir vorstellen.

Die lokale Variable wird meiner Meinung nach nicht vor Beendigung von Main collected. Wenn das Hauptfenster beendet wird, kehrt die Ausführung nochmal in die Main-Methode zurück. Also ist die lokale Variable immer noch erreichbar. Der GC guckt garantiert nicht in der Ausführung nach vorne, ob die Variable irgendwo nochmal verwendet wird oder nicht. Es werden nur die Referenzen gezählt, oder etwa nicht??

Gruss
Pulpapex

1.373 Beiträge seit 2004
vor 18 Jahren

Lass mich raten: Debug-Build? Dort funktioniert das nämlich nicht so ganz, weil im Debug-Build der Jitter die erreichbaren Wurzeln etwas konservativer ermittelt.


using System;
using System.Drawing;
using System.Windows.Forms;

namespace Leak {
	
	public class MemoryHog {
		int[] data;
		int id;
		static int nextID = 0;

		public MemoryHog() {
			id = ++nextID;
			Console.WriteLine( "MemoryHog #{0}", id );
			data = new int[1024 * 1024 * 50];
			for( int i = 0; i < data.Length; ++i ) {
				data[i] = i;
			}
		}

		~MemoryHog() {
			Console.WriteLine( "~MemoryHog #{0}", id);
		}
	}

	static class Program {

		static void HandleButtonClick( object sender, EventArgs e ) {
			// speicher verbrauchen
			MemoryHog hog = new MemoryHog();
		}

		static void Main() {
			MemoryHog memHog = new MemoryHog();
			Form form = new Form();
			Button button = new Button();
			button.Text = "waste memory";
			button.Size = new System.Drawing.Size( 100, 30 );
			button.Click += new EventHandler( HandleButtonClick );
			form.Controls.Add( button );

			Application.Run( form );
		}
	}
}

Teste dieses Programm einmal als Debug- und einmal im Release-Build. Im Release-Build wird das erste - lokale - "MermoryHog" beim Buttonclick und der dadurch ausgelösten GC entfernt. Im Debug-Build wartet der Jitter tatsächlich bis zum Ende von Main, bis das lokale Objekt entfernt wird.

@Pulpapex: Doch, der GC kann quasi in die Zukunft sehen. Warum? Während des Jittens werden an einigen Stellen im Code Tabellen hinterlegt die sagen, welche Objekte bis hierhin noch erreichbar sind.
Wenn die Garbage Collection beginnt, gilt jedes Objekt als "entfernbar". Der GC durchwandert nun z.B. den Callstack, und sucht die vom Jitter erzeugten Tabellen ab. Objekte, die dort erscheinen und alle Unterobjekte gelten als erreichbar und werden nicht mehr gelöscht. Hinzu kommen noch statische Variablen und anderes "Gemüse", aber so ungefähr funktioniert es.
Im Debug Build erzeugt der Jitter nur zu Beginn der Methode, nicht aber zwischendurch Tabellen, sodass der GC davon ausgehen muss, dass alle Objekte erreichbar sind.

MfG VizOne

Suchhilfe: Referenz, GC, 1000 Worte

_
416 Beiträge seit 2005
vor 18 Jahren

Tatsache, man lernt halt wirklich nie aus. Also ich nehm alles zurück und behaupte das Gegenteil.

Obwohl das schon wieder so ein Fall ist dass mir die Haare zu Berge stehen. Ich hatte schon öfters den Fall, dass eine im Debug-Modus einwandfrei getestete Version im prlötzlich Release rummuckte. X(