[F]Memory Leaks
-
So hier ist nun endlich der Artikel. Ich hatte in letzter Zeit nicht viel Zeit und daher ist dies auch noch nicht die entgültige Version.
Ich will alles nocheinmal auf Inhalt durchgehen. Dennoch kann man sehen, in welche Richtung der Artikel geht, daher würde ich gerne schon etwas Feed-Back hören.Inhalt
1. Einleitung
2. Die Grundidee
3. Funktionen zum Speicher Allozieren und Deallozieren
4. Funktionen für Klassen verfügbar machen
5. Anmerkungen und Hinweise
6. Schlusswort
7. Anforderungen / Benötigte Header1. Einleitung
Wie der Titel bereits verlauten lässt, handelt es sich in folgendem Artikel um sogenannte „Memory Leaks“ und wie man diese vermeiden kann.
Doch was genau ist eigentlich ein Memory Leak?
Nun, ein Memory Leak ist eine Stelle im Code, die Arbeitsspeicher verschwendet, also reserviert aber nicht wieder frei gibt. Ein Memory Leak kann dazu führen, dass das Programm abstürzt, der Computer unglaublich langsam läuft oder sogar Windows ohne zu fragen neu startet.
Sehen wir uns folgendes kurzes Programm einmal an:
int main() { char* ch = new char[1024]; // Speicher wird reserviert, aber nicht wieder freigegeben return 0; }
Ein Beispiel für einen Memory Leak.
1024 Bytes in das virtuelle Nirwana zu schicken, erscheint auf den ersten Blick als nicht so schlimm (schließlich handelt es sich um 1024 Bytes), sodass man meinen könnte, dieser Fehler wäre irrelevant.
Doch was passiert, wenn das Programm z.B. auf einem Server liefe, und es jedes Mal, wenn sich jemand auf die Website einklinkt, aufgerufen würde?
Wie wir wissen, macht Kleinvieh auch Mist. Daher würde es auf dem Server recht schnell zu einem Absturz kommen, der den betreffenden Betreiber Arbeitsaufwand oder eventuell sogar Geld kosten könnte.
Daher ist es natürlich klar, dass jeder Programmierer versuchen sollte, diese Fehler möglichst zu vermeiden, damit es keine Probleme mit dem Programm gibt oder Patches aufgespielt werden müssen.2. Die Grundidee
Nun gilt es, eine Möglichkeit für dieses Problem zu finden. Fangen wir doch mit dem oben angeführten Beispielprogramm an. Wäre es nicht toll, wenn sich der Speicher der alloziiert wurde, nicht von selbst bei der Beendigung des Programms löschen würde? Was ist dazu nötig, um dies zu bewerkstelligen?
Ich habe mir gedacht, dass es wohl das sinnvollste wäre, wenn ich den bisher reservierten Speicher speichern, und am Ende des Programms alle Speicherbereiche löschen würde.
Da die Funktionen, die wir programmieren wollen, natürlich alle irgendwie zusammen hängen und auch einfach zu bedienen sein sollen, habe ich mich entschlossen, sie in eine DLL zu verknüpfen.Jetzt bleibt nur noch eine Frage offen, nämlich die, was die DLL überhaupt beinhalten soll:
- Eine Funktion, die neuen Speicher alloziiert und in einer std::list speichert.
- Eine Funktion, die alten Speicher freigibt und diesen aus der std::list löscht.
- Eine Funktion, die nach der Beendigung des Programms allen noch nicht gelöschten Speicher löscht.
Nachdem nun alles geklärt ist, wie wir weiter vorgehen werden, ist es an der Zeit, mit dem eigentlichen Programmieren anzufangen.
3. Funktionen zum Speicher allozieren und deallozieren
3.1 Projekt erstellen
Nachdem wir uns ein leeres Win32 DLL Projekt erstellt haben, kann es auch schon losgehen. Ich habe die DLL „MemoryManager“ getauft.
3.2 Voraussetzungen für die Funktionen schaffen
So, nun geht es endlich mit der Programmierung los.
Als erstes brauchen wir eine globale Variable, die den allozierten Speicher speichert. Ich habe mich hier für eine std::list entschieden, da hier Einfügen als auch Löschen in der Liste sehr schnell vonstatten gehen. Diese sollte auf jeden Fall in der cpp Datei stehen, da es sonst Probleme mit dem Linker gibt, wenn die .h Datei in mehr als einer .cpp Datei eingebunden wird.// MemoryManager.cpp: std::list<void*> g_MemoryList; // void* weil der Zeiger Typ unabhängig gespeichert werden soll.
3.3 Funktion zum Allozieren von Speicher
Unsere erste Funktion soll uns mit neuem Speicher versorgen. Damit diese Typ unabhängig arbeiten kann, muss sie einen leeren Zeiger (void*) zurückgeben. Aufgabe der Funktion ist es, uns neuen Speicher zurückzugeben, dazu benötigt sie zwei Parameter. Erstens size_t objsize und zweitens size_t arraysize, also die Größe des zu allozierenden Objekts und die Größe des Arrays, der Defaultparameter soll hier 1 betragen (1=kein Array).
Kommen wir nun zum Funktionsaufbau:__declspec(dllexport) void* MMAllocMemory(size_t objsize,size_t arraysize) { if(size <= 0) throw invalid_argument("MMAllocMemory:size <= 0"); if(arraysize <= 0) throw invalid_argument("MMAllocMemory:arraysize <= 0"); void* pMemory = ::operator new(size*arraysize); g_MemoryList.push_back(pMemory); return pMemory; }
Wie man sehen kann, wirft die Funktion Exceptions bei falschen Angaben, schließlich ist es nicht Aufgabe der Funktion bei falschen Angaben einfach 0 zurückzugeben. Es bleibt eben ein schwerer Fehler, der auch in dem operator new eine Exception vom Typ bad_alloc zur Folge hätte.
Natürlich kann man die throw invalid_argument(…); Anweisung auch durch return NULL; ersetzen. Falls man aber nun versuchen würde den NULL Zeiger zu verwenden, entsteht ebenfalls ein Fehler. Dieser ist aber meist schwerer zu finden.
3.4 Funktion zum Deallozieren von Speicher
Jetzt haben wir eine Möglichkeit Speicher zu reservieren. Aber wir brauchen natürlich auch eine, mit der wir den Speicher wieder freigeben können.
Die Aufgaben der Funktion sind einfach: Speicher löschen, aus der Liste löschen und danach den Zeiger auf NULL setzen.
Damit wir den Speicher auf NULL setzen können, brauchen wir also eine Referenz auf einen Zeiger.
Schließlich entsteht folgender Code:
__declspec(dllexport) bool MMFreeMemory(void*& pMemory) { if(pMemory == NULL) throw invalid_argument("MMFreeMemory:pMemory==NULL"); list<void*>::iterator iter = g_MemoryList.begin(); for(iter;iter!=g_MemoryList.end();iter++) { if((*iter)==pMemory) { delete (*iter); g_MemoryList.erase(iter); return true; } } return false; }
Nun muss man noch eine Funktion entwerfen, die den gesamten Speicher löscht.
__declspec(dllexport) void MMFreeAllMemory() { list<void*>::iterator iter = g_MemoryList.begin(); for(iter;iter!=g_MemoryList.end();iter++) { delete (*iter); } g_MemoryList.clear(); }
3.5 Speicher nach Beendigung des Programms löschen
Jetzt haben wir alle Funktionen erstellt, die wir benötigen, um unseren Speicher schon einigermaßen gut zu verwalten. Doch wie schaffen wir es, den Speicher automatisch nach Programmende wieder freizugeben?
Hier hilft uns die DLL selbst, denn sie hat (wie jedes eigenständige Programm) auch eine main Funktion. Nur heißt diese nicht „main“ oder „WinMain“ sondern „DllMain“. Sie wird auch aufgerufen, wenn die DLL nicht mehr benötigt wird, also das Programm beendet wird. Das ist genau das, was wir brauchen.
BOOL DllMain(HANDLE hDllHandle,DWORD dwReasonForCall, LPVOID lpReserved) { if(dwReasonForCall == DLL_PROCESS_DETACH) MMFreeAllMemory(); return TRUE; }
Schon ist unsere Bibliothek für den Anfang ausreichend.
3.6 Worauf man achten muss
Diese Funktionen sind keinesfalls perfekt! Im Gegenteil, sie haben sogar einige schwere Fehler! Schauen wir uns folgenden Code an.
int* pInt = (int*)MMAllocMemory(sizeof(int)); // Allozieren eines ints. Klappt! int* pIntArr = (int*)MMAllocMemory(sizeof(int),4) // Allozieren eines Arrays. Klappt! Vector<string>* pVec (vector<string>*)MMAllocMemory(sizeof(vector<string>)); // Allozieren eines std::vectors. Klappt nicht! MMFreeMemory((void*&)pInt); MMFreeMemory((void*&)pIntArr); MMFreeMemory((void*&)pVec);
Wo liegt der Fehler? Warum klappt es denn nicht? Und warum funktioniert es denn mit den ints?
Unsere entwickelten Funktionen sind zwar in der Lage, Speicher zu reservieren und wieder freizugeben, aber sie rufen niemals den Konstruktor eines Objektes auf, da die Funktionen den zu allozierenden Speicher als Typ unabhängig betrachten!
Mit den Integers ist es etwas anderes. Diese müssen nicht konstruiert werden. Daher wird die Funktion mit allen Strukturen, Klassen, Unionen funktionieren, die in ihrem Konstruktor nichts konstruieren müssen. Folglich sollte für eine Klasse, die nur einen oder mehrere ints beinhaltet, problemlos Speicher reserviert werden.
Eine Klasse, die einen std::vector beinhaltet, funktioniert nicht, da der vector konstruiert werden muss und das nicht der Fall ist, wenn der Konstruktor der eigentlichen Klasse nicht aufgerufen wird.4. Funktionen für Klassen verfügbar machen
Wie in dem Titel bereits formuliert, wollen wir klären, ob und wie sich das Problem mit Klassen lösen lässt.
C++ beinhaltet die Möglichkeit, Operatoren zu überladen. Diese Möglichkeit wollen wir nutzen, um das Problem in den Griff zu bekommen, indem wir operator new und operator delete bzw. ihre Array Gegenstücke überladen.
Die Operatoren sollen nicht das normale new überdecken, denn der Benutzer soll immer noch die Freiheit haben, die Bibliothek nicht zu benutzen, wenn er es nicht wüscht. Außerdem können wir selbst keinen Speicher mehr reservieren, wenn wir den normalen new Operator überdecken. Also müssen wir die Operatoren in einer Klasse unterbringen, von der später abgeleitet werden kann, um die Funktion der Speichersicherung zu unterstützen.
Ich habe die Klasse hier einfach „MMObject“ getauft.
So entsteht folgender Code:
class __declspec(dllexport) MMObject { public: MMObject(void); virtual ~MMObject(void) = 0; static void* operator new(size_t size); static void* operator new[](size_t size); static void operator delete(void* rawMemory); static void operator delete[](void* rawMemory); }; MMObject::MMObject(void) { } MMObject::~MMObject(void) { } void* MMObject::operator new(size_t size) { void* ptr = MMAllocMemory(size); if(ptr == NULL) throw std::overflow_error("Arbeitsspeicher voll"); return ptr; } void* MMObject::operator new[](size_t size) { void* ptr = MMAllocMemory(size); if(ptr == NULL) throw std::overflow_error("Arbeitsspeicher voll"); return ptr; } void MMObject::operator delete(void* rawMemory) { if(rawMemory==NULL) throw std::invalid_argument("MMObject::operator delete: rawMemory = NULL"); MMFreeMemory(rawMemory); } void MMObject::operator delete[](void* rawMemory) { if(rawMemory==NULL) throw std::invalid_argument("MMObject::operator delete[]: rawMemory = NULL"); MMFreeMemory(rawMemory); }
Das ist der komplette Code der Klasse. Das einzige was sie beinhaltet, ist ein virtueller Defaultdestruktor, der nichts erledigt (daher auch mit =0 gekennzeichnet), ein Konstruktor, der ebenfalls nichts tut und noch vier Funktionen für das Speichermanagement, diese garantieren uns, dass der Konstruktor des Objekts aufgerufen wird.
Operator new und Operator delete kommen einmal als normale Fassung vor und einmal als Arrayfassung. Beide sind nötig, damit der komplette Speicher gesichert wird.
Auch hier lasse ich Exceptions zurückgeben, falls ein Fehler auftritt.
Wie man sieht, lehnen sich die Operatoren stark an die schon entwickelten Funktionen an, sodass nicht mehr viel zu erklären ist. Um nun einer Klasse zu ermöglichen mit dem MemoryManager zu arbeiten, muss man diese nur von MMObject ableiten.
5. Anmerkungen und Hinweise
Es gibt immer noch etwas, worauf man achten muss, um keinen Fehler zu erzeugen.
5.1 Zugriffsfehler
Hat man beispielsweise einen int mittels MMAllocMemory() alloziert, so kann man den Speicher auf keinen Fall mit dem normalen delete löschen, denn: Der Speicher gehört der Anwendung gar nicht, sondern der DLL, die von der Anwendung benutzt wird. So kann zwar auf einen Speicherbereich der DLL geschrieben oder von ihm gelesen werden, aber er kann niemals gelöscht werden!
5.2 Speichernutzung des MemoryManagers
Nun noch etwas zu dem Speicher, den der MemoryManager selbst benötigt.
Viel Arbeitsspeicher benötigt er nicht, da er nicht die Objekte selbst speichert, sondern die Zeiger der Objekte. Diese benötigen wesentlich weniger Speicherplatz als ein ganzes Objekt. Somit ist der Speicherverbrauch eher gering.
5.3 Vereinfachung
Sehen wir uns einmal folgenden Codeausschnitt an:
int* pInt = (int*)MMAllocMemory(sizeof(int)); MMFreeMemory((void*&)pInt);
Hier fragt man sich zu Recht, ob das denn nicht einfacher gehe. Natürlich geht es. Man braucht nur einige geeignete Makros, die die Arbeit erleichtern:
#define MM_ALLOC(memory,typ) { memory=(typ*)MMAllocMemory(sizeof(typ)); } #define MM_ALLOC_ARRAY(memory,typ,arraysize) { memory=(typ*)MMAllocMemory(sizeof(typ),arraysize); } #define MM_SAFE_RELEASE(memory) { MMFreeMemory((void*&)memory); } #define MM_SAFE_DELETE(memory) { delete memory; memory = NULL; } #define MM_SAFE_DELETE_ARRAY(memory) {delete[] memory; memory = NULL; }
Was diese einzelnen Makros letztlich tun, ist klar. Sie vereinfachen die Schreibarbeit enorm.
Was vorher so ausgedrückt werden musste,
int* pInt = (int*)MMAllocMemory(sizeof(int)); MMFreeMemory((void*&)pInt);
kann man jetzt viel einfacher beschreiben:
int* pInt = NULL; MM_ALLOC(pInt,int); MM_SAFE_RELEASE(pInt);
Das Beste daran ist, dass man nicht mehr so viele Klammern setzen muss.
Zur Namensgebung der Makros muss ich sagen, dass ich sie einem Buch entnommen habe, weil ich sie sehr passend formuliert fand.
6. Schlusswort
Ich hoffe natürlich, dass euch mein Artikel gefallen hat! Ich habe das Projekt mit dem Visual Studios 2003 Compiler programmiert und kompiliert. Hier sollte es also keinerlei Probleme geben!
7. Anforderungen / Benötigte Header
Benötigter Header ist nur „list.h“ und natürlich darf man in den .cpp Dateien nicht „using namespace std;“ vergessen!
-
Hallo,
Insgesamt schonmal ganz schön. Ich fände einen kleinen Beispielcode zur Verwendung noch ganz nett.
Außerdem fände ich es interessant, mich nicht auf diesen Memory-Manager festzulegen. Sprich: ich schreibe new und je nachdem, ob ich USE_MEMORY_MANAGER definiert habe oder nicht wird's so oder so verwendet.
Letztlich würde ich sowas hauptsächlich benutzen wollen, um während der Entwicklung testen zu können, ob mein Programm brav alles wieder aufräumt. Ich weiß aber nicht, inwiefern das in das Konzept dieses Artikels paßt. Vielleicht wäre das auch eher was eigenes.
-
Ich habe den Artikel noch einmal gelesen uns schon mal ein paar Fehler ausgemerzt.
Nun zu dir Jester:
Dein Vorschlag ist ja schon mal nicht schlecht!
Das Problem ist nur, dass USE_MEMORY_MANAGER ein Makro ist, dass irgentwo in deinem Programm mit #define festgelegt wurde.Die Klasse, die new und delete überläd befindet sich aber in einer schon kompilierten und verlinkten DLL, daher würde diese Möglichkeit ausscheiden.
Die einzige Möglichkeit soetwas zu implementieren ist, dass ich vllt. eine statische Variable einbaue, die regelt, ob der Manager benutzt wird oder nicht.
Wofür meinst du brauche ich noch ein Beispiel. Sag mir das bitte noch einmal genauer, damit ich weiß, welche Funktionen du meinst.
-
SALOMON schrieb:
Nun zu dir Jester:
Dein Vorschlag ist ja schon mal nicht schlecht!
Das Problem ist nur, dass USE_MEMORY_MANAGER ein Makro ist, dass irgentwo in deinem Programm mit #define festgelegt wurde.Die Klasse, die new und delete überläd befindet sich aber in einer schon kompilierten und verlinkten DLL, daher würde diese Möglichkeit ausscheiden.
Die einzige Möglichkeit soetwas zu implementieren ist, dass ich vllt. eine statische Variable einbaue, die regelt, ob der Manager benutzt wird oder nicht.
Okay, das Problem sehe ich ein. Wie gesagt, ich hab's auch nur überflogen und das paßt dann wohl wirklich nicht zu dem Konzept was Du hier vorstellst.
Wofür meinst du brauche ich noch ein Beispiel. Sag mir das bitte noch einmal genauer, damit ich weiß, welche Funktionen du meinst.
Du hast doch am Anfang dieses Beispiel mit dem Speicherloch wo Du ein new char[1024] machst. Um einen schnellen Überblick zu kriegen, was Du mit Deinem Artikel erreichen willst wäre es cool, genau dieses Beispiel mit der Funktionalität Deiner Lib implementiert zu sehen.
Btw. fände ich eine Funktion, die ich fragen kann ob noch Speicher freizugeben ist interessant. So könnte ich prüfen, ob ich sauber gearbeitet habe und sonst ne Fehlermeldung rausgeben.
Ein letztes, der Header von list heißt <list> und nicht <list.h>.
Bis auf das letzte ist aber alles kein Muß. Der Artikel liest sich auch so sehr schön.
-
Jester schrieb:
Ein letztes, der Header von list heißt <list> und nicht <list.h>.
Klar werde ich ändern.
Das Beispiel vom Anfang werde ich auch nochmal ans Ende schreiben. Natürlich mit Benutzung der Funktionen.
Implementieren, dass du nachher eine Liste bekommst, wie viele MemoryLeaks vorhanden sind, ist auch kein Problem. Du kannst sie ja in der MMFreeAllMemory()
zählen. Aber ich bin mir nicht ganz sicher, ob es das ist, was du haben möchtest.
-
Der Artikel sieht nicht schlecht aus, nur ob die Makros wirklich eleganter sind als die direkten Funktionsaufrufe, wage ich zu bezweifeln.
Eventuell wäre es ja nicht schlecht, aus deinen Funktionen einen geeigneten Alloicator zu bauen - damit ließen sich auch die STL-Container in die Speicherverwaltung einbauen.
PS:
void MMObject::operator delete(void* rawMemory) { if(rawMemory==NULL) throw std::invalid_argument("MMObject::operator delete: rawMemory = NULL"); MMFreeMemory(rawMemory); }
Afaik ist "delete NULL;" im Standard explizit erlaubt, sollte also keinen Fehler verursachen.
-
welches problem löst dein code?
bitte nicht das "speicherloch" in der main, das war gar keins.
nach prozessende ist dein speicher wieder freigegeben. du hast mit schrecklichen methoden ein nicht-loch gestopft.echte speicherlöcher gehen so:
int main(){ for(;;){ int* p=new int; } }
und da hast nichtmal andeutungsweise dran gebohrt.
mein server kackt nach wenigen minuten ab. probier ihn doch mal aus. langsamere löscher sorgen dafüpr, daß ein server erst nach 2 wochen abkackt. ein server ist dabei ein prozess, keine maschine.
wenn du lösungen zu speicherlöchern anbieten magst, dann bitte welche, die es erleichtern, den fehlerhaften code, der das speicherloch verursacht hat, zu finden und den fehler zu korrigieren.
//size_t arraysize if(arraysize <= 0)
es ist eigentlich nicht job eines debug-heaps, zu gucken, ob der benutzer da minuszahlen reinmacht. aber size_t ist eh nichtnegativ. warum ist 0 als size verboten?
Vector<string>* pVec (vector<string>*)MMAllocMemory(sizeof(vector<string>));
geht fein mit
//#include <new> Vector<string>* pVec=new(MMAllocMemory(sizeof(vector<string>))) vector<string>);
nur wird innerhalb des vectors dann new benutzt und nicht dein kram. einen fehler in vector wirste nicht finden. auch keinen nichtvirtuellen destruktor einer klasse mit new-benutzendem member (böses loch).
du solltest vielleicht den globalen operator new mit einem debug-new überladen und
#define new new(__FILE__,__LINE__)
machen. und bei programmende nur die probleme anzeigen, nämlich speicher, der nicht deleted wurde, und zwar vor allem die position im quellcode, damit man die löcher jagen und vernichten kann.
den aktuellen weg brauchste nicht weiterzuverfolgen.
-
Ich faende es sinnvoller in einem C++ Artikel ueber mem leaks ueber smartpointer zu sprechen als selbst eine dll zu implementieren die a) betriebssystem abhaengig, b) nicht objektorientiert, c) nicht thread-safe, d)umstaendlich da ohne templates und schliesslich e) eine funktionalitaet bietet, die von smartpointern deutlich besser bereit gestellt wird.
Sorry das ist hart, aber lieber wenig gute Artikel als einen haufen schlechter.
edit: ich sehe, dass cstoll einen Artike ueber Speicherverwaltung geschrieben hat, der deckt das eigentlich ab...
-
Korbinian schrieb:
edit: ich sehe, dass cstoll einen Artike ueber Speicherverwaltung geschrieben hat, der deckt das eigentlich ab...
Nur mit der Entdeckung von Speicherlecks habe ich mich dort nicht beschäftigt.
(und bei genauem Betrachten hat Volkard recht - der Code erledigt nur Aufräumarbeiten, die das System bei Programmende sowieso machen würde (und wie du selbst bemerkt hast, ist er C++-untauglich), ohne etwas gegen echte Speicherlecks zu unternehmen)
-
Aber ist ja alles nicht schlimm. Volkard hat ja auch wunderbare Ideen aufgezeigt, was man stattdessen machen könnte. Dadraus kriegt man sicher nen schönen Artikel hin.
-
Ja gut, dann werde ich das evntl. machen. Ich werde aber warscheilich so schnell eh keine Zeit finden (ist momentan knapp bemessen).
-
Nur so als Anmerkung: Ich hab da mal einen Artikel geschrieben (aber Englisch):
http://www.codeproject.com/tools/leakfinder.asp