Packfiles und Garbage Collection: Wie Git Speicher optimiert
git
HEAD
Git · Internals · Packfiles · Garbage Collection
Packfiles und Garbage Collection: Wie Git Speicher optimiert
Von losen Objekten zu delta-komprimierten Packs

Ein gewachsenes .git-Verzeichnis, das mehrere Gigabyte umfasst, ist selten ein Zufall, sondern die Summe jedes Commits, jedes Rebase und jeder versehentlich eingecheckten Binärdatei, die Git niemals von selbst löscht. Dieser Artikel erklärt, wie lose Objekte zu delta-komprimierten Packfiles werden, was git gc technisch wirklich tut, welche Rolle der Reflog beim Schutz vor Datenverlust spielt, und wie man ein aufgeblähtes Repository gezielt diagnostiziert und wieder verschlankt.

13 Min. Lesezeit Packfiles · Delta-Kompression · git gc Git 2.x · CLI · Repository-Wartung

1. Warum Git-Repositories mit der Zeit aufblähen

Jeder Commit, jedes Amend, jedes Rebase erzeugt neue Objekte in .git/objects, aber Git löscht diese Objekte bei normaler Arbeit nie automatisch. Auch ein git commit --amend oder ein interaktives Rebase überschreibt keinen alten Commit, sondern erstellt einen neuen und lässt den alten Commit unangetastet im Objektspeicher zurück, erreichbar über den Reflog. Über Monate summiert sich das: Hunderte überschriebene Commits, verworfene Branches und experimentelle Merges bleiben technisch vorhanden, auch wenn kein Branch mehr auf sie zeigt.

Besonders schwer wiegen versehentlich committete Binärdateien: Ein zehn Megabyte großes PDF oder ein Datenbank-Dump, der Sekunden später mit git rm wieder entfernt wird, bleibt trotzdem für immer Teil der Historie, weil jede Dateiversion als eigenes, unveränderliches Blob-Objekt gespeichert wird. Das erklärt, warum ein .git-Verzeichnis oft um ein Vielfaches größer ist als der aktuelle Checkout, und warum git clone in gewachsenen Projekten überraschend lange dauert.

2. Lose Objekte: eine Datei pro Objekt

Jedes neue Objekt landet zunächst als loses Objekt unter .git/objects/xx/yyyy..., wobei xx die ersten zwei Zeichen des SHA-Hashes sind und der Rest den Dateinamen bildet. Der Inhalt wird per zlib deflate komprimiert, aber jedes Objekt bleibt eine eigene Datei auf der Festplatte. Für ein kleines Repository mit wenigen hundert Commits ist das völlig unproblematisch und sogar praktisch, weil jedes Objekt sofort einzeln lesbar ist.

Bei tausenden Commits wird die Zahl der losen Objekte schnell zum Problem, nicht wegen der Kompression, sondern wegen des Dateisystem-Overheads: Jede Datei belegt mindestens einen Inode, viele Dateisysteme werden bei hunderttausenden kleinen Dateien in wenigen Verzeichnissen spürbar langsamer, und jeder Lesevorgang bedeutet einen eigenen Systemaufruf. Das folgende Kommando macht sichtbar, wie viele lose Objekte tatsächlich existieren, bevor überhaupt ein git gc gelaufen ist.


# Count loose object files directly on disk
$ find .git/objects -type f -not -path "*/pack/*" | wc -l
14832

# Total size of the .git directory
$ du -sh .git
612M    .git

# Git's own summary: loose vs. packed objects and their sizes
$ git count-objects -v
count 14832
size 187344
in-pack 92150
packs 3
size-pack 421088
prune-packable 0
garbage 0
size-garbage 0

3. Packfiles: viele Objekte in einer einzigen .pack-Datei

