Speicher und Garbage Collector in Go: Wie man mit dem GC nicht verrückt wird
\nHaben Sie sich jemals gefragt, wohin der Speicher in Ihrer Go-Anwendung verschwindet? Besonders wenn sie anfängt, Gigabytes zu fressen, und Sie nur JSON parsen? Kommt Ihnen bekannt vor? Willkommen in der Welt des Garbage Collectors (GC) in Go – dem mysteriösen Mechanismus, der entweder Ihr Projekt rettet oder es in eine lahme Plattform verwandelt. Heute schauen wir uns an, wie Speicher in Go funktioniert, wie der GC entscheidet, wann aufgeräumt werden muss, und wie Sie nicht in die "STW-Wartefalle" tappen.
\nWenn Sie seit mindestens ein paar Monaten in Go programmieren, sind Sie dem schon begegnet: plötzliche Pausen, steigender Speicherverbrauch, rätselhafte gc pause in Profilen. Werfen wir einen Blick unter die Haube und verstehen, wie man Speicher in Go bewusst verwaltet.
Wie Speicher in Go organisiert ist: Heap und Stack
\nBevor wir über den Garbage Collector sprechen, lassen Sie uns verstehen, wo die Daten in Go überhaupt leben. Anders als in Sprachen wie C, wo Sie selbst für malloc/free verantwortlich sind, verwendet Go automatische Speicherverwaltung. Aber das bedeutet nicht, dass alles in einen Topf geworfen wird.
\nIn Go gibt es zwei Hauptspeicherbereiche:
\n- \n
- Stack – schneller, automatisch verwalteter Speicher für lokale Variablen. Jede Goroutine hat ihren eigenen Stack (beginnt mit 2 KB und wächst dynamisch). \n
- Heap – langsamerer, aber flexibler Speicher für Daten, die einen Funktionsaufruf "überleben". \n
Die Hauptfrage: Was landet im Heap und was bleibt auf dem Stack? Dafür ist die Escape Analysis zuständig – ein intelligenter Mechanismus des Go-Compilers. Er analysiert, ob eine Variable den Gültigkeitsbereich der Funktion "verlässt". Wenn ja – geht sie in den Heap. Wenn nicht – bleibt sie auf dem Stack, und der GC erfährt nichts von ihr.
\nBeispiel:
\npackage main\n\nfunc createPointer() *int {\n x := 42\n return &x // x "entkommt" – landet im Heap\n}\n\nfunc noEscape() int {\n x := 42\n return x // x bleibt auf dem Stack\n}\n\nIm ersten Fall sieht der Compiler, dass der Zeiger auf x nach außen zurückgegeben wird, und platziert die Variable im Heap. Im zweiten Fall ist alles einfach und schnell. Das ist Speicherverwaltung in Go auf Mikroebene.
Der Garbage Collector in Go: Wie er funktioniert
\nKommen wir zum Hauptdarsteller – dem Garbage Collector (GC). In Go wird ein paralleler, dreifarbiger, nicht-inkrementeller Collector mit dem "Mark-and-Sweep"-Algorithmus verwendet. Klingt beängstigend? Eigentlich ist alles logisch.
\nDer GC in Go durchläuft drei Phasen:
\n- \n
- Mark Setup (STW) – eine kurze Pause zum Aktivieren des Write Barriers. Dauert normalerweise 10-30 Mikrosekunden. \n
- Marking (Concurrent) – die Hauptarbeit: Der GC durchläuft den Objektgraphen und markiert lebende Objekte. Wird parallel zu Ihrem Code ausgeführt. \n
- Sweep (Concurrent + STW) – Bereinigung toter Objekte. Wird teilweise parallel erledigt, aber die finale Synchronisation kann eine kurze Pause verursachen. \n
Eine Schlüsseleigenschaft des GC in Go ist, dass er keine Defragmentierung durchführt. Das bedeutet, dass der Speicher nach der Bereinigung fragmentiert sein kann, aber Go verwendet Bitmaps zur Verfolgung freier Blöcke, was die Speicherzuweisung schnell macht.
\nEin Beispiel der GC-Arbeit sieht man in den Logs:
\nGODEBUG=gctrace=1 go run main.go\n\ngc 1 @0.013s 4%: 0.031+0.42+0.017 ms clock, 0.25+0.10/0.42/0.0+0.13 ms cpu, 4->4->2 MB, 5 MB goal, 8 P\n\nHier: 0.031+0.42+0.017 ms – das ist STW + Mark + STW. 4->4->2 MB – Heap-Größe vor, nach dem Mark und nach dem Sweep.
Warum der GC bremst: STW und Pausen
\nDer Hauptfeind der Leistung ist STW (Stop The World). Obwohl Go versucht, Pausen zu minimieren, treten sie dennoch auf. Besonders wenn:
\n- \n
- Sie viele Zeiger haben (der GC muss alle Objekte durchlaufen) \n
- Der Heap groß ist (mehr Objekte – längeres Marking) \n
- Sie viel Müll erzeugen (hohe Allokationsrate) \n