Ein Packfile bündelt viele einzelne Objekte in genau zwei Dateien: eine .pack-Datei mit den eigentlichen, oft delta-komprimierten Objektdaten, und eine begleitende .idx-Datei, die einen sortierten Index aus Hash und Byte-Offset enthält, damit Git ein einzelnes Objekt finden kann, ohne das gesamte Pack sequenziell zu durchsuchen. Beide Dateien liegen zusammen unter .git/objects/pack/.

Aus tausenden losen Dateien werden so oft nur zwei, was Checkout- und Clone-Geschwindigkeit spürbar verbessert, weil der Netzwerktransfer nur noch ein einziges großes, bereits komprimiertes Objekt statt tausender kleiner SSH- oder HTTP-Anfragen umfasst. git gc erstellt Packfiles automatisch, aber auch git repack -a -d lässt sich jederzeit manuell aufrufen, um alle losen Objekte und bestehenden Packs zu einem einzigen, optimierten Pack zusammenzufassen.

4. Delta-Kompression: Objekte als Differenzen speichern

Innerhalb eines Packs speichert Git die meisten Objekte nicht als vollständige Kopie, sondern als Delta gegenüber einem ähnlichen Basisobjekt, meist einer früheren Version derselben Datei. Zwei Commits, die sich nur in wenigen Zeilen einer großen Datei unterscheiden, benötigen dann lediglich die Differenz plus einen Verweis auf das Basisobjekt, statt den vollständigen Inhalt zweimal zu speichern.

Die Auswahl der Delta-Kandidaten erfolgt heuristisch: Git gruppiert Objekte ähnlicher Größe und ähnlichen Dateinamens und testet dann, welche Kombination die beste Kompression ergibt. pack.window (Standard 10) begrenzt, wie viele Objekte als Basiskandidat verglichen werden, und pack.depth (Standard 50) begrenzt, wie viele Deltas in einer Kette hintereinander erlaubt sind, bevor wieder ein vollständiges Objekt gespeichert wird, um die Rekonstruktionszeit beim Lesen zu begrenzen.

5. git gc im Detail: was beim Garbage Collection wirklich passiert

git gc ist kein einzelner Vorgang, sondern eine Kette mehrerer Wartungsschritte: Lose Objekte werden zu einem neuen Pack zusammengefasst und delta-komprimiert, bestehende, ineffiziente Packs werden gegebenenfalls neu gepackt, abgelaufene Reflog-Einträge werden entfernt, und Objekte, die danach von keinem Branch, Tag oder Reflog-Eintrag mehr erreichbar sind, werden für die Löschung vorgemerkt beziehungsweise entfernt.

Git ruft git gc --auto ohnehin regelmäßig automatisch nach Operationen wie git commit oder git fetch auf, aber nur, wenn Schwellenwerte überschritten sind: gc.auto (Standard 6700 lose Objekte) und gc.autoPackLimit (Standard 50 Packs) bestimmen, ab wann automatisch aufgeräumt wird. Ein manueller Lauf mit --aggressive erzwingt eine gründlichere, aber deutlich langsamere Delta-Suche über das gesamte Repository.


# Standard maintenance: repack loose objects, expire old reflog entries
$ git gc
Enumerating objects: 14832, done.
Counting objects: 100% (14832/14832), done.
Delta compression using up to 8 threads
Compressing objects: 100% (12904/12904), done.
Writing objects: 100% (14832/14832), done.
Total 14832 (delta 8210), reused 0 (delta 0), pack-reused 0

$ du -sh .git
198M    .git

# Aggressive repack with a wider delta search, plus immediate pruning
$ git gc --aggressive --prune=now
Enumerating objects: 106982, done.
Compressing objects: 100% (98410/98410), done.
Total 106982 (delta 61230), reused 92150 (delta 55870)

$ du -sh .git
164M    .git

6. Unreachable Objects und die Rolle des Reflogs

Ein Commit gilt als unreachable, sobald kein Branch, kein Tag und kein anderer erreichbarer Commit mehr auf ihn verweist, etwa nach git commit --amend, git rebase oder git branch -D. Git löscht solche Objekte trotzdem nicht sofort, weil der Reflog, eine lokale Historie jeder Bewegung von HEAD und Branch-Zeigern, sie weiterhin referenziert und damit künstlich am Leben hält.

Standardmäßig verfallen Reflog-Einträge für erreichbare Commits nach 90 Tagen (gc.reflogExpire) und für bereits unreachable Commits nach 14 Tagen (gc.reflogExpireUnreachable). Genau dieses Sicherheitsnetz macht git reflog zum wichtigsten Werkzeug, um versehentlich gelöschte Commits, verlorene Branches oder einen fehlgeschlagenen Rebase innerhalb dieses Zeitfensters problemlos wiederherzustellen, ohne auf ein externes Backup angewiesen zu sein.

7. Pruning: wann Objekte tatsächlich gelöscht werden

Ein Objekt wird erst dann tatsächlich von der Festplatte entfernt, wenn es sowohl unreachable ist, als auch nicht mehr durch einen Reflog-Eintrag geschützt wird, und zusätzlich älter ist als die in gc.pruneExpire konfigurierte Gnadenfrist, standardmäßig zwei Wochen. git gc allein löscht daher in der Praxis selten sofort etwas, sondern räumt hauptsächlich auf und packt neu.

git prune ohne Optionen respektiert dieselbe zweiwöchige Frist, git gc --prune=now hebt sie vollständig auf und entfernt sofort alles, was nicht mehr erreichbar ist. Genau das macht --prune=now riskant: Ist ein Commit gerade erst durch einen Fehlgriff unreachable geworden und noch nicht im Reflog vermerkt, verschwindet er unwiderruflich, ohne die übliche zweiwöchige Rettungsfrist.


# Full sequence to actually purge unreachable history immediately
# WARNING: destructive, only use when you are certain nothing is needed

# 1. Expire every reflog entry right now, removing the safety net
$ git reflog expire --expire=now --all

# 2. List what would be purged: unreachable objects not protected by reflog
$ git fsck --unreachable --no-reflog
unreachable commit a1b2c3d4e5f67890abcdef1234567890abcdef12
unreachable blob 9f8e7d6c5b4a3928170695847362514039281706

# 3. Actually remove the unreachable objects from disk
$ git prune -v
Removing a1b2c3d4e5f67890abcdef1234567890abcdef12
Removing 9f8e7d6c5b4a3928170695847362514039281706

$ git count-objects -v
count 0
size 0
in-pack 92150
packs 1
size-pack 164024

8. Repository-Bloat diagnostizieren und .git verkleinern

git count-objects -v liefert in Sekunden eine Übersicht über Anzahl und Größe loser Objekte sowie bereits vorhandener Packs, ganz ohne das Repository zu verändern. du -sh .git zeigt die tatsächliche Gesamtgröße auf der Festplatte, oft überraschend größer als der aktuelle Arbeitsstand, wenn die Historie viele große, längst gelöschte Dateien enthält.

Um die größten Übeltäter innerhalb eines Packs zu finden, hilft git verify-pack -v kombiniert mit einer Sortierung nach Objektgröße. Sind die größten Objekte identifiziert und stellen sich als versehentlich committete Binärdateien heraus, reicht git gc allein nicht aus, denn diese Objekte bleiben Teil der Historie. Nur ein Historien-Rewrite mit git filter-repo entfernt sie endgültig aus allen Commits, gefolgt von einem harten Prune aller alten Referenzen. Für regelmäßige, unbeaufsichtigte Wartung auf zentralen Bare-Repositories eignet sich ein geplanter Workflow-Lauf besser als manuelle Eingriffe.


# Find the largest objects inside a packfile by uncompressed size
$ git verify-pack -v .git/objects/pack/pack-*.idx \
    | sort -k3 -n -r \
    | head -n 5
7c2b1a9f... blob   9484032 2103841 481920
3e8d4f21... blob   4210688  982104 512004
b1a0f7c3... blob   2094112  601022 918233
90fa2e11... blob   1887744  399102 105881
e4c5d6a7... blob   1024006  287441  22019

# Cross-reference the largest hash with the file path it belongs to
$ git rev-list --objects --all | grep 7c2b1a9f
7c2b1a9f... assets/legacy-export-2019.sql.gz

# .github/workflows/git-maintenance.yml
# Scheduled maintenance for a self-hosted bare mirror repository
name: Git repository maintenance

on:
  schedule:
    # Every night at 02:15 UTC
    - cron: "15 2 * * *"
  workflow_dispatch: {}

jobs:
  maintain:
    runs-on: self-hosted
    steps:
      - name: Run incremental git maintenance
        run: |
          cd /srv/git/mirrors/mironsoft.git
          git maintenance run --task=loose-objects
          git maintenance run --task=incremental-repack

      - name: Full weekly repack with aggressive delta search
        if: github.event.schedule == '15 2 * * 0'
        run: |
          cd /srv/git/mirrors/mironsoft.git
          git gc --aggressive --prune=now

      - name: Report repository size after maintenance
        run: |
          du -sh /srv/git/mirrors/mironsoft.git

9. Speicher- und Wartungsstrategien im Vergleich

Die folgende Übersicht fasst zusammen, welches Symptom auf welche Ursache hindeutet, mit welchem Befehl sich das diagnostizieren lässt, und welche Maßnahme tatsächlich hilft, statt pauschal git gc aufzurufen und zu hoffen.

Situation Symptom Diagnose-Befehl Lösung
Viele kleine Commits ohne Wartung Tausende lose Dateien unter .git/objects git count-objects -v git gc (repackt automatisch)
Versehentlich committetes Binary .git um ein Vielfaches größer als Checkout git verify-pack -v | sort -k3 -nr git filter-repo + git gc --prune=now
Amend- und Rebase-lastige Historie git gc löscht sichtbar nichts git reflog 90 Tage warten oder reflog gezielt expiren
Viele kleine Packs statt einem großen Clone und Checkout werden langsamer ls .git/objects/pack | wc -l git repack -a -d oder git maintenance run
Sofortiges Löschen gewünscht git prune tut scheinbar nichts git count-objects -v git gc --prune=now (riskant, kein Sicherheitsnetz)

Für dauerhaft schlanke Repositories lohnt sich außerdem ein regelmäßiger, automatisierter Wartungslauf statt punktueller manueller Eingriffe, insbesondere auf zentralen Mirror- oder Bare-Repositories, auf die viele CI-Jobs täglich zugreifen.

Mironsoft

Repository-Wartung, CI/CD-Performance und Git-Infrastruktur für PHP- und Magento-Teams

Ein aufgeblähtes Repository wieder schlank machen?

Wir analysieren eure .git-Historie, finden die größten Übeltäter, entfernen sie sicher aus der Vergangenheit und richten automatisierte Wartungsläufe ein, damit euer Repository dauerhaft schlank bleibt.

Repository-Audit

Größte Objekte, unreachable Commits und Bloat-Ursachen identifizieren

History-Rewrite

Versehentliche Binärdateien und Secrets sicher aus der Historie entfernen

Wartungsautomatisierung

Geplante git maintenance und gc-Läufe für Mirrors und CI-Pipelines

10. Zusammenfassung

Git speichert jedes Objekt zunächst als lose Datei, bündelt sie aber über git gc oder git repack zu Packfiles, die mittels Delta-Kompression gegen ähnliche Basisobjekte deutlich kleiner sind als die Summe ihrer Einzeldateien. Unreachable Commits verschwinden nicht sofort, sondern bleiben über den Reflog standardmäßig 90 Tage lang als Sicherheitsnetz erreichbar, bevor sie überhaupt für ein Pruning infrage kommen.

Erst wenn ein Objekt weder erreichbar noch durch den Reflog geschützt noch jünger als die zweiwöchige gc.pruneExpire-Frist ist, entfernt Git es tatsächlich von der Festplatte, sei es über den regulären git gc-Zyklus oder explizit über git prune. Für strukturell große Objekte wie versehentlich committete Binärdateien reicht das allein nicht: Nur ein Historien-Rewrite mit git filter-repo, gefolgt von einem sofortigen Prune, entfernt sie endgültig und macht Diagnosewerkzeuge wie git count-objects und git verify-pack zum festen Bestandteil regelmäßiger Repository-Wartung.

Packfiles und Garbage Collection auf einen Blick

Lose Objekte vs. Packfiles

Eine Datei pro Objekt unter .git/objects/xx/ wird zu einer einzigen .pack- und .idx-Datei gebündelt.

Delta-Kompression

Ähnliche Objekte werden als Differenz gegenüber einem Basisobjekt gespeichert, statt vollständig dupliziert.

Reflog & Pruning

Unreachable Objekte bleiben 90 Tage über den Reflog geschützt, gc.pruneExpire verzögert die Löschung um 2 Wochen.

Diagnose & Wartung

git count-objects -v, git verify-pack -v und geplante git maintenance-Läufe halten Repositories schlank.

11. FAQ: Packfiles und Garbage Collection

1Was ist der Unterschied zwischen losen Objekten und einem Packfile?
Lose Objekte sind einzelne, zlib-komprimierte Dateien unter .git/objects/xx/. Ein Packfile bündelt viele Objekte in einer .pack- plus .idx-Datei mit zusätzlicher Delta-Kompression.
2Was macht git gc konkret in einem Lauf?
Packt lose Objekte delta-komprimiert neu, packt ineffiziente Packs um, entfernt abgelaufene Reflog-Einträge und löscht unreachable, ungeschützte Objekte.
3Wie lange bleiben unreachable Commits über den Reflog erreichbar?
90 Tage für erreichbare Commits (gc.reflogExpire), 14 Tage nach dem Unreachable-Werden (gc.reflogExpireUnreachable). Beide Werte sind konfigurierbar.
4Wann löscht Git ein unreachable Objekt tatsächlich?
Wenn es unreachable ist, keinen Reflog-Schutz mehr hat und die gc.pruneExpire-Frist von standardmäßig zwei Wochen abgelaufen ist.
5Ist git prune gefährlich?
Ohne Optionen relativ sicher wegen der zweiwöchigen Gnadenfrist. git gc --prune=now oder --expire=now sind riskant, da sie diese Frist umgehen.
6Was ist der Unterschied zwischen git gc und git gc --aggressive?
Standard-gc nutzt moderate pack.window/depth-Werte und ist schnell. --aggressive sucht gründlicher nach besseren Deltas, dauert aber deutlich länger.
7Wie finde ich die größten Objekte in meinem Repository?
git verify-pack -v kombiniert mit sort -k3 -n -r und head zeigt die größten Objekte, deren Pfad sich über git rev-list --objects --all ermitteln lässt.
8Reicht git gc, um eine versehentlich committete Binärdatei loszuwerden?
Nein, git gc entfernt nur unreachable Objekte. Eine noch referenzierte Datei bleibt Teil der Historie und muss mit git filter-repo entfernt werden.
9Was ist Delta-Kompression und wie wählt Git die Basisobjekte aus?
Objekte werden als Differenz zu einem ähnlichen Basisobjekt gespeichert. Git gruppiert nach Größe/Name und testet innerhalb des pack.window die beste Kombination.
10Wie halte ich ein zentrales Bare-Repository dauerhaft klein?
Mit einem geplanten Wartungsjob wie git maintenance run oder einem Cron-gesteuerten git gc in der CI/CD-Pipeline statt manuellen Eingriffen.